《編寫可讀代碼的藝術》讀書筆記

前面的話:

        我發現你們都喜歡在正文前嘮嗑幾句,多是作技術的日常自己就比較沉悶,跟周邊的人也不能無所顧忌的交流,畢竟國人都比較講究含蓄。在網上就不一樣了,能夠暢所欲言,因此每次除了分享知識,還順帶交待下本身,讓技術也就不那麼」冰冷「了。html

嘮嗑的話:

          前幾天看本身的爲知,發現每一年都有個讀書計劃列表,不過大多都沒有完成,今年甚至連計劃都尚未。心裏以爲不安,正好從深圳回來的時候,公司把以前購買的書也一併帶回來了,從書架上挑了一本《編寫可讀代碼的藝術》來看,做爲今年的讀書開篇,順便也把讀書計劃繼續完成。node

 

正文

這本書很薄,花了兩天時間就看完了,內容通俗易懂;jquery

它是根據做者在實際中碰到的問題,由淺入深,從幾個方面來敘述如何編寫可讀的代碼。程序員

書中指正的幾個地方,在我本身作過的項目中是真實存在,有頓悟的感受。web

實現一樣的功能,有的代碼讀起來味同嚼蠟,有的代碼讀起來妙不可言,耐人尋味。ajax

我想每一個coder所追求的無非都是那種書寫代碼時行雲流水般的感受,別人看到時爲之一嘆的知足感。json

本書對想提高本身代碼可讀性有必定幫助,想讀的朋友能夠搜一下。windows

第一部分 表面層次的改進

       每個變量名,每一段註釋,每一組分塊多會對你的可讀性產生影響。若是不想忍受重構時的苦難,那就從基本作起。緩存

Part 2 把信息裝到名字裏

1.選擇專業的詞 服務器

    在不一樣場景中綜合考慮詞所表達的意義,這要求對項目所對應的業務很是熟悉。 其實在項目需求之時,有些專業詞語就應該整理成字典,讓項目組人員熟悉這些詞彙,對項目溝通頗有幫助。

編碼時花點時間去想一個合適的詞,雖然耽誤一些編碼的時間,可是對維護時有極大的好處。

2.找到更有表現力的詞

send --- deliver,dispatch,announce,distribute,route

find --- search,extract,locate,recover

start --- lunch,create,begin,open

make --- create,set up,build,generate,compose,add,new

*英語的同義詞有不少,合適的詞能讓讀代碼的人一眼明白,而不用去猜想。

3.retval,tmp,i,j,k 等臨時變量

retval,tmp除非代碼很短,意義清晰才用。

在循環迭代中,i,j,k能夠適當加上前綴,多重循環中更容易發現錯誤。

4.具體代替抽象

hex_id->id,若是須要注意id的格式

CanListenOnPort()優於ServerCanStart(),直接描述方法要作什麼。

5.爲名字附帶更多信息

id->hex_id

6.帶單位的值

start_ms->start

delay_secs->delay

size_mb->size

讀代碼的人不須要進入方法就知道應該使用什麼樣的值

7.關鍵信息附加

html_utf8->html 若是編碼很重要

pLast->Last 指針last

8.名字長度

小做用域中使用簡短的名字

儘可能不要本身創造縮略,衆所周知的縮略使用沒有問題。

丟掉不須要的詞

ToString->ConvertToString

9.名字格式傳遞含義

static const int kMaxOpenFiles = 100;
class LogReader{
public:
void OpenFile(string local_file);
private:
int offset_;
DISALLOW_COPY_AND_ASSIGN(LogReader);
}

*用下劃線來表示私有變量,確實讓人一眼就明白。

10.格式規範

js中,構造函數使用大寫,普通函數首字母小寫。

jquery庫中,jquery返回的結果也加上$符號。

var $all_image = $('img');

 

Part 3 不會誤解的名字

1.Filter()

究竟是「挑出」仍是「篩選」

results = Database.Objects.Filter("year<=2011");

2.Clip(text,length)

是尾部刪除length長度,仍是裁掉最大長度的length一段

Truncate(text,length)->Clip(text,length);

Truncate(text,max_chars)->Truncate(text,max_length);

3.使用min和max來表示(包含)極限

MAX_ITEMS_IN_CART->CART_TOO_BIG_LIMT;

4.使用first和last表示包含範圍

integer_range(first,last)->integer_range(start,stop);

