如何提升代码的可读性? - 读《编写可读代码的艺术》

《编写可读代码的艺术》封面

一. 为何读这本书

不少同行在编写代码的时候每每只关注一些宏观上的主题:架构,设计模式,数据结构等等,却忽视了一些更细节上的点:好比变量如何命名与使用,控制流的设计,以及注释的写法等等。以上这些细节上的东西能够用代码的可读性来归纳。javascript

不一样于宏观上的架构,设计模式等须要好几个类,好几个模块才能看出来:代码的可读性是可以马上从微观上的,一个变量的命名,函数的逻辑划分,注释的信息质量里面看出来的。html

宏观层面上的东西当然重要,可是代码的可读性也属于评价代码质量的一个没法让人忽视的指标:它影响了阅读代码的成本(毕竟代码主要是给人看的),甚至会影响代码出错的几率!java

这里引用《编写可读代码的艺术》这本书里的一句话:git

对于一个总体的软件系统而言,既须要宏观的架构决策,设计与指导原则,也必须重视微观上的的代码细节。在软件历史中,有许多影响深远的重大失败,其根源每每是编码细节出现了疏漏。程序员

所以笔者认为代码的可读性能够做为考量一名程序员专业程度的指标。github

或许已经有不少同行也正在努力提升本身代码的可读性。然而这里有一个很典型的错觉(笔者以前就有这种错觉)是:越少的代码越容易让人理解。objective-c

可是事实上,并非代码越精简就越容易让人理解。相对于追求最小化代码行数,一个更好的提升可读性方法是最小化人们理解代码所须要的时间。算法

这就引出了这本中的一个核心定理:编程

可读性基本定理:代码的写法应当使别人理解它所须要的时间最小化。设计模式

这本书讲的就是关于“如何提升代码的可读性”。 笔者总结下来,这本书从浅入深,在三个层次告诉了咱们如何让代码易于理解:

  • 表层上的改进:在命名方法(变量名,方法名),变量声明,代码格式,注释等方面的改进。
  • 控制流和逻辑的改进:在控制流,逻辑表达式上让代码变得更容易理解。
  • 结构上的改进:善于抽取逻辑,借助天然语言的描述来改善代码。

二. 表层的改进

首先来说最简单的一层如何改进,涉及到如下几点:

  • 如何命名
  • 如何声明与使用变量
  • 如何简化表达式
  • 如何让代码具备美感
  • 如何写注释

如何命名

关于如何命名,做者提出了一个关键思想:

关键思想:把尽量多的信息装入名字中。

这里的多指的是有价值的多。那么如何作到有价值呢?做者介绍了如下几个建议:

  • 选择专业的词汇,避免泛泛的名字
  • 给名字附带更多信息
  • 决定名字最适合的长度
  • 名字不能引发歧义

选择专业的词汇,避免泛泛的名字

一个比较常见的反例:get

get这个词最好是用来作轻量级的取方法的开头,而若是用到其余的地方就会显得很不专业。

举个书中的例子:

getPage(url)

经过这个方法名很难判断出这个方法是从缓存中获取页面数据仍是从网页中获取。若是是从网页中获取,更专业的词应该是fetchPage(url)或者downloadPage(url)

还有一个比较常见的反例:returnValueretval。这二者都是“返回值”的意思,他们被滥用在各个有返回值的函数里面。其实这两个词除了携带他们原本的意思返回值之外并不具有任何其余的信息,是典型的泛泛的名字。

那么如何选择一个专业的词汇呢?答案是在很是贴近你本身的意图的基础上,选择一个富有表现力的词汇。

举几个例子:

  • 相对于make,选择create,generate,build等词汇会更有表现力,更加专业。
  • 相对于find,选择search,extract,recover等词汇会更有表现力,更加专业。
  • 相对于retval,选择一个能充分描述这个返回值的性质的名字,例如:
var euclidean_norm = function (v){
    var retval = 0.0;
    for (var i = 0; i < v.length; i += 1;)
       retval += v[i] * v[i];
    return Match.sqrt(retval);
}
复制代码

这里的retval表示的是“平方的和”,所以sum_squares这个词更加贴切你的意图,更加专业。

可是,有些状况下,泛泛的名字也是有意义的,例如一个交换变量的情景:

if (right < left){
    tmp = right;
    right = left;
    left = tmp;
}
复制代码

像上面这种tmp只是做为一个临时存储的状况下,tmp表达的意思就比较贴切了。所以,像tmp这个名字,只适用于短时间存在并且特性为临时性的变量。

给名字附带更多信息

除了选择一个专业,贴切意图的词汇,咱们也能够经过添加一些先后缀来给这个词附带更多的信息。这里所指的更多的信息有三种:

  • 变量的单位
  • 变量的属性
  • 变量的格式

为变量添加单位

有些变量是有单位的,在变量名的后面添加其单位可让这个变量名携带更多信息:

  • 一个表达时间间隔的变量,它的单位是秒:相对于duractionducation_secs携带了更多的信息
  • 一个表达内存大小的变量,它的单位是mb:相对于sizecache_mb携带了更多的信息。

为变量添加剧要属性

有些变量是具备一些很是重要的属性,其重要程度是不容许使用者忽略的。例如:

  • 一个UTF-8格式的html字节,相对于htmlhtml_utf8更加清楚地描述了这个变量的格式。
  • 一个纯文本,须要加密的密码字符串:相对于passwordplaintext_password更清楚地描述了这个变量的特色。

为变量选择适当的格式

对于命名,有些既定的格式须要注意:

  • 使用大驼峰命名来表示类名:HomeViewController
  • 使用小驼峰命名来表示属性名:userNameLabel
  • 使用下划线链接词来表示变量名:product_id
  • 使用kConstantName来表示常量:kCacheDuraction
  • 使用MACRO_NAME来表示宏:SCREEN_WIDTH

决定名字最适合的长度

名字越长越难记住,名字越短所持有的信息就越少,如何决定名字的长度呢?这里有几个原则:

  • 若是变量的做用域很小,能够取很短的名字
  • 驼峰命名中的单元不能超过3个
  • 不能使用你们不熟悉的缩写
  • 丢掉没必要要的单元

若是变量的做用域很小,能够取很短的名字

若是一个变量做用域很小:则给它取一个很短的名字也无妨。

看下面这个例子:

if(debug){
    map <string,int>m;
    LookUpNamesNumbers(&m);
    Print(m);
}
复制代码

在这里,变量的类型和使用范围一眼可见,读者能够了解这段代码的全部信息,因此即便是取m这个很是简短的名字,也不影响读者理解做者的意图。

相反的,若是m是一个全局变量,当你看到下面这段代码就会很头疼,由于你不明确它的类型:

LookUpNamesNumbers(&m);
Print(m);
复制代码

驼峰命名中的单元不能超过3个

咱们知道驼峰命名能够很清晰地体现变量的含义,可是当驼峰命名中的单元超过了3个以后,就会很影响阅读体验:

userFriendsInfoModel

memoryCacheCalculateTool

是否是看上去很吃力?由于咱们大脑同时能够记住的信息很是有限,尤为是在看代码的时候,这种短时间记忆的局限性是没法让咱们同时记住或者瞬间理解几个具备3~4个单元的变量名的。因此咱们须要在变量名里面去除一些没必要要的单元:

丢掉没必要要的单元

有些单元在变量里面是能够去掉的,例如:

convertToString能够省略成toString

不能使用你们不熟悉的缩写

有些缩写是你们熟知的:

  • doc 能够代替document
  • str 能够代替string

可是若是你想用BEManager来代替BackEndManager就比较不合适了。由于不了解的人几乎是没法猜到这个名称的真正意义的。

因此遇到相似这种状况咱们不能偷懒,该是什么就是什么,不然会起到相反的效果。由于它看起来很是陌生,跟咱们熟知的一些缩写规则相去甚远。

名字不能引发歧义

有些名字会引发歧义,例如:

  • filter:过滤这个词,能够是过滤出符合标准的,也能够是减小不符合标准的:是两种彻底相反的结果,因此不推荐使用。
  • clip:相似的,究竟是在原来的基础上截掉某一段仍是另外截出来某一段呢?一样也不推荐使用。
  • 布尔值:read_password:是表达须要读取密码,仍是已经读了密码呢?因此最好使用need_password或者is_authenticated来代替比较好。一般来讲,给布尔值的变量加上is,has,can,should这样的词可使布尔值表达的意思更加明确

