代码整洁之道 总结

1.什么样的代码才是整洁的

从字面意思上理解,整洁的代码,对于程序员来说非常的一目了然,简单、整洁,结构清晰,逻辑清楚。代码其实是一种语言,传递的是逻辑,如果这份代码可以像我们说话一样快速的将逻辑传递给读者,那么这样一份代码就是一份整洁的代码。可以借助沃德原则:"如果每段代码欧让你感到深合已意,那就是整洁代码"

 

2.如何保持代码整洁

首先便是要有保持代码整洁的意识,书中反复提到的提到的一条童子军军规:让营地比你来时更干净。要写好整洁的代码,需要我们心中有着一个意识,我要写好我的代码,我的代码要让自己和别人看着舒服。

 

3.命名

3.1 介绍

软件中命名随处可见,包括变量、函数、参数、类、目录等等,我们就像是一个父亲,给自己的孩子取一个让别人看到就知道的名字。

3.2 名副其实

通过名称就知道要表达的意思。例如:表示已经消逝的时间天数,使用elapsetimeInDays就比d好得多。表示插入数据成功,用insertSuccess就比flag好得多。

3.3 避免误导

命名时避免使用与本意相悖的词。例如:hp,aix,sco不应做变量名,因为它们是UNIX平台的专有名词。使用accountList来指一组账号,除非它真的是List类型。如果容纳账号的容器是Set,那就会引起错误的判断。在命名时最好不要把容器内容带上,使用accountGroup或者bunchOfAccounts更好。

提防使用相差比较小的名称。例如:XYZControllerForEfficentHandingOfStrings和ZYZControllerForEfficientStorageOfStrings。

误导性真正可怕的例子:用小写字母l和大写字母O做变量。

3.4 有意义的区分

光是添加数字或是废话来做区分是远远不够的。

添加数字的反例:Spring中关于属性的拷贝方法 BeanUtils.copyProperties(source,target),参数就使用source和target来表示,而不是a1,a2.

使用废话的反例:存在类名Product,ProductInfo,ProductData。名称不同,但意思无区别。废话都是冗余,Variable永远不该出现在变量名中。

3.5 使用读得出来的名称

例如: (生成日期,年月日时分秒) 变量命名时 gennerationTimeStamp 就比genymdhms好的多,前者比后者读起来好得多,也更好理解。

3.6 使用可搜索的名称

单字母名称和数字常量有个问题,很难在一大篇文字中找出。找MAX_CLASS_PER_STUDENT很容易,想找数字7就很麻烦。单字母名称仅用于短方法中的本地变量。名称长短应与其作用域相对应,比如在一个类中经常使用的,就可以通过大写字母的方式来表示。

3.7 避免使用编码

反例: 变量名加入成员前缀 m_name  表示接口时加上I字母 接口名:IShapeFactory

3.8 避免思维映射

在循环体中通过变量来判断条件时,使用i,j或者k就比其他字母好。千万别用小写字母l和打字字母O。

3.9 类名

类名和对象名应该是名字或者名字短语。如:Customer、WikiPage、Account

3.10 方法名

方法名应该是动词或动词短语,如:postPayment,deletePage或save。

3.11 别扮可爱

例如: 将数据一半进行删除 deleteHalfData 就比 ringingFinger(打响指) 好,只有看过复联的人才知道。

3.12 每个概念对应一个词

给每个抽象概念选一个词,并且一以贯之。例如:插入统一同insert或者save。不要有时用insert,有时用save.

3.13 别用双关语

例如:在一个类中有两个add方法。一个是求和,一个是添加数据。要么把求和改成sum,要么把添加数据改成append.

3.14 使用解决方法领域名称

只有程序员才会阅读你的代码。根据所涉领域来命名不是最好的方法,可以用一些计算机科学术语,算法名,模式名,数学术语来命名。例如:AccountVisitor,JobQueue

3.15 使用源自所涉问题领域的名称

如果不能使用程序猿熟悉的术语来命名,那就采用所设计问题领域的名称来命名吧。至少,维护代码的程序猿可以去请求领域专家。

3.16 添加有意义的语境

很少有名称是能自我说明的。你需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。

例如: 有名为firstName,lastName,street,houseNumber,city,state和zipcode的变量,当它们一起出现时,很明确构成一个地址。但如果是state单独出现时,就很难推断时表示地址了。可以通过addrState来标识。更好的方案是将类命名为Address。

