menu

clean-code

《代码整洁之道》

代码猴子与童子军军规

  1. 我们就像一群代码猴子,上蹿下跳,自以为领略了编程的真谛。可惜,当我们抓着几个酸桃子,得意洋洋坐到树枝上,却对自己造成的混乱熟视无睹。那堆“可以运行”的乱麻程序,就在我们的眼皮底下慢慢腐坏。

第一章 整洁代码

  1. 勒布朗法则:稍后等于永不(Later equals never)。
  2. 制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做的快的唯一方法——就是始终尽可能保持代码整洁。
  3. Javadoc 中的 @author 字段告诉我们自己是什么人。我们是作者,作者都有读者。实际上,作者有责任与读者做良好沟通。下次你写代码的时候,记得自己是作者,要为评判你工作的读者写代码。

第三章 函数

  1. 函数的第一规则是要短小。第二条规则是还要更短小…经过漫长的试错,经验告诉我,函数就该小。
  2. 函数应该做一件事。做好这件事。只做这一件事。判断函数是否不止做了一件事,有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。
  3. 要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中混杂不同的抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
  4. 写出短小的 switch 语句往往很难。写出只做一件事的 switch 语句也很难。我们总无法避开 switch 语句,不过还是能够确保 switch 都埋藏在较低的抽象层级,而且永远不重复。
  5. 最理想的参数数量是零,其次是一,再次是二…从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。
  6. 函数承诺只做一件事,但还是会做其它被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动,导致古怪的时序性耦合及顺序依赖。
public class UserValidator {
    private Cryptographer cryptographer;

    public boolean checkPassword(String userName, String password) {
        User user = UserGateway.findByName(userName);
        if (user != null) {
            String codePhrase = user.getPhraseEncodeByPassword();
            String phrase = cryptographer.decrypt(codePhrase, password);
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true;
            }
        }
        return false;
    }
}

副作用就在于对 Session.initialize() 的调用。checkPassword 函数是用来检查密码的。该名称未暗示它会初始化该次会话。当某个误信了函数名的调用者想要检查用户有效性时,就得冒着抹除现有会话数据的风险。这一副作用造成了一次时序性耦合。也就是说,checkPassword 只能在特定时刻调用。

第四章 注释

  1. 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注释总是一种失败。我们总无法找到不用注释就能表达自我的方法,所以总要有注释,这并不值得庆贺。
  2. 如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。
  3. 有时,有理由用 // TODO 形式在源代码中放置要做的工作列表。TODO 是一种程序员认为应该做,但由于某些原因目前还没做的工作。
  4. 没有什么比被良好描述的公共 API 更有用和令人满意的了。如果你在编写公共 API,就该为它编写良好的 Javadoc。
  5. 删掉无用而多余的 Javadoc 吧,这些注释只是一味将代码搞得含糊不明,完全没有文档上的价值。
  6. 所谓每个函数都要有 Javadoc 或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释徒然让代码变得散乱,满口胡言,令人迷惑不解。
  7. 20 世纪 60 年代,曾经有那么一段时间,注释掉的代码可能有用。但我们已经拥有优良的源代码控制系统如此之久,这些系统可以为我们记住不要的代码。我们无需再用注释来标记,删掉即可,它们丢不了,我担保。

第五章 格式

  1. 代码格式很重要,必须严肃对待。代码格式关乎沟通,而沟通是专业开发者的头等大事。
  2. 你今天编写的功能,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码不复存在,你的风格和律条仍会存活下来。
  3. 若某个函数调用了另外一个,就应该把它们放在一起,而且调用者应该尽可能放在被调用者上面。

第六章 对象和数据结构

  1. 最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或 DTO(Data Transfer Objects)。DTO 是非常有用的结构,尤其是在于数据库通信、或解析套接字传输的消息之类的场景中。