5.使用begin和end表示包含-排除範圍

PrintEventInRange("OCT 16 12:00am","OCT 17 12:00am")->

PrintEventInRange("OCT 16 12:00am","OCT 16 11:59:59pm")->

6.給布爾值命名

need_password(user_is_authenticated)->read_password

is,has,can,should使單詞更明確

避免使用反義詞

bool use_ssl = false ->disable_ssl = true;

7.與指望匹配

get*如今看成」輕量級訪問器「,若是getXX過於複雜,使用者過於輕率使用。

8.list:size() 

void ShrinkList(list<Node>& list,int max_size){
while(list.size()>max_size){
FreeNode(list.back());
list.pop_back();
}
}

list.size()不是事先算好的數,而是一個O(n)操做。致使ShinkList變爲O(n2)操做。

*C++標準庫list.size()已經改成O(1)操做

9.權衡備選名字

 

Part 4 審美

  • 使用一致的佈局,讓人習慣這種風格。
  • 讓類似的代碼看上去類似。
  • 把代碼行分組,造成代碼塊。

1.使代碼排版優美

如今通常編輯器提供了自動排版。

2.一目瞭然的對齊

經過額外引入行來使得排版緊湊。

3.用方法來整理不規則的代碼

4.使代碼整齊。

5.選有意義的排序。

details,location,phone,email,url.

代碼組織使應該有必定的順序,尤爲是大類。

一旦固定,不要輕易修改,不然會讓人迷惑。

6.把聲明按塊組織起來

C#中有#region 段落 #endregion

7.把代碼分紅「段落」

8.保持我的風格一致。

 

Part 5 註釋

1.不要爲那些從代碼自己就能快速推斷的事實寫註釋

2.不要爲了註釋而註釋

3.改進名字的意義比增長註釋更好。

ReleaseRegistryHandle(RegistryKey* key)->

//This doesn't modify the actual registry.

DeleteRegistry(RegistryKey* key);

4.加入"導演評論「

//二叉樹比用哈希錶快40%

//哈希運算的代價比左/右比較大得多。

*這樣能夠避免了維護人作無謂的優化。

5.爲代碼瑕疵註釋

TODO:尚未處理的事情

FIXME:已知的沒法運行代碼

HACK:比較粗糙的解決方案

XXX:重要問題

6.給常量加註釋

NUM_THREADS = 8; #as long as it's >=2 * num_processors,that's good enough.

7.意料之中的提問

void Clear(){
//Force vector to relinquish its memory(look up "STL swap trick") 
vector<float>().swap(data);
}

8.未雨綢繆

//調用外部服務來發送郵件.(1分鐘後超時) 
void SendEmail(string to,string subject,string body);

9.全局觀註釋

//這個文件包含一些輔助函數,爲咱們文件系統提供了更便利的接口.

//它處理了文件權限以及其餘基本細節.

10.總結性註釋

#find all the itmes that customers purchased for themselves.
for customer_id in all_customers:
   for sale in all_sales[customer_id].sales:
     if sale.recipient==customer_id

 

Part 6 言簡意賅

1.讓註釋緊湊

//CategoryType->(socre,weight)

typedef has_map<int,pair<float,float>> ScoreMap;

2.避免使用不明確的代詞.而使用具體的指代.

the data too big -> it's too big.

3.潤色粗糙的句子

#give higher priority to URLs we've never crawled before.->

#depending on whether we've already crawled this url before,give it a different priority.

4.精確描述函數的行爲

//return the number of lines in this file. × 
//Count how many newline bytes('\n') are in the file. √
int CountLines(string filename){}

5.用輸入/輸出列子來講明特別狀況

//remove the suffix/prefix of 'chars' from the input 'src'. + 
//Example:Strip("abba/a/ba","ab") returns "/a". √ 
String Strip(string src,string chars){ }

6.聲明代碼的意圖

//iterate through the list in reverse order × 
//display each price,from highest to lowest √ 
for(list<Product>::reverse_iterator it = products.rbegin();...)

7.具名函數參數的註釋

Connect(/* timeout_ms = */ 10,/* use_encryption */ false);

8.採用信息量較高的詞

//this class acts as a chahing layer to the database. 
//Canonicalize the street address(remove extra spaces,"avenue"->"ave",etc) 

 

第二部分 簡化循環與邏輯

Part 7 把控制流變得易讀

1.循環參數的順序

