跳转至

《代码整洁之道》

简介

《代码整洁之道》是 2009 年 12 月由人民邮电出版社出版的图书,作者是马丁([美]Robert C. Martin)。本书主要讲述了代码质量与其整洁度成正比的道理,并由此揭示代码整洁之道。

我于 2018 年 9 月开始拜读此书,陆陆续续读的很慢,深感文章之高屋建瓴,遂做读书摘要记录之。

《代码整洁之道》/《Clean Code: A Handbook of Agile Software Craftsmanship》
检索 豆瓣读书
封面 img

读书摘要

Ch1 整洁代码

1.1 要有代码

  • 代码不需人工编写?

扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些层面上,这些细节无法被忽略成抽象,必须明确之,将需求明确到机器可以执行的细节程度,就是编程需要做的事,而这种规约正是代码。

  • 代码确然是我们最终用来表达需求的那种语言。我们可以创造各种与需求接近的语言。我们可以创造帮助把需求解析和汇整为正式结构的各种工具。然而,我们永远无法抛弃必要的精确性——所以代码永存。

1.2 糟糕的代码

  • 勒布朗(LeBlanc)法则:

Later equals never.

稍后等于永不。

1.3 混乱的代价

  • 随着混乱增加,团队生产力也持续下降,趋近于 0。管理层只得增加更多的人手到项目中,期望提成生产力。可是新人并不熟悉系统的设计,他们搞不清楚什么样的修改符合设计意图,什么样的修改违背设计意图。而且,他们以及团队中的其他人都背负着提升生产力的可怕压力。于是,他们制造更多混乱,驱动生产力向 0 那端不断下降。
1.3.1 华丽新设计
  • 开发团队要求做全新的设计,只有最优秀的人员进入新团队,其余人维护现有系统。在新系统功能足以抗衡旧系统之前,管理层不会替换掉旧系统。两支队伍之间的竞赛可能会持续极长时间。我就见过延续十年之久的。到了完成的时候,新团队的老成员早已不知去向,而现有成员则要求重新设计一套新系统,因为这系统太烂了。

花时间保持代码整洁不但有关效率,还有关生存。

1.3.2 态度
  • 程序员遵从不了解混乱风险的经理的意愿,是不专业的做法。
1.3.3 谜题
  • 程序员面临一种基础价值谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后退。但开发者们背负期限的压力,只好制造混乱。简言之,他们没时间让自己做得更快。

真正的专业人士明白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做得快的唯一方法——就是始终尽可能保持代码整洁。

1.3.4 整洁代码的艺术
  • 写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。这种代码感就是关键所在。有些人生而有之。有些人费点劲儿才能得到。它不仅让我们看到代码的优劣,还予我们以借归戒之力化劣为优的攻略。
1.3.5 什么是整洁的代码
  • 访谈录

Ch2 有意义的命名

下文列出取个好名字的几条简单原则

2.2 名副其实

  • 变量、函数或类的命名应该已经答复所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算名副其实。
  • 只要简单改一下名称,就能轻易知道发生了什么。这就是选用好名称的力量。

2.3 避免误导

  • 程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。
  • hpaixsco都不该用作变量名,因为它们都是 Unix 平台或类 Unix 平台的专有名词。
  • 别用 accountList来指称一组账号,除非它真的是 List 类型。
  • 堤防使用不同之处较小的名称。想区分模块中某处的 XYZControllerForEffcientHandlingOfStrings和另一处的 XYZControllerForEfficientStorageOfStrings,会花多长时间呢?这两个词外形实在太相似了。
  • 以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。

2.4 做有意义的区分

  • 如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。
  • 光是添加数字系列或是废话远远不够,即便这足以让编译器满意。如果名称必须相异,那其意思也应该不同才对。
  • 以数字系列命名是依义命名的对立面,这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。
  • 废话是另一种没有意义的区分,废话都是冗余。Variable 一次永远不应当出现在变量名中。Table 一词永远不应当出现在表明中。

2.5 使用读得出来的名称

  • 使用恰当的英文词,而非傻乎乎的自造词。

2.6 使用可搜索的名称

  • 单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。
  • 长名称胜于短名称,搜索得到的名称胜于用自造编码代写就的名称。
  • 单字母名称仅用于短方法中的本地变量、名称长短应于其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋予其以便搜索的名称。

