前面的话:
我发现大家都喜欢在正文前唠嗑几句,可能是做技术的平常本身就比较沉闷,跟周边的人也不能无所顾忌的交流,毕竟国人都比较讲究含蓄。在网上就不同了,可以畅所欲言,所以每次除了分享知识,还顺带交待下自己,让技术也就不那么”冰冷“了。
唠嗑的话:
前几天看自己的为知,发现每年都有个读书计划列表,不过大多都没有完成,今年甚至连计划都还没有。内心觉得不安,正好从深圳回来的时候,公司把之前购买的书也一并带回来了,从书架上挑了一本《编写可读代码的艺术》来看,作为今年的读书开篇,顺便也把读书计划继续完成。
正文
这本书很薄,花了两天时间就看完了,内容通俗易懂;
它是根据作者在实际中碰到的问题,由浅入深,从几个方面来叙述如何编写可读的代码。
书中指正的几个地方,在我自己做过的项目中是真实存在,有顿悟的感觉。
实现同样的功能,有的代码读起来味同嚼蜡,有的代码读起来妙趣横生,回味无穷。
我想每个coder所追求的无非都是那种书写代码时行云流水般的感觉,别人看到时为之一叹的满足感。
本书对想提升自己代码可读性有一定帮助,想读的朋友可以搜一下。
第一部分 表面层次的改进
每一个变量名,每一段注释,每一组分块多会对你的可读性产生影响。如果不想忍受重构时的苦难,那就从基本做起。
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去发布到博客内容又更乱。心累,以后慢慢改吧,大家多包含。