跳转至

📔 代码整洁之道 学习笔记

阅读时间:2023.10.25 ~ 2023.11.01

更新时间:2023.11.01

1. 整洁代码

  • 将需求明确到到机器可以执行的细节程度,就是编程要做的事。这种规约就是代码。

  • 糟糕代码的代价

    • 会导致维护性越来越差,最终没法管理。
  • 为什么会出现糟糕的代码?

    • 赶需求发布时间,时间不足
    • 开发经验少。

1.1 混乱的代价

  • 进度缓慢

    • 有些团队在项目初期进展迅速,但也有那么一两年的时间却慢如蜗行。
  • 代码修改影响面控制较差

    • 对代码的每次修改都影响到其它两三处代码。
    • 修改无小事。
  • 生产力下降

    • 导致增加人手来提升生产力。
  • 新人没法快速熟悉系统设计

    • 导致无法分析设计意图,从而导致生产力向零端下跌。

1.2 什么是整洁代码?

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

  • 程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。

  • 制造混乱无助于赶上期限,混乱只会拖慢你,叫你错过期限。

    • 赶上期限的唯一方法,做得快的唯一方法:
      • 始终尽可能保持代码整洁。
  • 写出整洁代码的方式

    • 遵循大量的小技巧,培养“整洁感”。
    • 培养“代码感”,可以帮助你从混乱代码中看出可能与变化,从而选出最优方案,并指导程序员制定修改计划。
  • 整洁代码的标准

    • Bjarne Stroustrup的观点:

      • 优雅和高效,代码逻辑应当直截了当,缺陷难以隐藏。
      • 尽量减少依赖关系,便于维护。
      • 分层次战略完善错误处理代码,性能调至最优,省得引诱别人做没意义的优化,从而造成混乱。
    • Grady Booch的观点:

      • 整洁的代码简单直接。
      • 整洁代码不隐藏设计者的意图,干净利落的抽象和直截了当的控制语句。
    • Dave Thomas 的观点(测试驱动开发方式)

      • 整洁的代码应可由作者之外的开发者阅读和增补。
      • 有单元测试和验收测试。
      • 使用了有意义的命名。
      • 尽量少的依赖关系,其明确地定义和提供清晰、尽量少的API。
      • 可以通过字面表达含义。
    • Michael Feathers 的观点:

      • 如何在意代码?是作者关注细节在意过的代码。
    • Ron Jeffries 的观点:

      • 简单的代码依其重要顺序:
        • 能通过所有测试。
        • 没有重复代码。
        • 体现系统中的全部设计理念。
        • 包括尽量少的实体,比如类、方法、函数等。
      • 总结:
        • 不要重复代码,只做一件事,表达式、小规模抽象。
    • Ward CunningHam 的观点:
      • 如果每个例程深合己意,就是整洁代码。
      • 如果代码让编程语言看起来像是专为解决问题而存在,则称为漂亮的代码。

读与写花费时间的比例超过10:1。写新代码时,一直在阅读旧代码。

编写代码的难度,取决于读周边代码的难度。要想干得快,要想早点做完,要想轻松写代码,先让代码易读。

对于代码要注意保持整洁,因为会出现代码随着时间流逝而腐坏。

  • 让营地比你来时更干净。
  • 如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。

2. 有意义的命名