length>10 -> 10<=length

比較左側:「被問詢的」表達式,它的值更傾向於不斷變化。

比較右側:用來作比較的表達式,它的值更傾向於常量。

"尤達表示法":null==obj -> obj==null (尤達大師口氣)

*現代編譯器已不需如此

2.if/else語句塊

首先處理正邏輯而不是負邏輯的狀況。

先處理掉簡單的狀況。

首先處理有趣或可疑的狀況。

3.?:條件表達式

簡單直觀的狀況下使用,追求最小化人們理解的時間,而不是最小化行數。

4.避免do/whiile

public bool ListHasNode(Node node,string name,int max_length)
{
  while(node!=null && max_length-->0)
 {
  if(node.name().equals(name))
   return true;
 }
   return false;
}

*爲了省略do而重複body也是愚蠢的。

5.從函數中提早返回

public bool Contains(string str,string substr)
{
  if(str==null || substr==null) return false;
  if(substr.equals("")) return true;
   ...
}

若是不用「保護語句」(guard clause)會很不天然。

6.重名昭著的goto

if(p==NULL) goto exit;
...
exit:
fclose(file1);
fclose(file2);

只有這種形式的goto值得推薦。

7.最小化嵌套

當改動代碼時,從全新的角度審視它,做爲總體看待。

if(user_result==success){
  if(permission_result!=success){
   reply.WrieErrors("error reading permissions");
    reply.Done();
  return;
}
reply.WriteErrors("");
}
else
{
reply.WriteErrors(user_result);
}
reply.Done();

permission_result!=success條件是後來添加,但這樣看起來不直觀。

if(user_result!=SUCCESS)
{
  reply.WriteErrors(user_result);
  reply.Done();
  return;
}
if(permission_result!=SUCCESS){
  reply.WrieErrors(permission_result);
  reply.Done();
  return;
}
reply.WriteErrors("");
reply.Done();

用函數的當即返回來減小嵌套。

8.減小循環內的嵌套

for(int i=0;i<result.size();i++){
 if(results[i]!=NULL)
  non_null_count++;
 if(results[i]->name!=""){
  count<<"considering candidate..."<<endl;
}
}
}

能夠利用continue來作保護

for(int i=0;i<result.size();i++){
if(results[i]==NULL)
continue;
non_null_count++;
if(results[i]->name=="")
continue;
count<<"considering candidate..."<<endl;
}

9.代碼執行流程

線程:不清楚什麼時間執行代碼

信號量/中斷處理:有些代碼隨時有可能執行

異常:可能會從多個函數調用中向上冒泡執行

函數指針和匿名函數:很難知道到底會執行什麼,由於在編譯時還沒決定

虛方法:object.virtualMethod()可能會調用一個未知子類代碼

 

Part 8 拆分超長表達式

1.用解釋變量

if line.split(':')[0].strip()=="root";
...
var username = line.split(';')[0].strip();
if username=="root";

2.總結變量

bool user_owns_document = (request.user.id==document.owner_id);
if(user_owns_document){
//user can edit this document 
...
}
if(!user_owns_document){
...
}

3.使用德摩根定理

not(a or b or c) <=> (not a) and (not b) and (not c)

not(a and b and c) <=> (not a) or (not b) or (not c)

if(!(file_exists && !is_protected)) Error("could not read file").

if(!file_exists || is_protected) Error("could not read file");

4.不要濫用短路邏輯

assert((!(bucket=FindBucket(key))) || !bucket->IsOccupied());

bucket = FindBucket(key);

if(bucket!=NULL) assert(!bucket->IsOccupied());

不要讓代碼成爲思惟的減速帶。

達到簡明整潔的代碼可使用短路邏輯。

if(object && object->method())

...

x = a || b || c;

5.與複雜邏輯爭鬥

struct Range
{
int begin;
int end;
//for example:[0,5) overlaps with [3,8) 
bool OverlapsWidth(Range other);
}
bool Range::OverlapsWith(Range other){
return (begin>=other.begin && begin<other.end) ||
(end > other.begin && end <=other.end) ||
(begin <= other.begin && end >= other.end);}

bool Range::OverlapsWith(Range other)
{
if(other.end<=begin) return false; //they end before we begin 
if(other.begin>=end) return false; //they begin after we end 
return true; //only possibility left:they overlap 
}

從「反方向」解決問題,這裏的反向是「不重疊」。只有兩種可能:

另外一個範圍在這個範圍開始前結束。

另外一個範圍在這個範圍結束前開始。

6.拆分巨大的語句

var update_highlight = function ( message_num ){
var thumbs_up = $("#thumbs_up"+message_num);
var thumbs_down = $("#thumbs_down"+message_num);
var vote_value = $("#vote_value"+message_num);
var hi = "ighlighted";
if(vote_value == "up"){
thumbs_up.addClass(hi);
}
else if(vote_value=="Down"){
...
}
else{
...
}
}

7.簡化表達式創意

void AddStats(const Stats& add_from,Stats* add_to){

#define ADD_FIELD(field) add_to->set_##field(add_from.field()+add_to->field())
ADD_FIELD(total_memory);
...
#undef ADD_FIELD
}

利用宏的特性來簡化重複的工做,可是不鼓吹常用宏。

 

Part 9 變量與可讀性

變量越多,越難所有跟蹤它們的動向。

變量的做用域越大,就須要跟蹤它的動向越久。

變量改變越頻繁,就越難以跟蹤它的當前值。

1.減小變量

var now = datetime.datetime.now();

root_message.last_view_time = now;

沒有拆分任何複雜表達式 。

沒有更多的澄清。

只用過一次,沒有壓縮任何冗餘代碼。

這種變量能夠刪除掉。

2.減小中間結果

var remove_one = function(array,value_to_remove){
var index_to_remove = null;
for(var i =0;i<array.length;i+=1){
if(array[i]==value_to_remove){
index_to_remove=i;
break;
}
}
if(index_to_remove!=null){
array.splice(index_to_remove,1);
}
}

var remove_one = function(array,value_to_remove){
 for(var i =0;i<array.length;i+=1){
   if(array[i]==value_to_remove){
     array.splice(i,1);
  return;
}
}
}

3.減小控制流變量

bool done = false;
while(/* condition */ && !done){
...
if(..){
done = true;
continue;
}
}
while(/* condition */){
...
if(..){
break;
}
}

4.縮小變量的做用域

class LaregeClass{
string str_;
void Method1(){
str_ = ...;
Method2();
}

void Method2(){
//use str_ 
}
//lots of other methods that don't use str_ 
}

class LaregeClass{
void Method1(){
string str = ...;
Method2(str);
}

void Method2(string str){
//use str 
}
//other method can't see str. 
}

將類的成員變量「降格」爲局部變量。

5.C++中的if語句

if(PaymentInfo* info = database.ReadPaymentInfo()){
count << "user paid;"<< info->amount()<<endl;
}

*相似C#中的using語句做用域。

6.JavaScript中建立「私有」變量

submitted = false;//note:global variable 
var submit_form = function(form_name){
if(submitted){
return; //dont't double-submit the form 
}
...
submitted = true;
}