这一节讲了不少关于如何起好一个变量名的方法。其实有一个很简单的原则来判断这个变量名起的是不是好的:那就是:团队的新成员是否能迅速理解这个变量名的含义。若是是,那么这个命名就是成功的;不然就不要偷懒了,起个好名字,对谁都好。其实若是你养成习惯多花几秒钟想出个好名字,渐渐地,你会发现你的“命名能力”会很快提高。

如何声明与使用变量

在写程序的过程当中咱们会声明不少变量(成员变量,临时变量),而咱们要知道变量的声明与使用策略是会对代码的可读性形成影响的:

  • 变量越多,越难跟踪它们的动向。
  • 变量的做用域越大,就须要跟踪它们的动向越久。
  • 变量改变的越频繁,就越难跟踪它的当前值。

相对的,对于变量的声明与使用,咱们能够从这四个角度来提升代码的可读性:

  1. 减小变量的个数
  2. 缩小变量的做用域
  3. 缩短变量声明与使用其代码的距离
  4. 变量最好只写一次

减小变量的个数

在一个函数里面可能会声明不少变量,可是有些变量的声明是毫无心义的,好比:

  • 没有价值的临时变量
  • 表示中间结果的变量

没有价值的临时变量

有些变量的声明彻底是画蛇添足,它们的存在反而加大了阅读代码的成本:

let now = datetime.datatime.now()
root_message.last_view_time = now	
复制代码

上面这个now变量的存在是毫无心义的,由于:

  • 没有拆分任何复杂的表达式
  • datetime.datatime.now已经很清楚地表达了意思
  • 只使用了一次,所以而没有压缩任何冗余的代码

因此彻底不用这个变量也是彻底能够的:

root_message.last_view_time = datetime.datatime.now()
复制代码

表示中间结果的变量

有的时候为了达成一个目标,把一件事情分红了两件事情来作,这两件事情中间须要一个变量来传递结果。但每每这件事情不须要分红两件事情来作,这个“中间结果”也就不须要了:

看一个比较常见的需求,一个把数组中的某个值移除的例子:

var remove_value = 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);
    }
} 
复制代码

这里面把这个事情分红了两件事情来作:

  1. 找出要删除的元素的序号,保存在变量index_to_remove里面。
  2. 拿到index_to_remove之后使用splice方法删除它。(这段代码是JavaScript代码)

这个例子对于变量的命名仍是比较合格的,但实际上这里所使用的中间结果变量是彻底不须要的,整个过程也不须要分两个步骤进行。来看一下如何一步实现这个需求:

var remove_value = 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;
        }
    }
} 
复制代码

上面的方法里面,当知道应该删除的元素的序号i的时候,就直接用它来删除了应该删除的元素并当即返回。

除了减轻了内存和处理器的负担(由于不须要开辟新的内容来存储结果变量以及可能不用彻底走遍整个的for语句),阅读代码的人也会很快领会代码的意图。

因此在写代码的时候,若是能够“速战速决”,就尽可能使用最快,最简洁的方式来实现目的。

缩小变量的做用域

变量的做用域越广,就越难追踪它,值也越难控制,因此咱们应该让你的变量对尽可能少的代码可见

好比类的成员变量就至关于一个“小型局部变量”。若是这个类比较庞大,咱们就会很难追踪它,由于全部方法均可以“隐式”调用它。因此相反地,若是咱们能够把它“降格”为局部变量,就会很容易追踪它的行踪:

//成员变量,比较难追踪
class LargeCass{
  string str_;
  
  void Method1(){
     str_ = ...;
     Method2();
  }
  
  void Method2(){
     //using str_
  }
}
复制代码

降格:

//局部变量,容易追踪
class LargeCass{
  
  void Method1(){
     string str = ...;
     Method2(str);
  }
  
  void Method2(string str){
     //using str
  }
}
复制代码

因此在设计类的时候若是这个数据(变量)能够经过方法参数来传递,就不要以成员变量来保存它。

缩短变量声明与使用其代码的距离

在实现一个函数的时候,咱们可能会声明比较多的变量,但这些变量的使用位置却不都是在函数开头。