3.17 不要添加没用的语境

假设有一个名为“豪华版加油站”(Gas Station Deluxe)的应用,在其中给每个类加上GSD前缀就不是一个好点子。只要短名称足够清楚,就不要添加不必要的语境。

例如:GSDAccountAddress 就可以改成 AccountAddress

3.18 小结

我们有时候会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改的更清楚,其实大家都会感激你。

 

4.函数

4.1 短小

函数的第一规则是要短小。第二条规则是还要更短小。

4.2 只做一件事

函数应该只做一件事,做好这件事。

4.3 每个函数一个抽象层级

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中混杂不同抽象层级,往往让人迷惑。

反例: 一个方法中调用了其他方法.有getHtml()抽象层比较高的,也有PathParser.render(pathPath)抽象层级中等的,也有builder.append("xxxx")抽象层级底层的。

阅读代码时自顶向下阅读。每个函数后面都跟着位于下一个抽象层级的函数。

例如: A方法中调用了B方法。B方法应该跟在A方法后面,如果A中调用了多个方法,这些方法也应该按照一定逻辑顺序排在A后面。

4.4 使用描述性的名称

例如:includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage等。看到这些方法名,我们就知道这个方法是在做什么事情了。函数越短小,功能越集中,那就越好取名字。别害怕长名称,长而具有描述的名称,比短小而令人费解的名称好。长而具有描述的名称,要比描述性的长注释好。

4.5 函数参数

最理想的函数是零参数函数,其次是单参数函数,再次是二,应尽量避免三参数函数。参数不易对付,它带有太多概念性。

4.5.1 一元函数

向函数传入单个参数由三种普遍的理由。一个是问关于那个参数的问题,例如 boolean fileExist("MyFile").另一个可能是操作该参数,将其转化成其他东西。例如 UserDTO transfer(User user)。还有一个就是事件,有输出参数无输出参数。程序将函数看作一个事件。

4.5.2 二元函数

有两个参数的函数要比一元函数难懂。例如:writeField(name)比writeField(outputStream,name)好懂。如果可以的话,把outputstream提取成当前类的成员变量,从而无需再传递它。

4.5.3 三元函数

有三个参数的函数要比二元函数难懂的多,排序,琢磨,忽略的问题都会加倍提现,建议在写三元函数前一定要想清楚。

4.5.4 参数对象

如果函数需要三个,或三个以上的参数,就说明其中的一些参数应该封装为类了。例如:Circle makeCircle(double x,double y,double radius) 和Circle makeCircle(Point center,double radius);

4.6 动词与关键字

给函数去个好名字,能较好地解释函数的意图,以及参数的顺序和意图。函数和参数应当形成一种非常良好的动词/名词对形式。例如:writeField(name)就比witre(name)表示的意图更加清晰

4.7 无副作用

副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。例如:

boolean checkPassword(userName,password){

User user = UserGateway.findByName(userName);

if(user == null){

return false;

}

if(password.equals(user.getPassword()){

Session.initialize();

return true;

}

}

在验证用户名和密码的方法中,进行了session的初始化操作,这就是该方法的副作用,方法名并没有暗示会进行session初始化的操作,当某个误信了函数名的调用者想要检查密码时,就可能导致当前session被抹除的风险。可以通过修改方法名为checkPasswordAndInitializeSession(),虽然哪还是违反了"只做一件事"的原则。可以将session的初始化放到另一个方法,将检查密码和初始化session分开,在上层来调用两个方法。

4.8 分隔指令与询问

函数要么做什么事,要么回答什么事,但二者不可兼得。

反例:if(set("username","horizon")).该set方法的逻辑是先判断 是否有username属性,如果有,则设置horizon,返回true,如果没有则返回false.

应该要把判断属性和设置值分两步来做.

if(attributeExists("username")){

setAttribute("username","horizon");

}

4.9 使用异常替代返回错误码

从指令式函数返回错误码轻微违反了指令与询问分隔的规则。

例如: if(deletePage(page) == E_OK) 当返回错误码时,就是在要求调用者根据返回值去处理逻辑。这样会导致深层次嵌套。

直接在deltePage方法中,对错误的逻辑直接抛出异常,就不用去挨个判断返回了.

例如:try{

deletePage(page);

}catch(Exception e){

 

}

4.10 抽离try/catch代码块

try/catch代码块丑陋不堪,它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch中的代码块的主体部分抽离出来。

例如:

try{

methodA();

}catch(Exception e){

logError();

}

4.11 错误处理就是一件事

函数应该只做一件事,错误处理就是一件事。

4.12 别重复自己

重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为空志与消除重复而创建。在我们的开发过程中,相同的逻辑可能在多处被使用。这时,我们就应该根据逻辑将相同的逻辑抽离出来。

4.13 小结

遵循这些规则,函数就会短小,有个好名字,就可以被很好的归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,帮助你讲故事。

 

5.注释

5.1 介绍

注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败,如果你发现自己需要写注释,再想想看是否有办法通过代码来表达。只有代码能忠实地告诉你它做的事情

5.2 注释并不能美化糟糕的代码

写注释的常见动机是糟糕的代码的存在,与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。

5.3 用代码来阐述

有时,代码本身不足以解释其行为。但是其可能只有那么一两行,许多程序猿就觉得没有单独抽出去的必要,这种观点纯属错误。

例如:

//check to see if the employee is eligible for full benefits

if((employee.flags & HOURLY_FLAG) && emplyee.age > 65)

代码中对逻辑的判断添加了注释,但是完全有更优的方案,直接把判断的代码抽离成一个方法

if(employee.isEligibleForFullBenefits())

5.4 好注释

有些注释是必须的,也是有利的。不过要记住,唯一真正好的注释是你想办法不去写的注释

5.4.1 法律信息

有时,公司代码规范要求编写与法律有关的注释。如果可能,就指向一份标准许可或其他外部文档,不要把所有条款放在注释中。

5.4.2 提供信息的注释

//Return an instance of the Responder being tested

protected abstract Responder responderInstance(); 这类注释有时管用,但更好的方式是尽量利用函数名称传达信息。比如:将函数名改成responderBeingTested(),注释就是多余的了。

5.4.3 对意图的解释

有时,注释不仅提供了有关实现的有用信息,而且还提供了决定某个决定后面的意图。

//the status of extract,2 means requested,3 means extracting.

int getExtractStatus()

5.4.4 阐释

有时,注释把某些晦涩难懂的参数或返回值的意义翻译为某种刻度形式,也会是有用的。当然,更好的方法是尽量让参数或返回值自身就足够清楚

例如: assertTrue(a.compareTo(b) == 0)  // a == b

5.4.5 警示

有时,用于警告其他程序猿会出现某种后果的注释也是有用的。

例如:

public static SimpleDateFormat makeStadardHttpDateFormat(){

//SimpleDateFormat is not thread safe, so we

//need to create each instance independently

SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");

}

5.4.6 todo注释

有时,有理由用//TODO形式在源代码中放置要做的工作列表。

放大:注释可以用来放大某种看来不合理之物的重要性

例如:

String listItemContent = match.group(3).trim();

//the trim is real important.It removes the starting

//spaces that could cause thr item to be recognized

//as another list.

new ListItemWidget(this, listItemContent, this.level + 1);

return buildList(text.substring(match.end()));

公共API中的Javadoc:如果你在编写公共API,就该为它编写良好的Javadoc。就像其他注释一样,Javadoc也可能误导,不实用或者提供错误信息

5.5 坏注释

大多数注释都属此类。通过,坏注释都是糟糕的代码的支撑或借口,或者对错误决策的修正。

5.5.1 喃喃自语

如果知识因为你觉得应该或者因为过程需要就添加注释,那就是无谓之举。如果要写注释,就要话必要的时间写出最好的注释。

5.5.2 多余的注释:

例如:class User {

//the name of user

private String name;

}

5.5.3 误导性注释

有时,尽管初衷可嘉,程序猿还是会写出不够精确的注释,最终导致误导了读者。

5.5.4 循规式注释

所谓每个函数都要有Javadoc或每个变量都要有注释的规矩是不可取的,这类注释徒然让代码变得散乱。

5.5.5 日志式注释

有人在每次编写代码时,在模块开始出添加一条注释,表示本次修改的原因和日期。很久之前没有代码控制仓库还可以理解,不过有了代码仓库之后,这种注释就完全没有必要了。

5.5.6 废话注释

有时,你会看到纯然是废话的注释。

例如:

/**

Default constructor
**/
protected User(){

 

}

5.5.7 位置标记

有时,程序猿喜欢在源代码中标记某个特别位置.

例如:

// Actions //////////////////////////////

5.5.8 括号后面的注释

有时,程序猿会在括号后面放置特殊的注释。尽管这对于含有深度嵌套的长函数可能有意义。不过,最好的方法应该是将长函数拆分成一个个短函数

5.5.9 归属与署名

代码仓库能够记住是谁在合适添加了什么,没必要用那些签名来搞脏代码。

5.5.10 注释掉的代码

直接把代码注释掉是讨厌的做法,不用了就直接删掉,有代码仓库帮我们保存,不用留着。

5.5.11 HTML注释

源代码注释中的HTML标记是一种错误的做法,在代码中应避免它。

5.5.12 信息过多

别在注释中添加有趣的历史性话题或无关的细节描述。

5.5.13 不明显的联系

注释及其描述的代码之间的联系应该显而易见。如果你要写注释,至少让读者能看着注释和代码,并且理解注释所谈何物

5.5.14 函数头

短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好

5.5.15 非公共代码中的Javadoc

虽然Javadoc对于公共API非常有用,但对于不打算作公公用途的代码就完全没有必要用Javadoc了。

6.格式

6.1 目的

代码格式很重要,必须严肃对待。或许你认为"代码能工作"才是专业开发着的头等大事。然而,事实并不是这样的,今天编写的功能,下一版可能被修改,原始代码修改之后很久,其代码风格和可读性仍会影响后续开发的人。即便代码不存在,你的风格和律条仍然存活下来。

6.2 垂直格式

大部分出色的系统中,单个文件长度差不多200行-500行左右,尽管这并非不可违背的原则,也应该接受。短文件比长文件易于理解。

6.2.1 向报纸学习

源文件要向报纸那样,名称应当简单且一目了然,源文件最顶部应该给出高层次的概念和算法,细节应该往下逐次展开,直至最底层的函数和细节。

6.2.2 概念间垂直方向上的区隔

代码都是从上往下,从左往右读。每组代码行展示一条完整的思路,这些思路用空白行区隔开。例如:封包声明,导入声明和每个函数之间,都由空白行隔开。

6.2.3 垂直方向上的靠近

如果说空白行隔开了概念,那么靠近的代码则暗示了它们之间的紧密关系,紧密相关的代码应该互相靠近。

例如:同一个概念的方法应该靠近在一起

6.2.4 垂直距离

对于那些关系密切、放置与同一源文件中的概念,它们之前的区隔应成为易懂性的衡量标准,应避免读者在源文件和类中跳来跳去。

1.变量声名:变量声明应尽可能靠近其使用位置 

反例:  

InputStream is = null;

//间隔了十多行代码

is = new FileInputStream("myFile");

2.实体变量:实体变量应该在类的顶部声明

3.相关函数:若某个函数调用了另外一个,就应该把它们放在一起,而且调用者应尽可能在被调用者上面。

4.概念相关:概念相关的代码应该放在一起,相关性越强,彼此之前的距离就该越短。

6.2.5 垂直顺序

我们想自上向下展示函数调用依赖顺序,被调用函数应该在执行调用函数的下面,这样就建立了一种自顶向下贯穿模块的良好信息流。

6.3 横向格式

应该尽力保持代码行短小,一般来说,代码行的长度不要超过120个字符

6.3.1 水平方向上的区隔与靠近

我们使用空格字符将紧密相关的事物连接在一起,也用空格字符把相关性较弱的事物分隔开。

1.赋值:赋值语句的等号两边由空格隔开,因为赋值有两个确定的要素,左边和右边,空格字符加强了分隔效果。

例如: int lineSize = line.length();

2.函数名和参数:不在函数名和左圆括号之间加空格,因为函数与参数密切相关,如果隔开,就会显得没有关系

3.运算符:乘法因子之间不加空格,因为它们具有较高优先级。加减法之前用空格隔开,因为加减法优先级较低

6.3.2 缩进

大多数类声明,不缩进。类的方法相对该类缩进一个层级,方法的实现相对方法声明缩进一个层级。代码块的实现相对于其容器代码块索引一个层级,以此类推。不要在短小的if语句,while循环中违反缩进规则。

反例:public CommentWidget(ParentWidget parent, String text){super(parent, text);}

6.3.3 空范围

有时,while或for语句的语句体为空。我们很容易被while循环语句同一行末尾的分号欺骗,尽量不要这样使用。

反例: while(dis.read(buf, 0, readBufferSize) != -1);

6.4 团队规则

每个程序猿都有自己喜欢的格式规则,但如果在一个团队工作,就是团队说了算。一组开发者应当认同一种格式风格,每个成员都应该采用那种规则。

 

7.对象和数据结构

7.1 数据抽象

对类的设计应该要隐藏具体的实现,隐藏实现关乎抽象。类并不是简单地用取值器和赋值器将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。

例如:

具象点:

public class Point{

public double x;

public double y;

}

抽象点:

public interface Point{

double getX();

double getY();

void setCartesian(double x, double y);

void setPolar(double r, double theta);

}

7.2 数据、对象的反对称性

对象把数据隐藏于抽象之后,暴露操作数据的函数。而数据结构暴露其数据,不提供有意义的函数。它们其实是对立的。

对象和数据结构之间存在二分原理:过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。这其实就是一个抽象和具体的一个差异,对象其实是比较抽象的,而数据结构是具体的。

7.3 得墨忒耳律

得墨忒耳律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情形,对象隐藏数据,暴露操作。方法不应调用由任何函数返回的对象的方法,换言之,只跟朋友谈话,不与陌生人谈话。

反例:ctx.getOptions().getScratchDir();因为它调用了getOptions()返回值的getScratchDir()函数。

7.4 数据传送对象

最为精炼的数据结构,是一个只有公共变量、没有函数的类,这种数据结构就是我们用的DTO。在数据库通信、解析传递的参数时,是最常用的。

7.5 小结

对象暴露行为,隐藏数据。便于添加新对象类型而无需修改现有行为,同时也难以在既有对象中添加新行为。

数据结构暴露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向已有函数添加新数据结构。

 

8.错误处理

8.1 使用异常而非返回码

如果我们在某些判断某些操作的执行情况时,手动返回操作成功或者失败。那么就会导致一个问题,我们必须在调用这个方法之后立即检查执行情况。不如直接在操作失败是直接抛出异常,直接调用时不抛异常,就表示操作是成功的

例如: 

public Boolean handle(){

     int x = operation();

     return x > 0;

}

通过operation的返回值来判断是否操作成功,不如在operation内部操作失败时直接抛出异常。

public Boolean handle(){

     operation();

     return true;

}

8.2 先写Try-Catch-Finally语句

异常的好处之一是,它们在程序中定义了一个范围。执行try部分的代码时,你是在表明可随时取消执行,并在catch语句中续接。在编写可能抛出异常的代码时,先写出try-catch-finally语句,这能帮你定义应该需要做什么,无论try代码块中执行的代码出什么错都一样。

8.3 使用可控异常时catch链不要过深

当我们在方法里主动throw了一个异常之后,可能catch语句在这个方法的三个层级之上,这意味这每个调用该方法的函数都要修改,捕获新异常。以此类推,最终得到的就是一个从底层贯穿到顶层的修改链。

8.4 给出异常发生的环境说明

你抛出的每个异常,应创建信息充分的错误消息,和异常一起传递出去,包括失败的操作和失败类型。

8.5 定义常规流程

按照8.1所说,使用异常而非返回码,这样的话代码会变得简洁,但是可能会出现下面这种情况。

try{

     MealExpenses expenses = expenseReportDAO.getMeals(employee.getId());

     total += expenses.getTotal;

}catch(MealExpensesNotFound e){

     total += getMealPerDiem();

}

业务逻辑是,如果消耗了餐食,则计入总额。如果没有消耗,则员工得到当日餐食补贴。我们在判断是否消耗餐食时,通过抛出数据没有找到的异常来做逻辑判断,这其实是不合理的。应该在expenseReportDAO.getMeals()方法中,如果没有查询到对应的数据,就返回一个参数补贴的对象,而不是在异常里面去继续处理逻辑。

8.6 别返回null值

要讨论错误处理,就要提及到容易引发错误的做法,返回null值就是其中的一个。我们需要在每次调用都去判断是否为null,其实在增加我们的工作量,如果忘记检查,程序就会失控。不如直接抛出异常或者判处特例对象。

例如:

List employees = getEmployees();

if(employees != null){

   for(Employee e : employees){

      System.out.println(e.getName());

}

}

其实,我们只要在getEmployees()方法中,如果结果为null时,返回一个空的List,就没有必要去判断是否为null了

8.7 小结

如果将错误处理隔离看待,独立于主要逻辑之外,我们就能单独处理它,也极大提升了代码的可维护性和整洁性。

9.最终收获

我知道了我要写好我的代码。

你可能感兴趣的:(学习过程)