2.7 避免使用编码

  • 编码已经太多,无谓在自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外,还要再搞懂另一种编码语言。这对于解决问题而言,纯属多余的负担。
2.7.1 匈牙利语标记法(Hungarian Notation,HN)
  • 由于文本编辑器的进步,如今 HN 和其他类型编码形式都纯属多余。
2.7.2 成员前缀
  • 不必使用 m_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要。
2.7.3 接口和实现
  • 有时也会出现采用编码的特殊情形。比如你在做一个创建形状用的抽象工厂,该工厂是个接口,要用具体类实现。如何命名工厂和具体类呢?IShapeFactoryShapeFactory吗?我喜欢不加修饰的接口。前导字母 I 被滥用到了说好听点事干扰,说难听点根本就是废话。我不想让用户知道我给他们的是接口。我就想让他们知道哪个是 ShapeFactory。如果接口和实现必须选一个来编码的话,我宁肯选择实现。ShapeFactoryImp,甚至是丑陋的 CShaorFactory,都比到接口名称编码来得好。

2.8 避免思维映射

  • 不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题出现在选择是使用问题领域术语还是解决方案领域术语时。

2.9 类名

  • 类名和对象名应该是名词或名词短语,如 CustomerWikiPageAccountAddressParser。避免使用 ManagerProcessorDataInfo这样的类名。
  • 类名不应当是动词。

2.10 方法名

  • 方法名应当是动词或动词短语,如 postPaymentdeletePagesave。属性访问器、修改器和断言应该根据其值命名,并依 JavaBean标准加上 getsetis前缀。
  • 重载构造器时,使用描述了参数的静态工厂方法名。如:
Complex fulcrumPoint=Complex.FromRealNumber(23.0);

通常好于

Complex fulcrumPoint=new Complex(23.0);

2.11 别扮可爱

  • 扮可爱的做法在代码中经常体现为使用俗话或俚语。例如,别用 whack()来表示 kill()。别用 eatMyShorts()这类与文化紧密相关的笑话来表示 abort()
  • 言到意到。意到言到。
  • 宁可明确,毋为好玩。

2.12 每个概念对应一个词

  • 给每个抽象概念选一个词,并且一以贯之。例如,使用 fetchretrieveget来给在多个类中的同种方法命名。

2.13 别用双关语

  • 避免将同一单词用于不同目的。同一术语用于不同概念,基本就是双关语了。
  • 代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能一目览尽,而不必殚精竭虑地研究。

2.14 使用解决方案领域名称

  • 记住,只有程序员才会读你的代码。所以,尽管用那些计算机科学(CS)术语、算法名、模式名、数学术语吧。依据问题所涉领域来命名可不算是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义,其实他们早共通过另一名称了解这个概念了。

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

  • 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。
  • 优秀的程序员和设计师,其中工作之一就是分离解决方案领域和问题领域的概念。与所涉及问题领域更为贴近的代码,应当采用源自问题领域的名称。

2.16 添加有意义的语境

  • 很少有名称是能够自我说明的,反之,你需要用有良好命名的类、函数或名空间来放置名称,给读者提供语境。如果没有这个做,给名称添加前缀就是最后一招了。

2.17 不要添加没用的语境

  • 只要短名称足够清楚,就比长名称好。别给名称添加不必要的语境。
  • 精确是命名的要点。

Ch3 函数

3.1 短小

  • 函数的第一规则是短小、第二条规则是还要更短小。
  • 代码块和缩进:
  • if 语句、else 语句、while 语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具有说明性的名称,从而增加了文档上的价值。
  • 这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。

3.2 只做一件事(P33)

  • 过去 30 年,以下建议以不同形式一再出现:

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

  • 问题在于很难知道那件该做的事是什么。

  • 如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。

  • 要判断函数是否不只做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。

  • 函数中的区段

  • 一个函数被切分为多个区段,这就是函数做事太多的明显征兆。只做一件事的函数无法被合理地切分为多个区段。

3.3 每个函数一个抽象层级

  • 要确保函数只做了一件事,函数中的语句都要在同一抽象级上。
  • 自顶向下读代码:向下规则