有一个比较很差的习惯就是不管变量在当前函数的哪一个位置使用,都在一开始(函数的开头)就声明了它们。这样可能致使的问题是:阅读代码的人读到函数后半部分的时候就忘记了这个变量的类型和初始值;并且由于在函数的开头就声明了好几个变量,也对阅读代码的人的大脑形成了负担,由于人的短时间记忆是有限的,特别是记一些暂时还不知道怎么用的东西。

所以,若是在函数内部须要在不一样地方使用几个不一样的变量,建议在真正使用它们以前再声明它。

变量最好只写一次

操做一个变量的地方越多,就越难肯定它的当前值。因此在不少语言里面有其各自的方式让一些变量不可变(是个常量),好比C++里的const和Java中的final

如何简化表达式

有些表达式比较长,很难让人立刻理解。这时候最好能够将其拆分红更容易的几个小块。能够尝试下面的几个方法:

  • 使用解释变量
  • 使用总结变量
  • 使用德摩根定理

使用解释变量

有些变量会从一个比较长的算式得出,这个表达式可能很难让人看懂。这时候就须要用一个简短的“解释”变量来诠释算式的含义。使用书中的一个例子:

if line.split(':')[0].strip() == "root"
复制代码

其实上面左侧的表达式其实得出的是用户名,咱们能够用username来替换它:

username = line.split(':')[0].strip()
if username == "root"
复制代码

使用总结变量

除了以“变量”替换“算式”,还能够用“变量”来替换含有更多变量更复杂的内容,好比条件语句,这时候该变量能够被称为"总结变量"。使用书中的一个例子:

if(request.user.id == document.owner_id){
   //do something 
}
复制代码

上面这条判断语句所判断的是:“该文档的全部者是否是该用户”。咱们可使用一个总结性的变量user_owns_document来替换它:

final boolean user_owns_document = (request.user.id == document.owner_id);
if (user_owns_document){
   //do something
}
复制代码

使用德摩根定理

德摩根定理:

  1. not(a or b or c)等价于(not a) and (not b) and (not c)
  2. not(a and b and c)等价于(not a) or (not b) or (not c)

当咱们条件语句里面存在外部取反的状况,就可使用德摩根定理来作个转换。使用书中的一个例子:

//使用德摩根定理转换之前
if(!(file_exists && !is_protected)){}

//使用德摩根定理转换之后
if(!file_exists || is_protected){}
复制代码

如何让代码具备美感

在读过一些好的源码以后我有一个感觉:好的源码每每都看上去都很漂亮,颇有美感。这里说的漂亮和美感不是指代码的逻辑清晰有条理,而是指感官上的视觉感觉让人感受很舒服。这是从一种纯粹的审美的角度来评价代码的:富有美感的代码让人赏心悦目,也容易让人读懂。

为了让代码更有美感,采起如下实践会颇有帮助:

  • 用换行和列对齐来让代码更加整齐
  • 选择一个有意义的顺序
  • 把代码分红"段落"
  • 保持风格一致性

用换行和列对齐来让代码更加整齐

有些时候,咱们能够利用换行和列对齐来让代码显得更加整齐。

换行

换行比较经常使用在函数或方法的参数比较多的时候。

使用换行:

- (void)requestWithUrl:(NSString*)url 
  				method:(NSString*)method 
                params:(NSDictionary *)params 
               success:(SuccessBlock)success 
               failure:(FailuireBlock)failure{
    
}
复制代码

不使用换行:

- (void)requestWithUrl:(NSString*)url method:(NSString*)method params:(NSDictionary *)params success:(SuccessBlock)success failure:(FailuireBlock)failure{
    
}
复制代码

经过比较能够看出,若是不使用换行,就很难一眼看清楚都是用了什么参数,并且代码总体看上去整洁干净了不少。

列对齐

在声明一组变量的时候,因为每一个变量名的长度不一样,致使了在变量名左侧对齐的状况下,等号以及右侧的内容没有对齐:

NSString *name = userInfo[@"name"];
NSString *sex = userInfo[@"sex"];
NSString *address = userInfo[@"address"];
复制代码

而若是使用了列对齐的方法,让等号以及右侧的部分对齐的方式会使代码看上去更加整洁:

NSString *name    = userInfo[@"name"];
NSString *sex     = userInfo[@"sex"];
NSString *address = userInfo[@"address"];
复制代码

这两者的区别在条目数比较多以及变量名称长度相差较大的时候会更加明显。

选择一个有意义的顺序

当涉及到相同变量(属性)组合的存取都存在的时候,最好以一个有意义的顺序来排列它们:

  • 让变量的顺序与对应的HTML表单中字段的顺序相匹配
  • 从最重要到最不重要排序
  • 按照字母排序

举个例子:相同集合里的元素同时出现的时候最好保证每一个元素出现顺序是一致的。除了便于阅读这个好处之外,也有助于能发现漏掉的部分,尤为当元素不少的时候:

//给model赋值
model.name	  = dict["name"];
model.sex 	  = dict["sex"];
model.address = dict["address"];

 ...
  
//拿到model来绘制UI
nameLabel.text    = model.name;
sexLabel.text     = model.sex;
addressLabel.text = model.address;
复制代码

把代码分红"段落"

在写文章的时候,为了能让整个文章看起来结构清晰,咱们一般会把大段文字分红一个个小的段落,让表达相同主旨的语言凑到一块儿,与其余主旨的内容分隔开来。

并且除了让读者明确哪些内容是表达同一主旨以外,把文章分为一个个段落的好处还有便于找到你的阅读“脚印”,便于段落之间的导航;也可让你的阅读具备必定的节奏感。

其实这些道理一样适用于写代码:若是你能够把一个拥有好几个步骤的大段函数,以空行+注释的方法将每个步骤区分开来,那么则会对读者理解该函数的功能有极大的帮助。这样一来,代码既能有必定的美感,也具有了可读性。其实可读性又未尝不是来自于规则,富有美感的代码呢?

BigFunction{
  
     //step1:*****
     ....
       
     //step2:*****
     ...
        
     //step3:*****
     ....
  
}
复制代码

保持风格一致性

有些时候,你的某些代码风格可能与大众比较容易接受的风格不太同样。可是若是你在你本身所写的代码各处可以保持你这种独有的风格,也是能够对代码的可读性有积极的帮助的。

好比一个比较经典的代码风格问题:

if(condition){

}
复制代码

or:

if(condition)
{

}
复制代码

对于上面的两种写法,每一个人对条件判断右侧的大括号的位置会有不一样的见解。可是不管你坚持的是哪个,请在你的代码里作到始终如一。由于若是有某几个特例的话,是很是影响代码的阅读体验的。

咱们要知道,一个逻辑清晰的代码也能够由于留白的不规则,格式不对齐,顺序混乱而让人很难读懂,这是十分让人痛心的事情。因此既然你的代码在命名上,逻辑上已经很优秀了,就不妨再费一点功夫把她打扮的漂漂亮亮的吧!

如何写注释

首先引用书中的一句话:

注释的目的是尽可能帮助读者了解得和做者同样多。

在你写代码的时候,在脑海中可能会留下一些代码里面很难体现出来的部分:这些部分在别人读你的代码的时候可能很难体会到。而这些“不对称”的信息就是须要经过以注释的方式来告诉阅读代码的人。

想要写出好的注释,就须要首先知道:

  • 什么不能做为注释
  • 什么应该做为注释

什么不能做为注释

咱们都知道注释占用了代码的空间,并且实际上对程序自己的运行毫无帮助,因此最好保证它是物有所值的

不幸的是,有一些注释是毫无价值的,它无情的占用了代码间的空间,影响了阅读代码的人的阅读效率,也浪费了写注释的人的时间。这样的注释有如下两种:

  • 描述能马上从代码自身就能马上理解的代码意图的注释
  • 给很差的命名添加的注释

描述能马上从代码自身就能马上理解的代码意图的注释

//add params1 and params2 and return sum of them
- (int)addParam1:(int)param1 param2:(int)param2
复制代码

上面这个例子举的比较简单,但反映的问题很明显:这里面的注释是彻底不须要的,它的存在反而增长了阅读代码的人的工做量。由于他从方法名就能够立刻意会到这个函数的做用了。

给很差的命名添加的注释

//get information from internet
- (NSString *)getInformation
复制代码