软件中随处可见命名。所以对于取个好名字可以遵守几条简单的规则:

  • 名副其实

    • 变量、函数或类的名称应该答复了所有的大问题。如果名称需要用注释来补充,那就不算是名副其实。

    • 代码模糊度:

      • 上下文在代码中未被明确体现的程度。
    • 只要改为有意义的名称,代码就会有相当程度的改进。

  • 避免误导

    • 程序员必须避免留下掩藏代码本意的错误线索。

      • 应当避免使用与本意相悖的词。
    • 以同样的方式拼写出同样的概念才是信息。

      • 拼写前后不一致就是误导。
  • 做有意义的区分

    • 如果程序员只是为满足编译器或者解释器的需要而写代码,就会制造麻烦。

      • 在同一个作用范围内两样不同的东西不能重名,如果名称必须相异,那其意思也应该是不同才对。
    • 以数字系列命名的,会造成误导。因为完全没提供正确信息,没有提供导向作者意图的线索。

    • 废话是另一种没意义的区分。假设有一个 Product类,还有一个ProductInfo或者ProductData类,名称虽然系统,但意思却无区别。

    • 只要体现出有意义的区分,使用 a 和 the 类似的前缀都没问题。

    • 废话都是冗余

      • Name不会有float类型,所以NameString没必要。
    • 如果缺少明确约定,要区分名称,就要以读者能鉴别不同之处的方式来区分。

  • 使用读得出来的名称

    • 如果名称读不出来(糟糕的命名),会给新开发者解释变量的意义时,总是读出自造词,而不是恰当的单词。
  • 使用可搜索的名称

    • 单字母名称和数字常量无法在大篇幅代码中搜索出来。

    • 长名称胜于短名称,搜得到的名称胜过自造编码代写的名称。

    • 名称的长短应与其作用域大小相对应。

      • 若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。
  • 避免使用编码

    • 把类型或作用域编进名称里面,徒然增加解码的负担。

    • 也不必用 m_ 前缀来标明成员变量。

      • 应当把类和函数做得足够小,消除对成员前缀的需要。
    • 对于特殊情形的编码,如果不加修饰的接口,不要随意使用大写字母I。

  • 避免思维映射

    • 不应当让读者在脑中把你的名称翻译成他们熟知的名称。

    • 专业程序员了解,明确是王道。

    • 专业程序员善用其能,编写其他人能理解的代码。
  • 类名

    • 类名和对象名应该是名词或者名词短语。
    • 类名不应该是动词。
  • 方法名

    • 方法名应当是动词或者动词短语。
    • 属性访问器、修改器和断言应该根据其值命名,并依 Javabean 标准加上 getsetis 前缀。
    • 重载构造器时,使用描述了参数的静态工厂方法名。

      • 可以将相应的构造器设置为 private,强制使用这种命名手段。
  • 别扮可爱

    • 如果名称太耍宝,就只有作者一般有幽默感的人才记得住。
    • 言到意到,意到言到。
  • 每个概念对应一个词

    • 给每一个抽象概念选一个词,并且一以贯之。
  • 别用双关语

    • 双关语:同一术语用于不同概念。
    • 避免同一个单词用于不同目的。

    • 代码作者应尽力写出易于理解的代码。

      • 把代码写得让别人一目尽览,而不必殚精竭虑地研究。
  • 使用解决方案领域名称

    • 尽可能用CS相关的专业术语。因为只有程序员才会阅读。
    • 尽可能取个技术性的名称,是最靠谱的做法。
  • 使用源自所涉问题领域的名称

    • 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称。
    • 优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。
  • 添加有意义的语境

    • 使用有良好命名的类、函数或名称空间来设置名称,给读者提供语境。
    • 如果达不到,则可以通过给名称添加前缀的方式。
  • 不要添加没用的语境

    • 只要短名称足够清楚,就要比长名称好。
    • 别给名称添加不必要的语境。
  • 最后的话

    • 取好名字最难的地方在于需要良好的描述技巧和共有文化背景。