第七章 使用异常而非返回码

  1. 在很久以前,许多语言都不支持异常。这些语言处理和汇报错误的手段都有限。你要么设置一个错误标识,要么返回给调用者检查的错误码。这类手段的问题在于,它们搞乱了调用者代码。调用者必须在调用之后即可检查错误。不幸的是,这个步骤很容易被遗忘。最好是抛出一个异常,这样其逻辑不会被错误处理搞乱。
  2. 使用不可控异常。可控异常 checked exception 的代价是违反开闭原则。如果你在方法中抛出可控异常,而 catch 语句在三个层级之上,你就得在 catch 语句和抛出异常处之间的每个方法签名中声明该异常。这意味着对软件中低层级的修改,都将涉及较高层级的签名。最终得到的就是一个从软件最底端贯穿到最高端的修改链。
  3. 别返回 null 值。我不想去计算曾经见过多少每行代码都在检查 null 值的应用程序。Java 中有 Colletions.emptyList() 方法,该方法返回一个预定义不可变列表,这样编码,就能尽量避免 NullPointerException 的出现,代码也就更整洁了。
  4. 别传递 null 值。在大多数编程语言中,没有良好的方法能对付由调用者意外传入 null 值。事已如此,恰当的做法就是禁止传入 null 值。

第八章 边界

  1. 第三方代码帮助我们在更少时间内发布更丰富的功能。在利用第三方程序包时,该从何处入手呢?我们没有测试第三方代码的职责,但为要使用的第三方代码编写测试,可能最符合我们的利益。
  2. 学习第三方代码很难,整合第三方代码也很难,同时做这两件事难上加难。不要在生产代码中试验新东西,而是编写测试来遍览和理解第三方代码,这叫“学习性测试”。

第九章 单元测试

  1. TDD 三定律:
    • 定律一 在编写不能通过的单元测试前,不可编写生产代码。
    • 定律二 只可编写刚好无法通过的单元测试,不能编译也算不通过。
    • 定律三 只可编写刚好足以通过当前失败测试的生产代码。
  2. TDD 三定律其实说的是,先写失败的 Case,写完之后才开始写功能 Code,只要 Code 通过了 Case,就不要再写功能代码了。也就是说,写完一个测试,就要写对应的生产代码。
  3. 测试代码和生产代码一样重要。它可不是二等公民。它需要被思考、被设计和被照料。它该像生产代码一般保持整洁。
  4. 如果测试不能保持整洁,你就会失去它们。没有了测试,你就会失去保证生产代码可扩展的一切要素。有了测试,你就不担心对代码的修改!没有测试,每次修改都可能带来缺陷。
  5. 覆盖了生产代码的自动化单元测试程序组能尽可能地保持设计和架构的整洁。测试带来了一切好处,因为测试使改动变得可能。
  6. 整洁的测试有三个要素:可读性、可读性、可读性。测试应该明确、简洁,还有足够的表达力。在测试中,要以尽量少的文字表达大量的内容。
  7. F.I.R.S.T 规则:
    • Fast(快速) 测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正。
    • Independent(独立) 测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。
    • Repeatable(可重复) 测试应当可在任何环境中重复通过。
    • Self-Validating (自足验证) 测试应该有布尔值输出。
    • Timely(及时) 测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。

第十章 类

  1. 面向对象的其中一个设计原则是“开放——闭合原则”,即类应当对扩展开放,对修改封闭。我们希望将系统打造成在添加或修改特性时尽可能少惹麻烦的架子。在理想系统中,我们通过扩展系统而不是修改现有代码来添加新特性。
  2. 类的另一条设计原则是“依赖倒置原则”(Dependency Inversion Principle, DIP),DIP 认为类应该依赖于抽象而不是依赖于具体细节。

第十一章 系统

  1. 有一种强大的机制可以实现分离构造与使用,那就是依赖注入(Dependency Injection, DI),它是控制反转(Inversion of Control, IoC)在依赖管理中的一种应用手段。控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则。在依赖管理情境中,对象不应负责实体化对自身的依赖,而应当将这份权责移交给其它“有权力”的机制,从而实现控制的反转。
  2. “一开始就做对系统”纯属神话。反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事。这就是迭代和增量敏捷的精髓所在。

第十二章 迭进

  1. 简单设计的四条规则,按重要程度排序:
    • 运行所有测试;
    • 不可重复;
    • 表达了程序员的意图;
    • 尽可能减少类和方法的数量。
  2. 全面测试并持续通过所有测试的系统,就是可测试的系统。看似浅显,但却重要。不可测试的系统同样不可验证。不可验证的系统,绝不应该部署。
  3. 重复是拥有良好设计系统的大敌,它代表着额外的工作、额外的风险和额外且不必要的复杂度。要想创建整洁的系统,需要有消除重复的意愿。
  4. 软件项目的主要成本在于长期维护。代码应当清晰地表达其作者的意图。作者把代码写得越清晰,其他人花在理解代码上的时间也就越少,从而减少缺陷,缩减维护成本。
  5. 为了保持类和函数短小,我们可能会造出太多的细小类和方法。所以这条规则也主张函数和类的数量要少。我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍。不过更重要的是测试、消除重复和表达力。