我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。

3.4switch 语句

  • 写出短小的 switch 语句很难。Switch 天生要做 N 件事。不幸我们总无法避开 switch 语句,不过还是能够确保每个 switch 都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。

Payroll.java

public Money calculatePay(Employee e)
    throws InvalidEmployeeType {
      switch (e.type) {
        case COMMISSIONED:
          return calculateCommissionedPay(e);
        case HOURLY:
          return calculateHourlyPay(e);
        case SALARIED:
          return calculateSalariedPay(e);
        default:
          throw new InvalidEmployeeType(e.type);
  }
}

该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则(Single Responsibility Principle , SRP),因为有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle , OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有

isPayday(Employee e, Date date)

deliverPay(Employee e, Money pay)

该问题的解决方案是将 switch 语句埋到抽象工厂 底下,不让任何人看到。该工厂使用 switch 语句为 Employee 的派生物创建适当的实体,而不同的函数,如 calculatePay、isPayday 和 deliverPay 等,则藉由 Employee 接口多态地接受派遣。

对于 switch 语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍 [。当然也要就事论事,有时我也会部分或全部违反这条规矩。

Employee 与工厂

public abstract class Employee {
    public abstract boolean isPayday();

    public abstract Money calculatePay();

    public abstract void deliverPay(Money pay);
}
    -----------------

public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r)
            throws InvalidEmployeeType;
}
    -----------------

public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r)
            throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

3.5 使用描述性的名称

  • 给每个私有方法取个同样具有描述性的名称,如:isTableincludeSetup。好名称的价值怎么好评都不为过。

记得沃德原则:

如果每个例程都让你感到深合己意,那就是整洁代码。

要遵循这一原则,泰半工作都在于只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。

  • 别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。
  • 选择描述性的名称能清理你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。
  • 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。

#### 3.6 函数参数

  • 最理想的参数数量是零,其次是一,再次是二,应尽量避免三。有足够特殊的理由才能用三个以上参数。
  • 参数与函数名处在不同的抽象层级,它要求你了解目前并不特别重要的细节。
  • 从测试的角度看,参数甚至更叫人为难。如果参数多于两个,测试覆盖所有可能值的组合简直让人生畏。
3.6.1 一元函数的普遍形式
  • 向函数传入单个参数有两种极普遍的理由。

  • 操作该参数

  • 事件(event)

    在这种形式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如 void passwordAttemptFailedNtimes(int attempts)。小心使用这种形式,应该让读者很清楚地了解他是个事件。谨慎地选用名称和上下文语境。

  • 尽量避免编写不遵循这些形式的一元函数。

3.6.2 标识参数
  • 标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。如果标识为 true将会这样做,表示为 false 则会那样做!