var submit_form = (function(){
var submitted = false;//note:global variable 
return function(form_name){
if(submitted){
return; //dont't double-submit the form 
}
...
submitted = true;
}
}(());

最後的圓括號會使得外層這個匿名函數當即執行,返回內層的函數。

7.JavaScript全局做用域

js中若是定義變量省略了var,這個變量會放在全局做用域中。

var f = function (){
//DANGER:"i" is not declared with 'var'! 
for(i = 0;i<10;i++)
...
}
f();
alert(i); //alerts '10'.'i' is global variable! 

8.JavaScript中麼有嵌套做用域

if(...){
int x =1;
}
x++;//compile-error!'x' is undefined.

可是在Python和JS中,在語句塊中定義的變量會「溢出」到整個函數。這種規則下會變量有一些不一樣的寫法。

9.把定義向下移

不要一開始定義全部變量,把定義移到它的使用以前

def ViewFilterReplies(original_id){
root_message = Messages.objects.get(original_id)
root_message.view_count+=1
root_message.save()
all_replies = Mesages.objects.select(root_id=original_id)
filtered_replies =[]
for reply in all_replies:
if reply.spam_votes <= MAX_SPAM_VOTES:
filtered_replies.append(reply)
return filtered_replies
}

10.只寫一次變量

var setFirstEmptyInput = function (new_value){
for(var i=1;true;i++){
var elem = document.getElementById('input'+i);
if(elm = null)
return null;
if(elem.value === ''){
elem.value = new_value;
return elem;
}
}
}

 

 

第三部分 從新組織代碼

Part 10 抽取不相關子問題

看看某個函數或代碼塊:這段代碼高層次目標是什麼?

對於每一行代碼:它是直接爲了目標而工做嗎?這段代碼的高層次的目標是什麼?

若是足夠的行數在解決不相關的子問題,抽取代碼到獨立函數中。

1.findClosestLocation()

抽取spherical_distnace()用來「計算兩個經緯度座標點之間的球面距離」。

其實就是重構代碼。

2.純代碼工具

當你在想「我但願咱們的庫裏面有xxx()函數」, 那麼就動手寫一個。慢慢會積累很多工具代碼。

3.多用途代碼

ajax_post({
...
success:function(response_data){
var str = "{\n";
for(var key in oj){
str += "" + key + " = " + obj[key] +"\n";
}
return str+"}";
}
});

var format_pretty = function(obj){
var str = "{\n";
for(var key in oj){
str += "" + key + " = " + obj[key] +"\n";
}
return str+"}";
};

抽取format_pretty.當須要處理更多狀況時,能夠對format_pretty進行擴展

var format_pretty = function(obj,indent){
//handle null,undefined,strings,and non-objects. 
if(obj == null) return "null";
if(obj === undefined) return "undefined";
if(typeof obj === "string") return '"'+obj+'"';
if(typeof obj === "object") return string(obj);
if(indent === undefined) indent ="";
//hand(non-null) objects. 
var str = "{\n";
for(var key in oj){
str += "" + key + " = ";
str += format_pretty(obj[key],indent+"") +"\n";
}
return str + indent + "}";
};

4.項目專有功能

CHARS_TO_REMOVE = re.complie(r"[\.]+")
CHARS_TO_DASH = re.compile(r"[^a-z0-9]+")
def make_url_friendly(text):
text = text.lower()
text = CHARS_TO_REMOVE.sub('',text)
text = CHARS_TO_DASH.sub('-',text)
return text.strip("-")

business = Business()
business.name = request.POST["name"]
business.url = "/biz/"+make_url_friendly(business.name)
business.date_created = datetime.datetime.utcnow()
business.save_to_database()

5.簡化已有接口

js中操做cookie比較不理想,因此須要本身重寫。

永遠都不要安於使用不理想的接口。老是能夠本身建立包裝函數來隱藏接口的粗陋細節。

def url_safe_encrypt(obj):
obj_str = json.dumps(obj)
cipher = Cipher("aes_128_cbc",key=PRIVATE_KEY,init_vector=INIT_VECTOR,op=ENCODE)
encrypted_bytes = cipher.update(obj_str)
encrypted_bytes += cipher.final() #flush out the current 128 bit block
return base64.urlsafe_b64encode(encrypted_bytes)
user_info = { "username":"...","password":"..." }
url = "http://.../?user_info="+url_safe_encrypt(user_info)

6.過猶不及

若是上一個問題進一步拆分

def url_safe_encrypt(obj):
obj_str = json.dumps(obj)
return url_safe_encrypt_str(obj_str)

def url_safe_encrypt_str(data):
encrypted_bytes = encrypt(data)
return base64.urlsafe_b64encode(encrypted_bytes)

def encrypt(data):
cipher = make_chiper()
encrypted_bytes = cipher.update(obj_str)
encrypted_bytes += cipher.final() #flush out the current 128 bit block
return encrypted_bytes

def make_cipher()
return Cipher("aes_128_cbc",key=PRIVATE_KEY,init_vector=INIT_VECTOR,op=ENCODE)

引入衆多小函數對可讀性不利,付出代價卻沒有獲得實質價值。

 

PART 11 一次只作一件事

 

1.任務能夠很小

var vote_changed = function(old_vote,new_vote){
var score = get_score();
if(new_vote!==old_vote){
if(new_vote==='Up')
score += (old_vote === 'Down' ? 2:1);
else if(new_vote==='Down')
score -= (old_vote === 'Up'?2:1);
else if(new_vote==='')
score +=(old_vote==='Up'?-1:1);
}
};

var vote_value = function(vote){
if(vote ==='Up')
return +1;
if(vote === 'Down')
return -1;
return 0;
};

var vote_changed = function(old_vote,new_vote){
var score = get_score();
score -= vote_value(old_vote);//remove the old vote 
socre += vote_value(new_vote);//add the new vote 
set_score(score);
};

2.從對象中抽取值

當須要抽取對象中的值時,能夠優先將其提取。

var town = location_info["LocalityName"];
var city = location_info["SubAdministrativeAreaName"];
var state = location_info["AdministativeAreaName"];
var country = location_info["CoountryName"];
var first_half = "Middleof_nowhere";
if(state&&country!="USA")
first_half = state;
if(city)
first_half = city;
if(town)
first_half = town;
return first_half+", "+second_half;

3.JavaScript另外一種作法

var fisrt_half,second_half;
if(country === "USA"){
first_half = town || city || "middle-of-nowhere";
second_half = state || "USA";
}else {
first_half = town || city || state || "middle-of-nowhere";
second_half = country || "planet earth";
}
return first_half + ", " + second_half;

4.大例子

void UpdateCounts(HttpDownload hd){
counts["exit state"][ExitState(hd)]++;
counts["http response"][HttpResponse(hd)]++;
counts["content-type"][ContentType(hd)]++;
}

string ExitState(HttpDownload hd){
if(hd.has_event_log() && hd.event_log().has_exit_state()){
return ExitStateTypeName(hd.event_log().exit_state());
}
else{
return "unknow";
}
}

 

Part 12 把想法改變成代碼

1.清楚描述邏輯

使用天然語言來敘述一個邏輯

受權你有兩種方式:

你是管理

你擁有當前文檔

不然,沒法受權你。

if(is_admin_request()){
//authorized 
}else if($document && ($document['username']==$_SESSION['username'])){
//authorized 
}else{
return not_authorized();
}

2.瞭解函數庫

找到當前可見的提示並隱藏它

而後找到它的下一個提示並顯示。

若是沒有更多提示,循環回第一個提示。

var show_next_tip = function(){
var cur_tip = $('.tip:visible').hide();//find the currently visible tip and hide it; 
var next_tip = cur_tip.next('.tip'); //find the next tip after it 
if(next_tip.size()===0) // if we're run out of tips 
next_tip = $('.tip:first'); //cycle back to the first tip 
next_tip.show(); //show the new tip 
}

3.更大的問題

並行讀取三個迭代行

只要這些行不匹配,向前找知道它們匹配。

而後輸出匹配的行。

一直作到沒有匹配的行。

def PrintStockTransactions():
while True:
time = AdvanceToMatchingTime(stock_iter,price_iter,num_shares_iter)
if time is None:
return
#print the aligned rows.
print "@",time,
print stock_iter.ticker_symbol,
print prince_iter.price,
print num_shares_iter.number_of_shares
stock_iter.NextRow()
price_iter.NextRow()
num_shares_iter.NextRow()
AdvanceToMatchingTime:

看一下每一個當前行:若是它們匹配,那麼就完成。

不然,向前移動任何」落後「的行。

一直這樣作知道全部行匹配(或者迭代結束)。

def AdvanceToMatchingTime(row_iter1,row_iter2,row_iter3){
t1 = row_iter1.time
t2 = row_iter2.time
t3 = row_iter3.time
if t1==t2==t3
return t1;
tmax = max(t1,t2,t3)
#if any row is "behind," advance it .
#eventually,this while loop will align them all.
if t1 < tmax:row_iter1.NextRow()
if t2 < tmax:row_iter2.NextRow()
if t3 < tmax:row_iter3.NextRow()
}

 

Part 13 少寫代碼

知道何時不寫代碼對於一個程序員來講是他所學習的最重要技巧。所寫的每一行代碼都要測試和維護。經過重用和減小功能,能夠節省時間並保持代碼簡潔。

1.商店定位器

不須要去考慮南極北極,計算地表曲面的狀況。

2.增長緩存

對於特殊狀況,不須要去實現LRU(最近最少使用)緩存,僅僅須要一個條目緩存:

DiskObjet lastUsed;
DiskObject LookUp(string key){
if(lastUsed == null || !lastUsed.key().equals(key)){
lastUsed = loadDiskObject(key);
}
return lastUsed;
}

3.保持小代碼庫

建立越多越好的「工具」代碼來減小重複代碼

減小無用代碼或沒有用的功能

讓項目保持分開的子項目狀態

當心代碼的「重量」,保持代碼輕盈

4.熟悉周邊的庫

每隔一段時間,花點時間閱讀標準庫中的全部函數/模塊/類型的名字。

不是須要記住全部庫函數,而是「靈機一動」發現代碼中存在的相似東西。

 

Part 14 測試與可讀性

1.使測試易於閱讀和維護

咱們有一個文檔列表,它們的分數爲[-5,1,4,-99998.7,3].

在SortAndFilterDocs()以後,剩下的文檔應該有的分數是[4,3,1],並且順序也是這樣

CheckScoresBeforeAfter("-5,1,4,-99998.7,3","4,3,1');

對於這樣的輸入/情形,指望有這樣的行爲/輸出.

void CheckScoresBeforeAfter(string input,string expected_output){
vector<ScoredDocument> docs = ScoredDocsFromString(input);
SortAndFilterDocs(&docs);
string output = ScoredDocsToString(docs);
assert(output == expected_output);
}

vector<ScoredDocument> ScoredDocsFromString(string scores){
vector<ScoredDocument> docs;
replace(scores.begin(),scores.end(),',',' ');
//popluate 'docs' from a string of space-separeated scores. 
istringstream stream(socres);
double score;
while(stream >> score){
AddScoredDoc(docs,score);
}
return docs;
}

string ScoredDocsToString(vector<ScoredDocument> docs){
ostringstream stream;
for(int i=0;i<docs.size();i++){
if(i>0) stream << ", ";
stream<<doc[i].score;
}
return stream.str();
}

2.讓錯誤消息可讀

看語言庫/框架能給你提供何種幫助。

編寫程序來產生測試數據/代碼。

3.爲測試函數命名

被測試的類

唄測試的函數

唄測試的情形或bug

void Test_SortAndFilterDocs(){...}

void Test_SortAndFilterDocs_basicSorting(){...}

void Test_SortAndFilterDocs_NegativeValues(){...}

4.測試驅動開發(TDD)

可測試性差的代碼特徵

使用全局變量:

對於每一個測試都要重置全部的全局狀態。 

很難理解哪些函數有什麼反作用。沒辦法獨立考慮每一個函數,要考慮整個程序才能理解是否是全部代碼都能工做。

對外部組件有大量依賴的代碼:

很難給它寫出任何測試,由於要先搭建太多的腳手架。寫測試比較無趣,沒人願意寫測試。

系統可能會由於某一依賴失敗而失敗。對於改動來說很難知道會產生什麼樣的影響。很難重構類。系統會有更多失敗模式,而且要考慮更多恢復路徑。

代碼有不肯定的行爲:

測試會很古怪,並且不可靠。常常失敗的測試會被忽略。

這種程序可能會有競爭或其餘難以重現的bug。很難推理。產品中的bug很難跟蹤和改正。

可測試性好的代碼特徵

類中只有少或者沒有內部狀態:

容易測試,由於一個方法只要較少的設置。而且較少的隱藏狀態須要檢查。

有較少狀態的類更簡單,更容易理解。

類/函數只作一件事:

要測試它只須要較少的測試用例。

較小/較簡單的組件更加模塊化,而且通常來說系統有更少的耦合。

依賴少;耦合低:

每一個類獨立的測試。

系統能夠並行開發。能夠容易修改或者刪除類,而不會影響系統的其餘部分

函數的接口簡單,定義明確:

有明確的行爲能夠測試。測試簡單接口所需的工做量較少。

接口更容易學習,而且重用可能性較大。

每一個測試的最高一層應該越簡明越好。最好每一個測試的輸入/輸出能夠用一行代碼描述。

若是測試失敗了,它所發出的錯誤消息應該能讓你容易跟蹤並修復這個bug。

使用最簡單的而且可以完整運用代碼的測試輸入。

給測試函數其一個完整描述性的名字,使得每一個測試很明確。

 

Part 15 設計改進「分鐘/小時計數器」

設計跟蹤在過去的一分鐘和一小時裏web服務器傳輸了多少字節。

//Track the cumulative counts over the past minute and over the past hour. 
//useful,for exmaple,to track recent bandwidth usage. 
class MinuteHourCounter{
//Add a new data point(count>=0) 
//For the next minute,MinuteCount() will be larger by +count. 
//For the next hour,HourCount() will be larger by +count. 
void Add(int count);
//return the accumulated count over the past 60 seconds. 
int MinuteCount();
//Return the accumulated count over the past 3600 seconds. 
int HourCount();
};

class MinuteHourCounter{
list<Event> minute_events;
list<Event> hour_events; //only contians elements not in minute_events 
int minute_count;
int hour_count; //counts all events over past hour,including past minute 
void Add(int count){
const time_t now_secs = time();
ShiftOldEvents(now_secs);
//feed into the minute list(not into the hour list--that will happedn laster) 
minute_events.push_back(Event(count,now_secs));
miniute_count+=count;
hour_count+=count;
}

int MinuteCount{
ShiftOldEvents(time());
return minute_count;
};


int HourCount(){
ShiftOldEvents(time());
return hour_count;
}


//Find and delete old eents,and decrease hour_count and minute_count accordingly. 
void ShiftOldEvents(time_t now_secs){
const int minute_ago = now_secs-60;
const int minute_ago = noew_secs-3600;
//move events more than one minute old from 'minute_events' into 'hour_events' 
//(events older than one hour will be removed in the second loop.) 
while(!minute_events.empty() && minute_events.front().time<=minute_ago){
hour_events.push_back(minute_events.front());
minute_count -= minute_events.front().count;
minute_events.pop_front();
}


//remove events more than one hour old from 'hour_events' 
while(!hour_events.empty() && hour_events.front().time<=hour_ago){
hour_count -= hour_events.front().count;
hour_events.pop_front();
}
}
}

使用時間桶優化:

//a class that keeps counts for the past N buckets of time. 
class TrailingBucketCounter{
public:
//example:trailingbucketcounter(30,60)tracks the last 30 minute-buckets of time. 
TrailingBucketCounter(int num_buckets,int secs_per_bucket);
void Add(int count,time_t now);
//return the total count over the last num_buckets worth of time 
int TrailingCount(time_t now);
}

class MinuteHourCounter{
TrailingBucketCounter minute_counts;
TrailingBucketCounter hour_counts;
public:
MinuteHourCounter():
minute_count(/* num_buckets = */60,/* secs_per_bucket=*/1),
hour_counts(/* num_buckets = */60,/* secs_per_bucket=*/60){
}

void Add(int count){
time_t now = time();
minute_counts.Add(count,now);
hour_counts.Add(count,now);
}


int MinuteCount(){
time_t now = time();
return minute_counts.TrailingCount(now);
}

int HourCount(){
time_t now = time();
return hour_counts.TrailingCount(now);
}
}

class TrailingBucketCounter{
ConveyorQueue buckets;
const int secs_per_bucket;
time_t last_update_time; //the last time update() was called 
//calculate how many buckets of time have passed and shift() accordingly. 
void Update(time_t now){
int current_bucket = now / secs_per_bucket;
int last_update_bucket = last_update_time / secs_per_bucket;
buckets.Shift(current_bucket - last_update_bucket);
last_update_time = now;
}

public:
TrailingBucketCounter(int num_buckets,int secs_per_bucket):
buckets(num_buckets);
secs_per_bucket(secs_per_bucket){}

void Add(int count,time_t now){

Update(now);

buckets.AddToBack(count);
}

int TrailingCount(time_t now){
Update(now);
return bucktesTotalSum();
}
};

//A queue with a maximum number of slots,where old data gets shifted off the end. 
class ConveryorQueue{
queue<int> q;
int max_items;
int total_sum ; //sum of all items in q 
public:
ConveyorQueue(int max_itmes):max_items(max_items),total_sum(0){}
int TotalSum(){
return total_sum;
}


void Shift(int num_shifted){
//in case too many items shifted,just clear the queue. 
if(num_shifted>=max_items){
q = queue<int>(); //clear the queue 
total_sum = 0;
return;
}

//push all the needed zeros. 
while(num_shifted>0){
q.push(0);
num_shifted--;
}

//let all the excess items fall off. 
while(q.size()>max_items){
total_sum -= q.front();
q.pop();
}
}

void AddToBack(int count){
if(q.empty()) Shift(1); //make sure q has at least 1 item. 
q.back() += count;
total_sum += count;
}
}

 

推薦書籍

《代碼質量》

《代碼整潔之道》

《代碼之美》

《代碼之殤》

《重構-改善既有代碼的設計》

《程序員修煉之道 從小工到專家》

 

 

後面的嘮嗑:

    寫個博客真是不容易,我在wiz下寫的筆記,而後挪到windows live writer下,整個代碼全亂了,用wiz去發佈到博客內容又更亂。心累,之後慢慢改吧,你們多包含。

相關文章
相關標籤/搜索