该函数返回的是从网络获取的信息。但这里使用了get前缀,没法看出信息的来源。为了补充信息,使用注释来弥补。但其实这彻底没必要要。只要取一个适当的名字就行了:

- (NSString *)fetchInformation
复制代码

讲完了注释不该该是什么内容,如今讲一下注释应该是什么样的内容:

什么应该做为注释

本书中介绍的注释大概有如下几种:

  • 写代码时的思考

  • 对代码的评价

  • 常量

  • 全局观的概述

写代码时的思考

你的代码可能不是一蹴而就的,它的产生可能会须要一些思考的过程。然而不少时候代码自己却没法将这些思考表达出来,因此你就可能有必要经过注释的方式来呈现你的思考,让阅读代码的人知道这段代码是哪些思考的结晶,从而也让读者理解了这段代码为何这么写。若是遇到了比你高明的高手,在他看到你的注释以后兴许会立刻设计出一套更加合适的方案。

对代码的评价

有些时候你知道你如今写的代码是个临时的方案:它可能确实是解决当前问题的一个方法,可是:

  • 你知道同时它也存在着某些缺陷,甚至是陷阱

  • 你不知道有其余的方案能够替代了

  • 你知道有哪一个方案能够替代可是因为时间的关系或者自身的能力没法实现

也可能你知道你如今实现的这个方案几乎就是“完美的”,由于若是使用了其余的方案,可能会消耗更多的资源等等。

对于上面这些状况,你都有必要写上几个字做为注释来诚实的告诉阅读你的这段代码的人这段代码的状况,好比:

//该方案有一个很容易忽略的陷阱:****
//该方案是存在性能瓶颈,性能瓶颈在其中的**函数中
//该方案的性能可能并非最好的,由于若是使用某某算法的话可能会好不少
复制代码

常量

在定义常量的时候,在其后面最好添加一个关于它是什么或者为何它是这个值的缘由。由于常量一般是不该该被修改的,因此最好把这个常量为何是这个值说明一下:

例如:

image_quality = 0.72 // 最佳的size/quanlity比率
retry_limit   = 4    // 服务器性能所容许的请求失败的重试上限
复制代码

全局观的概述

对于一个刚加入团队的新人来讲,除了团队文化,代码规范之外,可能最须要了解的是当前被分配到的项目的一些“全局观”的认识:好比组织架构,类与类之间如何交互,数据如何保存,如何流动,以及模块的入口点等等。

有时仅仅添加了几句话,可能就会让新人迅速地了解当前系统或者当前类的结构以及做用,并且这些也一样对开发过当前系统的人员迅速回忆出以前开发的细节有很大帮助。

这些注释能够在一个类的开头(介绍这个类的职责,以及在整个系统中的角色)也能够在一个模块入口处。书中举了一个关于这种注释的例子:

//这个文件包含了一些辅助函数,尾门的文件系统提供了更便利的接口
复制代码

再举一个iOS开发里众所周知的网络框架AFNetworking的例子。在AFHTTPSessionManager的头文件里说明了这个类的职责:

//AFHTTPSessionManager` is a subclass of `AFURLSessionManager` with convenience methods for making HTTP requests. When a `baseURL` is provided, requests made with the `GET` / `POST` / et al. convenience methods can be made with relative paths
复制代码

在知道了什么不该该是注释以及什么应该是注释之后,咱们来看一下一个真正合格的注释应该是什么样子的:

注释应当有很高的信息/空间率

也就是说,注释应该用最简短的话来最明确地表达意图。要作到这一点须要作的努力是:

  • 让注释保持紧凑:尽可能用最简洁的话来表达,不该该有重复的内容
  • 准确地描述函数的行为:要把函数的具体行为准确表达出来,不能停留在代表
  • 用输入/输出的例子来讲明特别的状况:有时相对于文字,可能用一个实际的参数和返回值就能马上体现出函数的做用。并且有些特殊状况也能够经过这个方式来提醒阅读代码的人
  • 声明代码的意图:也就是说明这段代码存在的意义,你为何当时是这么写的缘由

其实好的代码是自解释的,因为其命名的合理以及架构的清晰,几乎不须要注释来向阅读代码的人添加额外的信息,书中有一个公式能够很形象地代表一个好的代码自己的重要性:

好代码 > (坏代码 + 注释)

三. 控制流和逻辑的改进