第十三章 并发编程

  1. 并发是一种解耦策略。它帮助我们把做什么(目的)和何时做(时机)分解开。解耦目的与时机能明显地改进应用程序的吞吐量和结构。
  2. 并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用,事情没那么简单。
  3. 并发算法的设计有可能与单线程系统的设计极不相同。目的与时机的解耦往往对系统结构产生巨大影响。
  4. 并发编程中的一些基础定义:
    • 限定资源:并发环境中有着固定尺寸或数量的资源。
    • 互斥:每一时刻仅有一个线程能访问共享数据或共享资源。
    • 线程饥饿:一个或一组线程在很长时间内或永久被禁止。例如,总是让执行得快的线程先运行,加入执行得快得线程没完没了,则执行时间长的线程就会“饥饿”。
    • 死锁:两个或多个线程互相等待执行结束。每个线程都拥有其它线程需要的资源,得不到其它线程拥有的资源,就无法终止。
    • 活锁:执行次序一致的线程,每个都想要起步,但发现其它线程已经“在路上”。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动。

第十四章 逐步改进

  1. 代码能工作还不够,能工作的代码经常会严重崩溃。满足于仅仅让代码工作的程序员不够专业。他们会害怕没时间改进代码的结构和设计,我不敢苟同。没什么比糟糕的代码给开发项目带来更深远和长期的损害了。
  2. 进度可以重订,需求可以重新定义,团队动态可以修正。糟糕的代码只会一直腐败发酵,无情地拖着团队的后腿。
  3. 保持代码持续整洁和简单,永不让腐坏有机会开始。

第十五章 JUnit 框架

  1. 成员变量的前缀可以删除。在现今的运行环境中,这类范围性编码纯属多余。
  2. 条件判断应当封装起来,从而更清晰地表达代码的意图。可以拆解处一个方法,解释这个条件判断。
public String compact(String message) {
    if (expected == null || actual == null || areStringsEqual()) {
        return Assert.format(message, expected, actual);
    }
}

// 拆解后...
public String compact(String message) {
    if (shouldNotCompact()) {
        return Assert.format(message, expected, actual);
    }
}

private boolean shouldNotCompact() {
    return expected == null || actual == null || areStringsEqual();
}

第十七章 味道与启发

  1. 让注释传达本该更好地在源代码控制系统、问题追踪系统或任何其它记录系统中保存的信息,是不恰当的。
  2. 除函数签名之外什么也没说的 Javadoc,也是多余的。
  3. 看到注释掉的代码,就删除它!别担心,源代码控制系统还会记得它。
  4. 每次看到重复代码,都代表遗漏了抽象。将重复代码叠放进类似的抽象,增加了你的设计语言的词汇量。其它程序员可以用到你创建的抽象设施。编码变得越来越快,错误越来越少,因为你提升了抽象层级。
  5. 死代码就是不执行的代码,可以在检查不会发生的条件的 if 语句中找到,可以在从不抛出异常的 try/catch 块中找到,可以在从不调用的小工具方法中找到,也可以在不会发生 switch/case 条件中找到。如果你找到死代码,就体面地埋葬它,将它从系统中删除掉。
  6. 特性依恋是 Martin Fowler 提出的代码味道之一。类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其它类中的变量和函数。我们要消除特性依恋。
  7. 用多态替代 if/else 或 switch/case。对于给定的选择类型,不应有多于一个 switch 语句。在那个 switch 语句中的多个 case,必须创建多态对象,取代系统中其它类似 switch 语句。
  8. 用命名常量替代魔术数。
  9. 现在 enum 已经加入 java 语言了,放心用吧!别再用那个 public static final int 老花招。那样做 int 的意义就丧失了,而用 enum 则不然,因为它们隶属于有名称的枚举。

评论:


技术文章推送

手机、电脑实用软件分享

微信搜索公众号: AndrewYG的算法世界
wechat 微信公众号:AndrewYG的算法世界