3. 函数

  • 短小

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

    • 每个函数只有两行、三行或者四行的长度,使得每一个函数都一目了然。

    • 代码块和缩进

      • if语句、else语句、while语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。
      • 块内调用的函数拥有较具有说明性的名称,从而增加了文档上的价值。
      • 函数不应该大到足以容纳嵌套结构,最好不要多于一层或者两层。
    • 保持函数易于阅读和理解。

  • 只做一件事

    • 函数应该做一件事,做好这件事,只做这一件事。
    • 要判断函数是否不止做了一件事,就是看能否再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。
    • 只做一件事的函数无法被合理地切分为多个区段。
  • 每个函数一个抽象层级

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

      • 让代码拥有自顶向下的阅读顺序,让每个函数后面都跟着位于下一抽象层级的函数。

      • 让代码读起来像是一系列自顶向下的TO起头段落是保持抽象层级协调一致地有效技巧。

      • 也是保持函数短小、确保只做一件事的要诀。
  • switch语句

    • 写出短小的switch语句很难。只做一件事的switch更难,switch天生就是要做N件事。

    • 能够确保每个switch都埋藏在较低的抽象层级,而却永远不会重复。

    • switch语句很容易违反两个原则(原因:有新的case id就会增加变长):

      • 单一权责原则(Single Responsibility Principle,SRP)。
      • 开放闭合原则(Open Closed Principle, OCP)。
  • 使用描述性的名称

    • 沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码”。
    • 函数越短小、功能越集中,就越便于取个好名字。
    • 别害怕长名称。长而具有描述性的名称,有一定的好处:

      • 比短而令人费解的名称好。
      • 比描述性的长注释好。
      • 可以快速理清关于模块的设计思路,并改进。
    • 别害怕花时间取名字

    • 命名方式要保持一直。使用与模块名一脉相承的短语、名词和动词给函数命名。
  • 函数参数

    • 最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。

      • 无足够特殊的理由最好别用三个以上的参数(多参数函数)。
    • 事件:

      • 有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态。
      • 注意:谨慎选用名称和上下文语境。
    • 对于转换的操作,如果函数要对输入参数进行转换操作,转换的结果就该体现在返回值上。

    • 向函数传入布尔值,就是告诉本函数不止做一件事。所以杜绝该使用方式。

    • 如果函数看来需要两个、三个或者三个以上参数,就说明其中一些参数应该封装为类。

      • 从参数创建对象,从而减少参数数量。
    • 给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。

      • 对于一元函数,函数和参数应当形成一种非常良好的动词/名词形式。
      • 对于好的名字,可以减轻记忆参数顺序的负担。
  • 无副作用

    • 函数操作只做一件事,但还是会做其他被藏起来的事情,从而导致古怪的时序性耦合及顺序依赖。所以说副作用是一种谎言。

    • 如果函数必须要修改某种状态,就修改所属对象的状态。

  • 分隔指令与询问

    • 函数要么做什么事,要么回答什么事,但二者不可得兼。
  • 使用异常替代返回错误码

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

      • 鼓励在if判断语句中把指令当做表达式使用。
    • 好处:

      • 当返回错误码时,就是要求调用者立刻处理错误。
      • 如果使用异常替代返回错误码,错误处理代码就可以从主路径代码中分离出来,得到简化。
    • 抽离 try/catch 代码块

      • 原因:

        • 代码丑陋
        • 搞乱代码结构。把错误处理与正常流程混为一谈。
      • 函数应该只做一件事。错误处理就是一件事。

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

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

    • 软件开发领域的所有创新就是不断尝试从源代码中消灭重复。

  • 结构化编程

    • Dijkstra的结构化编程规则:

      • 每个函数、函数中每个代码块都应该有一个入口、一个出口。
        • 每个函数中只有一个return语句,
        • 循环中不能有break或continue语句。
        • 不能有任何goto语句。
    • 只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。尽量避免使用 goto语句。

  • 如何写出这样的函数?

    • 先写,然后打磨代码,分解函数、修改名称、消除重复。
    • 缩短和重新安置方法,重新拆散类,保证测试通过。

4. 注释

若编程语言足够有表达力,则不需要使用注释。

注释的恰当用法是弥补在用代码表达意图时遭遇的失败。注释总是一种失败。

为什么贬低注释?

  • 注释会撒谎

  • 时间久远,维护不及时,就会出现错误的讯息。

程序员应当负责将注释保持在可维护、有关联、精确的高度。跟主张把力气都用在写清楚代码上,直接保证无需编写注释。有时也需要注释,但也该花心思尽量减少注释量。

不准确的注释要比没注释坏的多。

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

  • 写注释的常见动机之一是糟糕代码的存在。

  • 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。

  • 与其花时间编写解释搞出糟糕代码的注释,不如花时间清理那那一堆的糟糕的代码。

3.2 用代码来阐述

  • 有时,代码本身不足以解释其行为。

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

3.3 好注释

唯一真正好的注释是你想办法不去写的注释。

  • 法律信息

    • 版权及著作权声明(一般放在源文件开头注释处放置的内容)
  • 提供信息的注释

  • 对意图的解释

  • 阐释

  • 警示

    • 警示后续程序员会出现某种后果的注释。
  • TODO注释

    • 放置要做的工作列表。
  • 放大

    • 注释可以放大某种看似不合理事物的重要性。
  • 公共API中的doc说明

3.4 坏注释