控制流在编码中占据着很重要的位置,它每每表明着一些核心逻辑和算法。所以,若是咱们可让控制流变得看上去更加“天然”,那么就会对阅读代码的人理解这些逻辑甚至是整个系统提供很大的帮助。

那么都有哪相关实践呢?

  • 使用符合人类天然语言的表达习惯
  • if/else语句块的顺序
  • 使用return提早返回

使用符合人类天然语言的表达习惯

写代码也是一个表达的过程,虽然表现形式不一样,可是若是咱们可以采用符合人类天然语言习惯的表达习惯来写代码,对阅读代码的人理解咱们的代码是颇有帮助的。

这里有两个比较典型的情景:

  1. 条件语句中参数的顺序
  2. 条件语句中的正负逻辑

条件语句中参数的顺序:

首先比较一下下面两段代码,哪个更容易读懂?

//code 1
if(length > 10)

//code 2
if(10 < length)
复制代码

你们习惯上应该会以为code1容易读懂。

再来看下面一个例子:

//code 3
if(received_number < standard_number) 

//code 4
if( standard_number< received_number)
复制代码

仔细看会发现,和上面那一组状况相似,大多数人仍是会以为code3更容易读懂。

那么code1 和 code3有什么共性呢?

它们的共性就是:左侧都是被询问的内容(一般是一个变量);右侧都是用来作比较的内容(一般是一个常量)

这应该是符合天然语言的一个顺序。好比咱们通常会说“今天的气温大于20摄氏度”,而不习惯说“20摄氏度小于今天的气温”。

条件语句中的正负逻辑:

在判断一些正负逻辑的时候,建议使用if(result)而不是if(!result)

由于大脑比较容易处理正逻辑,好比咱们可能比较习惯说“某某某是个男人”,而不习惯说“某某某不是个女人”。若是咱们使用了负逻辑,大脑还要对它进行取反,至关于多作了一次处理。

if/else语句块的顺序

在写if/else语句的时候,可能会有不少不一样的互斥状况(好多个elseif)。那么这些互斥的状况能够遵循哪些顺序呢?

  • 先处理掉简单的状况,后处理复杂的状况:这样有助于阅读代码的人按部就班地地理解你的逻辑,而不是一开始就吃掉一个胖子,耗费很多精力。
  • 先处理特殊或者可疑的状况,后处理正常的状况:这样有助于阅读代码的人会立刻看到当前逻辑的边界条件以及须要注意的地方。

使用return提早返回

在一个函数或是方法里,可能有一些状况是比较特殊或者极端的,对结果的产生影响很大(甚至是终止继续进行)。若是存在这些状况,咱们应该把他们写在前面,用return来提早返回(或者返回须要返回的返回值)。

这样作的好处是能够减小if/else语句的嵌套,也能够明确体现出:“哪些状况是引发异常的”。

再举一个JSONModel里的例子,在initWithDictionary:error方法里面就有不少return操做,它们都体现出了“在什么状况下是不能成功将字典转化为model对象”的;并且在方法的最后返回了对象,说明若是到了这一步,则在转化的过程当中经过了层层考验:

-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err
{
    //check for nil input
    if (!dict) {
        if (err) *err = [JSONModelError errorInputIsNil];
        return nil;
    }

    //invalid input, just create empty instance
    if (![dict isKindOfClass:[NSDictionary class]]) {
        if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
        return nil;
    }

    //create a class instance
    self = [self init];
    if (!self) {

        //super init didn't succeed
        if (err) *err = [JSONModelError errorModelIsInvalid];
        return nil;
    }

    //check incoming data structure
    if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
        return nil;
    }

    //import the data from a dictionary
    if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
        return nil;
    }

    //run any custom model validation
    if (![self validate:err]) {
        return nil;
    }

    //model is valid! yay!
    return self;
}
复制代码

四. 代码组织的改进

关于代码组织的改进,做者介绍了如下三种方法:

  • 抽取出与程序主要目的“不相关的子逻辑”
  • 从新组织代码使它一次只作一件事情
  • 借助天然语言描述来将想法变成代码

抽取出与程序主要目的“不相关的子逻辑”