如:方法调用 render(true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到 render(Bool isSuite),稍微有点帮助,不过仍然不够。应该把函数一分为二:renderForSuite()renderForSingleTest()

3.6.3 二元函数
  • 有两个参数的函数要比一元函数难懂。如:writeField(name)writeField(outputStream,name)好懂。

使用二元函数要付出代价。应该尽量利用一些机制将其转换成一元函数。

例如:可以把 writeField方法写成 outputStream的成员之一,从而能这样用:outputStream.writeField(name)。或者,也可以把 outputStream写成当前类的成员变量,从而无需再传递它。还可以分理出类似 FieldWriter的新类,在其构造器中采用 outputStream,并且包含一个方法。

  • 当然,有些时候两个参数正好。

例如:Point p=new Point(0,0);就相当合理。笛卡尔点天生拥有两个参数。

3.6.4 三元函数
  • 有三个参数的函数要比二元函数难懂的多。排序、琢磨、忽略的问题都会加倍体现。
3.6.5 参数对象
  • 如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。

例如:下面两个声明的差别:

Circle makeCircle(double x,double y,double radius);
Circle makeCircle(Point center,double radius);

从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,就像上例中的 x 和 y 那样,往往就是该有自己名称的某个概念的一部分。

3.6.6 参数列表
  • 有时,我们想要向函数传入数量可变的参数。

例如,String.format方法:

String.format("%s worked %.2f hours.",name,hours);

如果可变参数像上例中那样被同等对待,就和类型为 List的单个参数没什么两样。这样一来,String.format实则是二元函数。

下面 String.format的声明也很明显是二元的:

public String format(String format,Object... args);

同理,有可变参数的函数可能是一元、二元甚至是三元。超过这个数量就可能要犯错了。

void monad(Integer... args);
void dyad(String name,Integer... args);
void triad(String name,int count,Integer... args);
3.6.7 动词与关键字
  • 给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词形式。

例如:write(name) 就相当令人认同,不管这个 “name”是什么,都要被 “write”。更好的名称大概是 writeField(name),它告诉我们,“name”是一个 “field”

  • 使用函数名称的关键字形式,可以把参数名称编码成函数名。

例如:assertEqual改成 assertExpectedEqualsActual(expected,actual)可能会好些。这大大减轻了记忆参数顺序的负担。

3.7 无副作用

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

包括以下:

  • 对自己类中的变量做出未能预期的改动。
  • 把变量搞成向函数传递参数或是系统全局变量。

无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。

  • 输出参数

  • 参数多数会被自然而然地看作是函数的输入。

  • 在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面向对象语言中对输出参数的大部分需求已经消失了,因为 this 也有输出函数的意味在内。
  • 普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。

3.8 分隔指令与询问

  • 函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态;或是返回该对象的有关信息。两样都干常会导致混乱。

例如:

public boolean set(String attribute,String value);

该函数设置某个指定属性,如果成功就返回 true,如果不存在那个属性则返回 false。这样就导致以下语句:

if(set("username","unclebob"))...

为了解决因此造成的混乱,可以将 set函数重命名为 setAndCheckIfExits,但这对提高 if 语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆发生:

if(attributeExits("username")){
    setAttribute("username","unclebob");
    ...
}

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

  • 从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在 if 语句判断中把指令当做表达式使用:
if(deletePage(page)==E_OK)

这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。

if(deletePage(page)==E_OK){
    if(registry.deleteReference(page.name)==E_OK){
        if(configKeys.deleteKey(page.name.makeKey())==E_OK){
            logger.log("page deleted");
        }else{
            logger.log("configkey not deleted");
        }
    }else{
        logger.log("deleteReference from registry failed");
    }else{
        logger.log("delete failed");
        return E_ERROR;
    }
}

另一方面,如果使用异常代替返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化:

try{
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
catch(Exception e){
    logger.log(e.getMessage());
}
3.9.1 抽离 Try/Catch 代码块
  • try/catch 代码丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把 try 和 catch 代码块的主体部分抽离出来,另形成函数。
public void delete(Page page){
    try{
        deletePageAndAllReference(page);
    }
    catch(Exception e){
      logError(e);
    }
}
private void deletePageAndAllReference(Page page) throws Exception{
    deletePage(page);
    registy.deleteReference(page.name);
    configKeys.deleteKey(page.makeKey());
}

private void logError(Exception e){
    logger.log(e.getMessage());
}

有了这样美妙的区隔,代码就更易于理解和修改了。

3.9.2 错误处理就是一件事
  • 函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着如果关键字 try 在某个函数中存在。它就该是这个函数的第一个单词,而且在 catch/finally 代码块后面也不该有其他内容。
3.9.3 Error.java依赖磁贴
  • 返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。
public enum Error{
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WAITING_FOR_EVENT;
}

这样的类就是一块依赖磁贴(dependency magnet)。其他许多类都得导入和使用它。当 Error 枚举修改时,所有这些其他的类都需要重新编译和部署。这对 Error 类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们复用旧的错误码,而不添加新的。

  • 使用异常代词错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。

3.10 别重复自己

  • 重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。

例如:

  • 全部考德(codd)数据库范式都是为消灭数据重复而服务。
  • 面向对象编程是如何将代码集中到基类,从而避免冗余。
  • 面向方面编程(Aspect Oriented Programming)、面向组件编程(Component Oriented Programming)多少也都是消除重复的一种策略。

3.11 结构化编程

  • Edsger Dijkstra 的结构化编程规则:

每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个 return 语句,循环中不能有 break 或 continue 语句,而且永永远远不能有任何 goto 语句。

我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。

所以,只要函数保持短小,偶尔出现的 return、break 或 continue 语句没有坏处,甚至还比单入单出原则更具有表达力。另一方面,goto 只在大函数中才有道理,所以应该尽量避免使用。

3.12 如何写出这样的函数

  • 在写论文或文章时,你先想什么就写什么,然后在打磨它。初稿也许粗陋无序,你就斟酌推敲,直到达到你心目中的样子。

Ch4 注释

  • Brian W.KernighanP.J.Plaugher

别给糟糕的代码加注释——重新写吧。

  • 什么也不比放置良好的注释来得有用。

什么也不会比乱七八糟的注释更有本事搞乱一个模块。

什么也不会比陈旧、提供错误信息的注释更有破坏性。

  • 注释并不比辛德勒的名单。它们并不“纯然的好”。实际上,注释最多也就是一种必须的恶。若编程语言足够有表达力,或者我们长于用这些语言来表达意图,就不那么需要注释——也许根本不需要。

4.1 注释不能美化糟糕的代码

  • 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。

4.2 用代码来阐述

  • 有时,代码本身不足以解释其行为。然而许多程序员都据此以为代码很少——如果有的话——能做好解释工作。这种观点是错误的。
//check to see if the employee is eligible for full benefits
if((employee.flags & HOURLY_FLAG)&&(employee.age>65))
if(employee.isEligibleForFullBenefits())

后者只用代码解释你大部分的意图。很多时候,简单到只需创建一个描述与注释所言同一事物的函数即可。

4.3 好注释

  • 有些注释是必须的,也是有利的。
4.3.1 法律信息
  • 有时,公司代码规范要求编写与法律有关的注释。

例如:版权及著作权声明就是必须和有理由在每个源文件开头注释处放置的内容。

这类注释不应是合同或法典。只要有可能,就指向一份标准许可或其他外部文档,而不要把所有条款放到注释中。

4.3.2 提供信息的注释
  • 有时,用注释来提供基本信息也有其用处。

例如:以下注释解释了某个抽象方法的返回值。

//return an instance of the responder being tested
protected abstract Responder responderInstance();

这类注释有时管用,但更好的方式是尽量利用函数名称传达信息。比如吧函数名称改为 responderBeingTested,注释就是多余的。

例如:

//format matched kk:mm:ss EEE,MMM dd,yyyy
Pattern timeMatcher=Pattern.compile("\\d*:\\d*:\\d*\\w*,\\d*,\\d*");

在本例中,注释说明,该正则表达式意在匹配一个经由 SimpleDataFormat.format函数利用特定格式字符串格式化的时间和日期。同样,如果把这段代码移到某个转换日期和时间格式的类中,就会更好、更清晰,而注释也就变得多此一举。

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

在对比两个对象时,作者决定将他的类放置在比其他东西更高的位置。

public int compareTo(Object o){
    if(o instanceof WikiPagePath){
        WikiPagePath p=(WikiPagePath) o;
        String compressedName=StringUtil.join(p.name,"");
        return comparessedName.compareTo(compressedArguementName);
    }
    //we are greather because we are the right type
  return 1;
}
4.3.4 阐释
  • 有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用的。通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码,帮助阐释其含义的代码就会有用。

当然,这也会冒阐述性注释本身就不正确的风险。

4.3.5 警示
  • 有时,用于警告其他程序员会出现某种后果的注释也是有用的。
4.3.6 TODO注释
  • 有时,有理由用 //TODO形式在源代码中放置要做的工作列表。
4.3.7 放大
  • 注释可以用来放大某种看来不合理之物的重要性。

4.4 坏注释

大多数注释都属于此类。通常,怀注释都是糟糕代码的支撑或借口,或者对错误决策的修正,基本上等于程序员的自说自话。

4.4.1 喃喃自语
  • 如果只是因为你觉得应该或者因为过程需要就添加注释,那就是无谓之举。如果你决定写注释,就要花必要时间确保写出最好的注释。
  • 任何迫使读者查看其他模块的注释,都没能与读者沟通好,不值所费。
4.4.2 多余的注释
4.4.3 误导性注释
4.4.4 循规式注释
  • 所谓每个函数都要有 Javadoc 或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释徒然让代码变得散乱,满口胡言,令人迷惑不解。
4.4.5 日志式注释
  • 很久以前,在模块开始处创建并维护这些记录还算有道理。那时,我们还没有源代码控制系统可用。如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。
4.4.6 废话注释
  • 与其纠缠于毫无价值的废话注释,程序员应该意识到,他的挫败感可以由改进代码结构而消除。
4.4.7 可怕的废话
4.4.8 能用函数或变量时就别用注释
4.4.9 位置标记
  • 有时,程序员喜欢在源代码中标记某个特别位置。

例如:

//Action ////////////////////////////////////

把特定函数趸放在这种标记栏下面,多数时候实属无理。鸡零狗碎,理当删除——特别是尾部那一长串无用的斜杠。

如果标记栏不多,就会显而易见。所以,尽量少用标记栏,只在特别有价值的时候用。如果滥用标记栏,就会沉没在背景噪音中,被忽略掉。

4.4.10 括号后面的注释
  • 有时,程序员会在括号后面放置特殊的注释。尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们更愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数。
4.4.11 归属与署名
  • 源代码控制系统非常善于记住是谁在何时添加了什么,没必要用那些小小的签名搞脏代码。你也许会认为,这种注释大概有助于他人了解应该和谁讨论这段代码。不过,事实却是注释在那儿放了一年又一年,越来越不准确,越来越和原作者没关系。
4.4.12 注释掉的代码
  • 直接把代码注释掉是讨厌的做法。别这么干。

其他人不敢删除注释掉的代码,他们会想,代码依然放在那儿,一定有其原因,而且这段代码很重要,不能删除。注释掉的代码堆积在一起,就像破酒瓶底的渣滓一般。

4.4.13 HTML注释
  • 源代码注释中的 HTML 标记是一种厌物。
4.4.14 非本地信息
  • 假如你一定要写注释,请确保它描述了离它最近的代码。别在本地注释的上下文环境中给出系统级的信息。
4.4.15 信息过多
  • 别在注释中添加有趣的历史性话题或这无关的细节描述。
4.4.16 不明显的关系
  • 注释及其描述的代码之间的联系应该显而易见。如果你不嫌麻烦要写注释,至少让读者能看着注释和代码,并且理解注释为何物。
4.4.17 函数头
  • 段函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好。
4.4.18 非公共代码中的 Javadoc
  • 虽然 Javadoc对于公共 api非常有用,但对于不打算作公共用途的代码就令人厌恶了。为系统中的类和函数生成 Javadoc页并非总有用,而 Javadoc注释额外的形势要求几乎等同于八股文章。

Ch5 格式

5.1 格式的目的

5.2 垂直格式

5.3 横向格式

5.4 团队规则

Ch6 对象和数据结构

6.1 数据抽象

  • 下面两端代码都表示笛卡尔平面上的一个点。

不过,其中之一曝露了其实现,而另一个则完全隐藏了其实现。

具象点:

public class Point{
    public double x;
    public double y;
}

抽象点:

public interface Point{
    double getX();
    double getY();
    void setCartesian(double x,double y);
    double getR();
    double getTheta();
    void setPolar(double r,double theta);
}

抽象点的代码明白无误地呈现一种数据结构。

曝露实现并非只是在变量之间放上一个函数层那么简单,隐藏实现关乎抽象,类并不是简单的用取值器与赋值器将其变量向外推,而是曝露抽象接口,这样使用者无需了解数据的实现就可以操作数据的本体。

  • 不愿曝露数据细节,更愿意以抽象形态表述数据。这不是简单的用接口和赋值器与取值器就万事大吉了,要如何呈现某个对象包含的数据,需要认真地思考,不要傻乐的添加赋值器与取值器,那是最坏的选择。

6.2 数据、对象的反对称性

  • 对象与数据结构之间的二分原理

过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。

反过来也讲得通:

过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因此必须修改所有类。

  • 在任何一个复杂系统中,都会有需要添加新数据类型而不是新函数而不是数据类型的时候。在这种情况下,过程式代码和数据结构更合适。

6.3 得墨忒耳律

  • 著名的得墨忒耳律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情形。如上节所见,对象隐藏数据,曝露操作。这意味着对象不应通过存取器曝露其内部结构,因为这样更像是曝露而非隐藏其内部结构。
  • 更准确的说,得墨忒耳律认为,类 C 的方法 f 只应该调用以下对象的方法:

  • C

  • 由 f 创建的对象
  • 作为参数传递给 f 的对象
  • 由 C 的实体变量持有的对象

方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。

  • 下列代码违反了得墨忒耳律(除了违反其他规则之外),因为它调用了 getOptions()返回值的 getScratchDir()函数,又调用了 getScratchDir()返回值的 getAbsolutePath()
final string outputdir=ctxt.getOptions().getScratchDir().getAbsolutePath();
6.3.1 火车失事
  • 这类的代码(上例)被称为火车失事,因为它看起来就像是一列火车,这类连串的调用通常被认为是肮脏的风格,应该避免。最好做类似如下的切分:
Options opts=ctxt.getOptions();
File scratchDir=opts.getScratchDir();
final String optputDir=scratchDir.getAbsolutePath();

上例代码代码是否违反得墨忒耳律呢?当然,模块知道 ctxt 对象包含有多个选项,每个选项中都有一个临时目录,而每个临时目录都有一个绝对路径。

这些代码是否违反得墨忒耳律,取决于 ctxtOptionsScrachDir是对象还是数据结构。如果是对象,则它们的内部结构应当隐藏而不曝露,而有关其内部细节的知识就冥想违反了得墨忒耳律。如果 ctxOptionsScratchDir知识数据结构。没有任何行为,则它们自然会曝露其内部结构,得墨忒耳律也就不适用了。

属性访问函数的使用把问题搞复杂了。如果像下面这样写代码,我们大概就不会提及对得墨忒耳律的违反。

final String oupputDir-ctxt.options.scratchDir.absolutePath;

如果数据结构只简单地拥有公共变量,没有函数,而对象则拥有私有变量和公共函数,这个问题就不那么混淆。然而,有些框架和标准甚至要求最简单的数据结构都要有访问器和改值器。

6.3.2 混杂
  • 这种混淆有时会不幸导致混合结构,一半是对象,一半是数据结构。这种结构拥有执行操作的函数,也有公共变量或公共访问器及改值器。无论出于怎样的初衷,公共访问器及改值器都把私有化变量公开化,诱导外部函数以过程式程序使用数据结构的方式使用这些变量。

此类混杂增加了添加新函数的难度,也增加了添加新数据结构的难度,两面不讨好。应避免创造这种结构。它们的出现,展示了一种乱七八糟的设计,其作者不确定——或者更糟糕,完全无视——他们是否需要函数或类型的保护。

6.3.3 隐藏结构
  • 假使 ctxtOptionsScrachDir是对象,由于对象应隐藏其内部结构,我们就不该能够看到内部结构。这样一来,如何取到临时目录的绝对路径呢?
ctxt.getAbsolutePathOfScratchDirectoryOption();

或者

ctxt.getScratchDirectoryOption().getAbsolutePath();

第一种方案可能导致 ctxt对象中方法的曝露。第二种方案是在假设 getScratchDirectoryOption()返回一个数据结构而非对象。两种方案感觉都不好。

如果 ctxt是个对象,就应该要求它做点什么,不该要求它给出内部情形。那我们为何还要得到临时目录的绝对路径呢?之后的模块代码可以表明取得临时目录绝对路径的初衷是为了创建指定名称的临时文件。所以可以直接让 ctxtx对象来做这件事。

SufferedOutputStream bos=ctxt.createScratchFileStream(classFileName);

这下看起来像是个对象做的事了!ctxt隐藏了其内部结构,防止当前函数因浏览它不该知道的对象而违反得墨忒耳律。

6.4 数据传递对象

  • 最为精练的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时候被称为数据传送对象,或 DTO(Data Transfer Objects)DTO是非常有用的结构,尤其是在与数据库通信、或解析套接字传递的消息之类场景中。在应用程序代码里一系列将原始数据转换为数据库的翻译过程中,它们往往是排头兵。
  • 更常见的是 Bean 结构。Bean 拥有有赋值器和取值器操作的私有变量。对豆结构的半封装会让某些 OO 纯化论者感觉舒服些,不过通常没有其他好处。