坏注释都是垃圾代码的支撑或者接口,或者对错误决策的修正,基本上等于程序员自说自话。

  • 喃喃自语

  • 多余的注释

  • 误导性注释

  • 循规式注释

  • 日志式注释

  • 废话注释

  • 可怕的废话

  • 能用函数或者变量时就别用注释

  • 位置标记

    • 尽量少用标记栏,只在特别有价值时才用。如果滥用标记栏,就会沉没在背景噪音中,被忽略掉。
  • 括号后面的注释

    • 括号后标记属于那个块的方式尽量避免。
  • 归属与署名

  • 注释掉的代码

  • HTML注释

  • 非本地信息

    • 注释一般在函数上面,保持注释对应函数描述。
  • 信息过多

    • 无需注释细节描述。
  • 不明显的联系

    • 注释及其描述的代码之间的联系应该显而易见。
  • 函数头

    • 短函数不需要太多描述。
      • 给短函数取个好名字,通常比写函数头注释要好。
  • 非公共代码中的JavaDoc

5. 格式

  • 团队一起采用一套简单的格式规则,所有成员都要遵从。可以使用格式规则的自动化工具。

5.1 格式的目的

  • 代码格式关乎沟通,而沟通是专业开发者的头等大事。

5.2 垂直格式

  • 短文件通常比长文件易于理解。

  • 源文件也要像报纸文章那样。名称应当简单明了且一目了然。

    • 源文件最顶部应该给出高层次概念和算法,细节应该往下渐次展开,直至找到源文件最底层的函数和细节。
  • 几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每代码行展示一条完整的思路。这些思路用空白行区隔开来。

  • 如果说空白隔开了概念,靠近的代码行则暗示了它们之间的亲密关系。

    • 紧密相关的代码应该互相靠近。
  • 除非有很好的理由,否则就不要把关系密切的概念放到不同的文件中,实际上,这也是避免使用 protected 变量的理由之一,应避免迫使读者在源文件和类中跳来跳去。

    • C++中变量声明
      • 应尽可能靠近其使用位置。
    • 循环中的控制变量
      • 应该总是在循环语句中声明。
    • 实体变量
      • 应该在类的顶部声明。
    • 相关函数

      • 互相调用应该放在一起,而且调用者应该尽可能放在被调用者上面。
    • 概念相关

      • 概念相关的代码应该放在一起。
      • 习惯性越强,彼此之间的距离就应该越短。
  • 垂直顺序

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

5.3 横向格式

  • 应该尽力保持代码行短小。一向遵循无需拖动滚动条到右边的规则,争取每行代码的长度不超过100或者120个字符。

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

    • 在赋值语句左右两边,空格字符加强了分隔效果。
    • 不在函数名和圆括号之间加空格。
    • 加减乘除之间,优先级问题,乘除靠一起,加减一起,两种之间用空格分割。
  • 对齐像是强调不重要的东西,把目光从真正的意义上拉开。

    • 汇编语言对齐增强可读性,别的语言可不追求。
    • 如果哟较长的列表需要做对齐处理,那问题就是在列表的长度上而不是对齐上。
  • 程序员相当依赖缩进模式。

    • 例如控制语句结构。如果没缩进模式,格式可读性会更差。
  • 空范围

    • 在控制语句中,不做任何执行,通过使用分号占位,要做好分号缩进。不然可读性更差。

5.4 团队原则

  • 一组开发者应当认同一种格式风格,每个成员都应该采用那种风格。

  • 好的软件系统是由一系列读起来不错的代码文件组成的,需要拥有一致和顺畅的风格。

  • 绝对不要用各种不同的风格来编写代码,这样会增加其复杂度。

6. 对象和数据结构

6.1 数据抽象

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

6.2 数据、对象的反对称性

  • 对象把数据隐藏于抽象之后,暴露操作数据的函数。数据结构暴露其数据,没有提供有意义的函数。

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

    • 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。
    • 过程式代码难以添加新数据结构,因为必须要修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。
  • 在任何一个复杂系统中,根据不同的情况去进行合适选择:

    • 如果要添加新数据类型而不是新函数时

      • 对象和面向对象比较适合。
    • 如果想添加新函数而不是数据类型时

      • 过程式代码和数据结构更合适。

6.3 得墨忒耳律

得墨忒耳律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情形。意味着对象不应通过存取器暴露其内部结构。因为这样更像是暴露而非隐藏其内部结构。

  • 不要写那种火车失事型的代码。避免使用这种肮脏风格的代码。

    final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    
  • 一半是对象,一半是数据结构,这种混合结构会导致不幸。

    • 这种结构拥有执行操作的函数,也有公共变量或者公共访问器及改值器。
  • 通过隐藏结构的方式进行优化,使得上述的代码看起来像是对象做的事。

    BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
    