一个函数里面每每包含了其主逻辑与子逻辑,咱们应该积极地发现并抽取出与主逻辑不相关的子逻辑。具体思考的步骤是:

  1. 首先确认这段代码的高层次目标是什么(主要目标)?
  2. 对于每一行代码,都要反思一下:“它是直接为了目标而工做么?”
  3. 若是答案是确定的而且这些代码占据着必定数量的行数,咱们就应该将他们抽取到独立的函数中。

好比某个函数的目标是为了寻找距离某个商家最近的地铁口,那么这其中必定会重复出现一些计算两组经纬度之间距离的子逻辑。可是这些子逻辑的具体实现是不该该出如今这个主函数里面的,由于这些细节与这个主函数的目标来说应该是无关的。

便是说,像这种相似于工具方法的函数实际上是脱离于某个具体的需求的:它能够用在其余的主函数中,也能够放在其余的项目里面。好比找到离运动场场最近的几个公交站这个需求等等。

而像这种“抽取子逻辑或工具方法”的作法有什么好处呢?

  • 提升了代码的可读性:将函数的调用与原来复杂的实现进行替换,让阅读代码的人很快能了解到该子逻辑的目的,让他们把注意力放在更高层的主逻辑上,而不会被子逻辑的实现(每每是复杂无味的)所影响。
  • 便于修改和调试:由于一个项目中可能会屡次调用该子逻辑(计算距离,计算汇率,保留小数点),当业务需求发生改变的时候只须要改变这一处就能够了,并且调试起来也很是容易。
  • 便于测试:同理,也是由于能够被屡次调用,在进行测试的时候就比较有针对性。

从函数扩大到项目,其实在一个项目里面,有不少东西不是当前这个项目所专有的,它们是能够用在其余项目中的一些“通用代码”。这些通用代码能够对当前的项目一无所知,能够被用在其余任何项目中去。

咱们能够养成这个习惯,“把通常代码与项目专有代码分开”,并不断扩大咱们的通用代码库来解决更多的通常性问题。

从新组织代码使它一次只作一件事情

一个比较大的函数或者功能可能由不少任务代码组合而来,在这个时候咱们有必要将他们分为更小的函数来调用它们。

这样作的好处是:咱们能够清晰地看到这个功能是如何一步一步完成的,并且拆分出来的小的函数或许也能够用在其余的地方。

因此若是你遇到了比较难读懂的代码,能够尝试将它所作的全部任务列出来。可能立刻你就会发现这其中有些任务能够转化成单独的函数或者类。而其余的部分能够简单的成为函数中的一个逻辑段落。

借助天然语言描述来将想法变成代码

在设计一个解决方案以前,若是你可以用天然语言把问题说清楚会对整个设计很是有帮助。由于若是直接从大脑中的想法转化为代码,可能会露掉一些东西。

可是若是你能够将整个问题和想法滴水不漏地说出来,就可能会发现一些以前没有想到的问题。这样能够不断完善你的思路和设计。

五. 最后想说的

这本书从变量的命名到代码的组织来说解了一些让代码的可读性提升的一些实践方法。

其实笔者认为代码的可读性也能够算做是一种沟通能力的一种体现。由于写代码的过程也能够被看作是写代码的人与阅读代码的人的一种沟通,只不过这个沟通是单向的:代码的可读性高,能够说明写代码的人思路清晰,并且TA能够明确,高效地把本身的思考和工做内容以代码的形式表述出来。 因此笔者相信能写出可读性很高的代码的人,TA对于本身的思考和想法的描述能力必定不会不好。

若是你真的打算好好作编程这件事情,建议你从最小的事情上作起:好好为你的变量起个名字。不要再以“我英语很差”或者“没时间想名字”做为托辞;把态度端正起来,平时多动脑,多查字典,多看源码,天然就会了。

若是你连起个好的变量名都懒得查个字典,那你怎么证实你在遇到更难的问题的时候可以以科学的态度解决它? 若是你连编程里这种最小的事情都很差好作,那你又怎么证实你对编程是有追求的呢?


本文已经同步到个人我的博客:传送门

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者之前发布的精选技术文章,以及后续发布的技术文章(以原创为主),而且逐渐脱离 iOS 的内容,将侧重点会转移到提升编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。

并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~

公众号:程序员维他命
相关文章
相关标签/搜索