本文最近一次更新于 6 年 7 个月前,其中的内容很可能已经有所发展或是发生改变。
相对于任何宏伟愿景,对细节的关注甚至是更为关键的专业性基础。首先,开发者通过小型实践获得可用于大型实践的技能和信用度。其次,宏大建筑中最细小的部分,比如关不紧的门、有点儿没铺平的地板,甚至是凌乱的桌面,都会将整个大局的魅力毁灭殆尽。这就是整洁代码之所系。
本书「序」中的这段话完美的诠释了作者写本书的意义。(简评在最后)
序
- 神在细节之中。
- 5S 哲学包括以下概念:
- 整理(Seiri)
- 整顿(Seiton)
- 清楚(Seiso)
- 清洁(Seiketsu)
- 身美(Shitsuke)
整洁代码
有人也许以为,关于代码的书有点落后于时代——代码不再是问题:我们应当关注模型和需求。……扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些层面上,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器可以执行的细节程度,就是编程要做的事。而这种规约正是代码。
勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。
多数人都知道一幅画是好还是坏。但能分辨优劣并不表示懂得绘画。能分辨整洁代码和肮脏代码,也不意味着会写整洁代码!
Bjarne Stroustrup(C++ 语言发明者):我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,高处一堆混乱来,整洁的代码只做好一件事。
Grady Booch(《面向对象分析与设计》作者):整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。
Ron Jeffries(《极限编程实施》作者):简单代码,依其重要顺序:
- 能通过所有测试;
- 没有重复代码;
- 体现系统中的全部设计理念;
- 包括尽量少的实体,比如类、方法、函数等。
Ward Cunningham(Wiki 发明者):如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
光把代码写好可不够。必须时时保持代码整洁。
有意义的命名
名副其实:变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在,它做什么事,应该怎么用。
避免误导:应当避免使用与本意相悖的词。别用 accountList 来指称一组账号,除非它真的是 List 类型。用 accountGroup 或 bunchOfAccounts,甚至 accounts 都会好一些。
做有意义的区分:以数字系列命名(a1、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息;没有提供导向作者意图的线索。
public static void copyChars(char a1[], char a2[]) { for (int i = 0; i < a1.length; i++) { a2[i] = a1[i]; } }
如果参数名改为 source 和 destination,这个函数就会像样许多。
使用读得出来的名称:
private Date genymdhms; // 生成日期,年、月、日、时、分、秒 private Date generationTimestamp;
使用可搜索的名称:窃以为单字母名称仅用于短方法中的本地变量。名称长短应于其作用域大小相对应。
避免思维映射:不应当让读者在脑中把你的名称翻译为他们熟知的名称。
类名:类名和对象名应该是名词或名词短语,如 Customer、WikiPage。避免使用 Manager、Data 这样的类名。
方法名:方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。
每个概念对应一个词:给每个抽象概念选一个词,并且一以贯之。
别用双关语:避免将同一单词用于不同目的。
函数
函数的第一规则是要短小。第二条规则是还要更短小。
函数应该做一件事。做好这件事。只做这一件事。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
函数要么做什么事,要么回答什么事,但二者不可兼得。函数应该修改某对象的状态,或是返回该对象的有关信息。
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。
注释
- Brian W. Kernighan 与 P. J. Plaugher:别给糟糕的代码加注释——重新写吧。
- 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注意,我用了「失败」一词。我是说真的。注释总是一种失败。我为什么要极力遍地注释?因为注释会撒谎。
- 好注释:
- 法律信息
- 提供信息的注释
- 对意图的解释
- 阐释
- 警示
- TODO
格式
- 你今天编写的功能,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。
- 垂直格式:
- 关系密切的概念应该相互靠近。
- 变量声明应尽可能靠近其使用位置。
- 实体变量应该在类的顶部声明。
- 若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽肯能放在被调用者上面。
- 横向格式:
- 应该尽量保持代码行短小。死守 80 个字符上限有点僵化,至多在 100 或 120 个字符。
- 赋值操作符周围加上空格字符,以此加上强调目的。
- 不在函数名和左圆括号之间加空格。
- 源文件是一种继承结构,而不是一种大纲结构。类中的方法相对该类缩进一个层级。方法的实现相对方法声明缩进一个层级。
对象和数据结构
过程式代码便于在不该动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
得墨忒耳率认为,类 C 的方法 f 只应该调用以下对象的方法:
- C
- 由 f 创建的对象;
- 作为参数传递给 f 的对象;
- 由 C 的实体变量持有的对象。
方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。
对象曝露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。数据结构曝露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向既有函数添加新数据结构。
错误处理
- 错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。
- 在某种意义上,try 代码块就像是事务。catch 代码块将程序维持在一种状态,无论 try 代码块中发生了什么均如此,所以,在编写可能抛出异常的代码时,最好先写出 try-catch-finally 语句。
- 你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。
- 对异常可以依据其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?不过,当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。
- 返回 null 值,基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没检查 null 值,应用程序就会失控。在方法中返回 null 值是糟糕的做法,但将 null 值传递给其他方法就更糟糕了。
边界
- 学习性测试(learning tests):不要在生产代码中试验新东西,而是编写测试来遍览和理解第三方代码
- 使用尚不存在的代码,将尚未开发完毕的 API 从中隔离出来。自己通过使用符合应用程序的接口,一旦 API 被定义出来,再将二者对接。
- 通过代码中少数几处引用第三方边界接口的位置来管理第三方边界。
单元测试
- TDD 三定律:
- 在编写不能通过的单元测试前,不可编写生产代码。
- 只可编写刚好无法通过的单元测试,不能编译也不算通过。
- 只可编写刚好足以通过当前失败测试的生产代码。
- 脏测试等同于——如果不是坏于的话——没测试。
- 测试代码和生产代码一样重要。它可不是二等公民。它需要被思考、被设计和被照料。它该像生产代码一般保持整洁。
- 整洁的测试有什么要素?有三个要素:可读性、可读性和可读性。
- 整洁的测试遵循以下 5 条规则:
- 快速(Fast):测试应该够快。
- 独立(Independent):测试应该相互独立。
- 可重复(Repeatable):测试应当可在任何环境中重复通过。
- 自足验证(Self-Validating):测试应该有布尔值输出。
- 及时(Timely):测试应及时编写。
类
- 类的第一条规则是类应该短小。第二条规则是还要更短小。
- 单一权责原则(SRP)认为,类或模块应有且只有一条加以修改的理由。
- 系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
- 通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。
- 开放-闭合原则(OCP):类应当对扩展开放,对修改封闭。
- 依赖倒置原则(DIP):类应当依赖于抽象而不是依赖于具体细节。
系统
- 软件系统应将启始过程和启始过程之后的运行时逻辑分离开,在启始过程中构建应用对象,也会存在相互缠结的以来关系。
- 可以使用抽象工厂模式让应用自行控制何时创建对象,但构造的细节却隔离于应用程序代码之外。
- 依赖注入(Dependency Injection):对象不应负责实体化对自身的依赖。反之,它应当将这份权移交给其他「有权力」的机制,从而实现控制的反转。
- 我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事。这就是迭代和增量敏捷的精髓所在。
- 面向方面编程(aspect-oriented):被称为方面的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。
迭进
- 简单设计规则 1:运行所有测试:遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近 OO 低耦合度、高内聚度的目标。
- 简单设计规则 2:重构:在重构过程中,可以应用有关优秀软件设计的一切知识。提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称等。
- 不可重复:「小规模复用」可大量降低系统复杂性。
- 表达力:做到有表达力的最重要方式却是尝试。
- 尽可能少的类和方法。
并发编程
名词 | 基础定义 |
---|---|
限定资源 | 并发环境中有着固定尺寸或数量的资源。例如数据库连接和固定尺寸读/写缓存等 |
互斥 | 每一时刻仅有一个线程能访问共享数据或共享资源 |
线程饥饿 | 一个或一组线程互相等待执行结束。 |
死锁 | 两个或多个线程互相等待执行结束。 |
活锁 | 执行次序一致的线程,每个都想要起步,但发现其他线程已经「在路上」。 |
对象是过程的抽象。线程是调度的抽象。
并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。
并发软件的中肯说法:
- 并发会在性能和编写额外代码上增加一些开销;
- 正确的并发是复杂的,即便对于简单的问题也是如此;
- 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待;
- 并发常常需要对设计策略的根本性修改。
生产者-消费者模型:一个或多个生产者线程创建某些工作,并置于缓存或者队列中。一个或者多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
读者-作者模型:当存在一个主要为读者线程提供信息源,但只是偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的积累。协调读者线程不去读取正在更新的信息,而作者线程倾向于长期锁定读者线程。
宴席哲学家:许多企业级应用中会存在进程竞争资源的情形,如果没有用心设计,这种竞争会遭遇死锁,活锁,吞吐量和效率低等问题。
本书后几章主要侧重于讲解 Java 代码的一些例子,对其它语言帮助不大,在这里就不做整理了。
正如我在上一篇读书笔记中所说的:每一本中都会充斥着许多作者的自己的观点、看法,而唯有价值观相符合或相接近的人才会觉得本书写得很不错,上一本《黑客与画家》是,这本《代码整洁之道》也是,你可能很难认为变量的命名需要有那么考究,函数的长短有那么重要,心里想着程序能运行就没事,甚至连 WARNING 都忽视掉,这类人想必并不是本书的目标群体。而本书的目标群体在开头已经注明了:你想成为一个更好的程序员。其实我觉得目标群体还可以加上一小撮人:有强迫症的程序员——比如我。
我曾经看自己四个月前的代码能羞愧得钻进地里,心想着怎么能写出这么烂的代码。这四个月固然有我对该门语言较高层级的数据结构更加熟悉,能更熟练的操作它们,但更多的是编程观念的改变:需要用心来写代码,不要简单敷衍了事,不要认为程序只要能运行就算成功。程序毕竟还是写给人看的,就算不是为了别人,看着意义明确的变量,缩进优美的段落,结构分明的函数,想必自己心里也会很舒畅的。