6.4 数据传送对象

  • 最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象(Data Transfer Objects,DTO)。

    • 主要在与数据库通信、或解析套接字传递的消息之类场景中。

    • Active Record是一种特殊的DTO形式。它们是拥有公共(或可豆式访问的)变量的数据结构。

      • 解决方案:把Active Record当做数据结构,并创建包含业务规则,隐藏内部数据结构的独立对象。

7. 错误处理

  • 错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。

  • 使用异常而非返回码

    • 遇到错误时,最好抛出一个异常。调用代码整洁,其逻辑不会被错误处理搞乱。
  • 先写try-catch-finally 语句

    • 异常的妙处之一是:在程序中定义了一个范围。执行try-catch-finally语句中try部分的代码时,是在表明可随时取消执行,并在catch语句中接续。

    • 在某种意义上,try代码块像是事务。catch代码块将程序维持在一起持续状态。

    • 尝试编写强行抛出异常的测试,再往处理器中添加行为,使之满足测试要求。结果就是要先构造try代码块的事务范围,而且也会帮助你维护好该范围的事务特征。

  • 使用不可控异常

    • 可控异常的代价是违反开放/闭合原则。但得在catch语句和抛出异常处之间的每个方法签名中声明该异常。
    • 可控异常意味着对软件中较低层次的修改,都将波及较高层级的签名。
      • 如果异常可控,则函数签名就要添加 throw 子句。
  • 给出异常发生的环境说明

    • 抛出的每个异常,都应当提供足够的环境说明,以便于判断错误的来源和处所。

    • 应创建信息充分的错误信息,并和异常一起传递出去。

  • 依调用者需要定义异常类

    • 当在应用程序中定义异常类时,最重要的是考虑应该是它们如何被捕获?

    • 将第三方API打包是良好的实践手段,可以降低其依赖。也可以有助于模拟第三方调用。

  • 定义常规流程

    • 特例模式(SPECIAL CASE PATTERN),创建一个类或配置一个对象,用来处理特例。
    • 异常行为被封装到特例对象中。
  • 别返回null值

    • 返回null值,基本上就是给自己增加工作量,也是给调用者添乱。只要有一处没检查null值,应用程序就会失控。
  • 别传递null值

    • 在方法中返回null值是糟糕的做法,但将null值传递给其它方法更糟糕。除非API要求你向它传递null值,否则就要尽可能避免传递null值。

    • 在大多数编程语言中,没有良好的方法能对付由调用者意外传入的null值。

    • 最好的做法:禁止传入null值。

8. 边界

  • 使用第三方代码

    • 第三方程序包和框架提供者追求普适性,这样就能在多个环境中工作,吸引广泛的用户。

    • 建议不要将Map(或在边界上的其它接口)在系统中传递。如果使用类似Map这样的边界接口,就把它保留在类或者近亲类中,避免从公共API中返回边界接口,或者将接口作为参数传递给公共API。

  • 浏览和学习边界

  • 学习性测试的好处不只是免费

    • 学习性测试毫无成本。编写测试则是获得这些知识的容易而不会影响其他工作的途径。

    • 学习性测试是一种精确测试,有诸多好处:

      • 帮助增进对API的理解。
      • 确保第三方程序包按照想要的方式工作。
  • 使用尚不存在的代码

    • 编写想得到的接口,好处之一是可以保证在我们的控制下。有助于保持客户代码可读,且集中它该完成的工作。
  • 整洁的边界

    • 边界的改动会发生有趣的事。有良好的软件设计,无需巨大投入和重写即可进行修改。

    • 边界上的代码需要清晰的分割和定义了期望的测试。应该避免代码过多地了解第三方代码中的特定信息。

    • 依靠能控制的东西,好过依靠你控制不了的东西,免得日后受它控制。

    • 除了Map封装的方式,可以使用ADAPTER模式将接口转换为第三方提供的接口。

9. 单元测试

  • TDD三定律

    • 定律一:在编写不能通过的单元测试前,不可编写生产代码。
    • 定律二:只可编写刚好无法通过的单元测试,不能编译也算不通过。
    • 定律三:只可编写刚好足以通过当前失败测试的生产代码。
  • 保持测试整洁

    • 脏测试等同于没测试,问题在于,测试必须随着生产代码的演进而修改。
    • 测试代码和生产代码一样重要。测试代码也需要被思考、被设计和被照料,让其像生产代码一样保持整洁。

    • 如果测试不能保持整洁,你就会失去它们,没有了测试,你就会失去保证生产代码可扩展的一切因素。

    • 覆盖了生产代码的自动化单元测试程序组尽可能地保持设计和架构的整洁。

    • 如果测试不干净,改动自己代码的能力就会有所牵制。

  • 整洁的测试

    • 三个重要的因素:可读性、可读性和可读性。在单元测试中,可读性甚至比在生产代码中还重要。

    • 构造 - 操作 - 检验(BUILD-OPERATE-CHECK)模式

      • 每个测试都需要清晰地拆分为三个环节:
        • 构造测试数据
        • 操作测试数据
        • 检验操作是否得到期望的结果。
    • 测试语言可以帮助程序员编写自己的测试,也可以帮助后来者阅读测试。

    • 守规矩的开发者也将其测试代码重构为更简洁和具有表达力的形式。

  • 每个测试一个断言

    • JUnit中每个测试函数都应该有且只有一个断言语句。
    • 单个断言是个好准则,通常都会创建支持这条准则的特定领域测试语言。

      • 最好的说法:单个测试中的断言数量应该最小化。
    • 每一个测试函数只测试一个概念。

    • 最佳规则应该是尽可能减少每个概念的断言数量,每个测试函数只测试一个概念。

  • F.I.R.S.T

    • 快速(First)

      • 测试应该够快
    • 独立(Independent)

      • 测试应该相互独立
    • 可重复(Repeatable)

      • 测试应当可在任何环境中重复通过。
    • 自足验证(Self-Validating)

      • 测试应该有布尔值输出。
    • 及时(Timely)

      • 测试应及时编写。

10. 类

  • 类的组织

    • 出现变量的顺序列表:公共静态常量 ----> 私有静态变量 ----> 私有实体变量 ----> 公共变量(较少) ----> 公共函数 ----> 被某个公共函数调用的私有工具函数
  • 类应该短小

    • 关于类的第一条规则就是类应该短小。
    • 对于函数,通过计算代码行数衡量大小。对于类,通过计算权责大小的方式来区分是否短小。

    • 单一权责原则(SRP)

      • 类或者模块应该有且仅有一条加以修改的理由。

      • 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成预期的系统行为。

    • 内聚

      • 类应该只有少量实体变量。
    • 保持内聚可以得到许多短小的类

  • 为了修改而组织

    • 在整洁的系统中,对类加以组织,以降低修改的风险。

    • 开放-闭合原则(OCP):类应当对扩展开放,对修改封闭。

    • 在理想系统中,可以通过扩展系统而非修改现有代码来添加新特性。

    • 由于需求在改变,对应的代码也会改变。通过降低连接度,对类有遵循另一条类设计原则:依赖倒置原则(Dependency Inversion Principle, DIP)

      • DIP认为类应当依赖于抽象而不是依赖于具体细节。

11. 系统

  • 将系统的构造与使用分开

    • 分解main

      • 将全部构造过程搬迁到main或称之为main的模块中,在设计系统的其余部分时,假定所有的对象都已正确构造和设置。
    • 工厂

      • 使用工厂模式将构造过程封装起来,将构造的细节隔离。
    • 通过依赖注入(Dependency Injection, DI),控制反转(Inversion Of Control, IoC)的方式实现分离构造与使用。

  • 测试驱动系统架构

    • 对于架构没必要先做大设计(Big Design Up Front,BDUF)。因为一旦构造过程开始,就不可能对其结构不做根本性改动。

    • 可以从 “简单自然” 但切分良好的架构开始做软件项目,快速交付可工作的用户故事。随着业务规模的增长而添加更多基础架构。

  • 优化决策

    • 模块化和关注面切分成就了分散化管理和决策。
    • 在巨大的系统中,不管是一座城市或一个软件项目,无人能做所有决策。
    • 延迟决策至最后一刻,这样可以让决策者基于最有可能的信息而作出选择。
  • 明智使用添加了可论证价值的标准

    • 通过特定的标准,封装组件连接,从而保证真实需求的交付相结合。
  • 系统需要领域特定语言

    • 领域特定语言(Domain-Specific Language, DSL):是一种单独的小型脚本语言或以标准语言写的API。
    • DSL在有效使用时能提升代码惯用法和设计模式之上的抽象层次。允许开发者在恰当的抽象层级上直指代码的初衷。

    • DSL允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO来表达。

12. 迭代

  • 通过迭代设计达到整洁目的

    • Kent Beck关于简单设计的四条规则:
      • 运行所有测试
      • 不可重复
      • 表达了程序员的意图
      • 尽可能减少类和方法数量(优先级最低,其余三条最重要)
  • 简单设计规则1:运行所有测试

    • 确保系统完全测试可帮助我们创建更好的设计。
    • 编写测试引致更好的设计。
  • 简单设计规则2~4:重构

    • 测试能保持代码和类的整洁。
    • 测试消除了对清理代码就会破坏代码的恐惧。
    • 消除重复,保证表达力,尽可能减少类和方法的数量。
  • 不可重复

  • 表达力

    • 良好的命名规范,让代码尽可能清晰地表达其作者的意图,减少缺陷,缩减维护成本。
  • 尽可能少的类和方法

    • 为了保持类和函数短小,可能会早出太多的细小类和方法,所以要尽可能减少。
    • 保持函数和类短小的同时,保持整个系统短小精悍。
    • 优先级最低的一条。

13. 并发编程

13.1 为什么要并发?

  • 并发是一种解耦策略,帮助我们把做什么(目的)和何时(时机)进行分离。

  • 解耦的目的与时机能明显改善应用程序的吞吐量和结构。

  • 对于并发编程,会存在如下几个误解点:

    • 并发总能改进性能

      • 并发有时可以改进性能,但只在多个线程或处理器之间能分享大量等待时间时管用,其他的时间不会提升性能反而会适得其反。
    • 编写并发程序无需修改设计

      • 并发算法的设计有可能与单线程系统的设计极不相同。目的与时机的解耦往往对系统结构产生巨大影响。

13.2 并发防御原则

  • 单一权责原则

    • SRP认为,方法/类/组件应当只有一个修改的理由。
    • 建议:将并发代码和非并发代码进行分离。
  • 推论:限制数据作用域

    • 两个线程修改共享对象的同一个字段时,可能会相互干扰,导致未预期的行为。

      • 解决方案:采用 synchronized关键字在代码中一块使用共享对象的临界区。
    • 建议:谨记数据封装,严格限制作用域,避免可能被共享的数据的访问。

  • 推论:使用数据副本

    • 避免共享数据的好方法之一:避免共享数据。
    • 通过使用对象副本能避免代码同步执行,则因避免锁而省下的价值有可能补偿得上额外的创建成本和垃圾收集开销。
  • 推论:线程应尽可能独立

    • 让每个线程都在自己的世界存在,不与其它线程共享数据。
    • 建议:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。

13.4 了解执行模型

  • 生产者--消费者模型

  • 读者-作者模型

  • 宴席哲学家

13.5 警惕同步方法之间的依赖

  • 避免使用一个共享对象的多个方法。

13.6 保持同步区域微小

  • 将同步延展到最小临界区范围之外,会增加资源竞争,从而降低执行效率。

  • 建议:尽可能减小同步区域。

13.7 很难编写正确的关闭代码

  • 建议:应尽早考虑关闭问题,尽早令其工作正常。

13.8 测试线程代码

  • 建议:编写有潜力暴露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。

  • 一些测试的建议:

    • 将伪失败看作是可能的线程问题。

      • 建议:不要将系统错误归咎于偶发事件。不要随意忽视偶发事件。
    • 先使非线程代码可工作。

      • 建议:不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可以工作。
    • 先写可插拔的线程代码。

      • 可以保证在不同的配置环境下运行。
    • 编写可调整的线程代码。

      • 目的:允许线程依据吞吐量和系统使用率自我调整。
    • 运行多于处理器数量的线程。

      • 任务交换的频繁,越有可能找到错过临界区或者导致死锁的代码。
    • 在不同平台上运行。

      • 建议:尽早并经常地在所有目标平台上运行线程代码。
    • 调整代码并强迫错误发生。

      • 通过硬编码的方式,增加侦测到缺陷的可能性。
      • 通过手工或者自动化的方式,让代码 “异动”,从而使线程以不同次序执行,从而有效地增加发现错误的机会。