Day 08: 【结语】程序码的气味和启发

https://ithelp.ithome.com.tw/upload/images/20210923/20138643Xzv7RnxUF1.png

「这个手环就像是为我的职业道德做出了公开声明。它是一个明显的指示,代表我承诺 『我将尽己所能把程序写到最好』。所以它仍在我的手腕上,当我写程序时,不断提醒着我对自己曾经做出过,撰写 Clean Code 的承诺」

取自: Clean Code (p.449)


  • 关於本书 (Clean Code, aka 无瑕的程序码)
    目前为止,Clean Code 80% 以上的内容都已概括介绍到,其中:
    • CH11: 系统
      省略了後段的 Java Proxies 及 AOP 部分,有兴趣的读者可自行研究
    • CH13: 平行化
      超出笔者能力所及 Orz...,故略过
    • CH.14-16: 案例讨论
      针对 Java 某些函式库 (JUnit, SerialDate...) 进行重构的案例探讨,算是选读章节

CH17: 程序码的气味和启发 (Code Smells & Inspiration)

  • 这一节是一套有关软件纪律的价值体系统整 (Clean 学派?)
  • 有兴趣深入了解的读者可自行购买 Uncle Bob 另一本专书: Refactoring

注解 (Comments)

  • C1: 不适当的资讯 (Inappropriate Information)
    注解应该保留给技术性纪录。某些像作者、修改纪录、效能报告之类的诠释资讯不该出现在注解中
  • C2: 废弃的注解 (Obsolete Comment)
    当注解和原先描述的程序越来越远时,代表注解过时了,尽快更新或移除它,避免不相关和误导
  • C3: 多余的注解 (Redundant Comment)
    如果程序码已经拿表达意图,那麽这段注解就是多余的
  • C4: 写得不好的注解(Poorly Written Comment)
    不要有废话,要简洁有力
  • C5: 被注解掉的程序码 (Commented-Out Code)
    这是非常令人厌恶的事物,会令人抓狂!

开发环境 (Environment)

  • E1: 需要多个步骤以建立专案或系统 (Build Requires More Than One Step)
    你应该要能利用一个简单的指令就可以取出整个程序系统,再下一个指令就能建好专案
  • E2: 需要多个步骤以进行测试 (Tests Require More Than One Step)
    同上,应该要一个指令就能快速简单地执行所有的单元测试

函式 (Functions)

  • F1: 过多的参数 (Too Many Arguments)
    没有参数是最棒的,其次是 1, 2, 3 个,尽量不要超过 3 个
  • F2: 输出型参数 (Output Arguments)
    这是不直觉的,通常参数是输入用的
  • F3: 旗标参数 (Flag Arguments)
    Boolean 参数违反了「只做一件事」原则,会造成困惑,应该移除
  • F4: 被遗弃的函式 (Dead Function)
    移除不再被呼叫的函式。你的版本控制系统会记得这些函式

一般状况 (General)

  • G1: 同份原始档存在多种语言 (Multiple Languages in One Source File)
    尽量让同份原始档使用的语言数量减少
  • G2: 明显该有的行为未被实现 (Obvious Behaviour is Unimplemented)
    最少惊奇原则 (The Principle of Least Surprise):读者要能从函式名称直觉地了解函式本意
  • G3: 在边界上的不正确行为 (Incorrect Behavior at the Boundaries)
    察看所有的边界条件,并替它们写测试
  • G4: 无视安全规范 (Overridden Safeties)
    不要忽视编译器警告或未通过的测试
  • G5: 重复的程序码 (Duplication)
    本书最重要的规范之一,Don't Repeat Yourself (DRY)
  • G6: 在错误抽象层次上的程序码 (Code at Wrong Level of Abstraction)
    划分「高层次一般概念」和「低层次细节概念」 (e.g., 所有的高层概念放在基底类别,低层概念放在衍生类别)
  • G7: 基底类别相依於其衍生类别 (Base Classes Depending on Their Derivatives)
    基底类别不应该知道任何有关衍生类别的资讯 (例外: 有限状态机实作)。甚至可以再把基底和衍生类别拆到不同档案里
  • G8: 过多的资讯 (Too Much Information)
    优质的软件开发者会限制类别和模组中暴露的介面数量。类别拥有的方法和实体变数愈少愈好、函式知道的变数愈少愈好
  • G9: 被遗弃的程序码 (Dead Code)
    请移除不会被执行的程序码
  • G10: 垂直分隔 (Vertical Separation)
    变数和函式定义应该要很靠近被使用的地方
  • G11: 不一致性 (Inconsistency)
    遵守惯例 (例: 维持变数命名的一致性)
  • G12: 杂乱的程序 (Clutter)
    没有任何实作的预设建构子、不被使用的变数、函式、没有资讯的注解...等,都该被移除
  • G13: 人为的耦合 (Artificial Coupling)
    不要让「没有直接目的」的两个模组耦合、不要将变数、常数或函式放置在一个临时方便但不洽当的地方
  • G14: 特色留恋 (Feature Envy)
    类别的方法应该只对同一类别里的变数和函式感兴趣,而不是留恋其他类别里的资讯 (e.g., 使用「别的物件」的 setter 和 getter)。这是强烈程序码气味之一,
  • G15: 选择型参数 (Selector Arguments)
    没有什麽比函式呼叫挂上一个 True / False 参数要来的令人厌恶 (e.g., calculateWeeklyPay(false))
  • G16: 模糊的意图 (Obscured Intent)
    程序应尽可能地具有表达力,避免魔术数字 (Magic Number)、跨行的表达式、匈牙利命名法...等
    // (X) 程序码不是愈短愈好!
    public int m_otCalc() {
      return iThsWkd * iThsRte +
        (int) Math.round(0.5 * iThsRte *
          Math.max(0, iThsWkd - 400)
      );
    }
    
  • G17: 错置的职责 (Misplaced Responsibility)
    程序码应该被放在一个读者自然而然会认为它该存在的地方
  • G18: 不适当的静态宣告 (Inappropriate Static)
    少用静态方法。如果真的想要一个静态函式,先确认你不可能想让函式有多型行为
  • G19: 使用具解释性的变数 (Use Explanatory Variables)
    让程序可读性提升的最有效方式之一,是将计算过程拆解成许多富有意义名称的暂存变数
  • G20: 函式名称要说到做到 (Function Names Should Say What They Do)
    如果你必须看到函式的实现内容或文件,才能了解这个函式在做什麽,那你应该替函式换一个更好的名称
    Date newDate = date.add(5);
    
    // vs.
    
    Date daysLater = date.addDaysTo(5);
    
  • G21: 了解演算法 (Understand the Algorithm)
    只是想办法让函式通过所有的测试还不够,必须知道解决方案为何是正确的
  • G22: 让逻辑相依变成实体相依 (Make Logical Dependencies Physical)
    模组不应该对被相依的模组有任何预先的假设 (逻辑相依),它仅向被相依的模组询问所需的讯息
  • G23: 用多型取代 If/Else 或 Switch/Case (Prefer Polymorphism to If/Else or Switch/Case)
    One Switch 原则: 对於给定的选择型态,不应有超过 1 个以上的 switch 叙述
  • G24: 遵循标准的惯例 (Follow Standard Conventions)
    团队要有一套程序码开发标准规范
  • G25: 用有名称的常数取代魔术数字 (Replace Magic Numbers with Named Constants)
    这是最古老的规范之一,COBOL、FORTRAN 的手册都如此建议。除非是一些能自我解释或很容易被辨识的常数才可省略
    // (X)
    if(page.size() == 55){
      //...
    }
    
    // (O)
    if(page.size() == page.MAX_PAGE_SIZE){
      //...
    }
    
    int dailyPay = hourlyRate * 8;
    double circumference = radius * PI * 2;
    
  • G26: 要精确 (Be Precise)
    e.g., 不要用 Float 来表示货币、能只用 List 就不要宣告成 ArrayList、如果函式可能回传 NULL,就要有检查机制、如果可能出现同步问题、就要确保有使用锁定 (Lock) 机制
  • G27: 结构胜於常规 (Structure over Convention)
    「具强制决策设计特性」的结构胜过「惯例」
  • G28: 封装条件判断 (Encapsulate Conditionals)
    // (O)
    bool shouldBeDeleted = timer.hasExpired() && timer.isRecurrent() == false;
    if(shouldBeDeleted) {
      ...
    }
    
    // (X)
    if(timer.hasExpired() && timer.isRecurrent() == false) {
      ...
    }
    
  • G29: 避免否定的条件判断 (Avoid Negative Conditions)
    if(shouldBeDeleted) {...}
    
    // vs.
    
    if(!shouldNotBeDeleted) {...}
    
  • G30: 函式应该只做一件事 (Functions Should Do One Thing)
    public void paySalary() {
      foreach(Employee e in employees) {
        if(e.isPayDay()) {
          Money pay = e.calculatePay();
          e.deliverPay(pay);
        }
      }
    }
    
    上述函式做了三件事:检视所有职员、检查哪几位职员该获得薪资、付薪资。应当改写成
    public void paySalary() {
      foreach(Employee e in employees){
        payIfNecessary(e);
      }
    }
    
    private void payIfNecessary(Employee e) {
      if(e.isPayDay()) {
        calculateAndDeliverPay(e);
      }
    }
    
    private void calculateAndDeliverPay(Employee e) {
      Money pay = e.calculatePay();
      e.deliverPay(pay);
    }
    
  • G31: 隐藏时序耦合 (Hidden Temporal Couplings)
    当函式的呼叫须依照次序时 (e.g., 先...、再...、才能...),不应该隐藏这些耦合。建立生产线,让每个函式都产生下一个函式需要的结果 (防止呼叫次序被误改)
  • G32: 不要随意 (Don't Be Arbitrary)
    组织程序码应该要有理由,而且要确保理由与程序码结构有关
  • G33: 封装边界条件 (Encapsulate Boundary Conditions)
    将边界条件的处理放置於同一个地方
  • G34: 函式内容只该下降一个抽象层 (Functions Should Descend Only One Level of Abstraction)
    函式里的叙述都要在同样的抽象层次上,这些叙述只能比函式名称低一个抽象层次。划分抽象层次概念,是进行函式重构时,最重要也最难做到的事情之一
  • G35: 可调整的资料应放置於高阶层次 (Keep Configurable Data at High Levels)
    将设定性质的常数放在非常高阶的位置、并向下传递到程序的各个角落
    public static void main(String[] args) { 
      Arguments arguments = parseCLI(args);
      ...
    }
    
  • G36: 避免传递性导览 (Avoid Transitive Navigation)
    确保模组只须了解它们的立即合作者,并不需要了解整个系统的导览图
    a.getB().getC().doSomething();
    
    以上例来说,将来若想在模组 B 和 C 之间安插新的模组 Q 时,必须找出所有的 "a.getB().getC()" 叙述,并安插入 getQ(),这使架构变得生硬,我们不应漫步在系统的物件图里,倒不如让立即的合作者提供所需要的服务
    myCollaborator.doSomething();
    

JAVA

  • J1: 利用万用字元来避免冗长的引入列表 (Avoid Long Import Lists by Using Wildcards)
    宜使用 import package.*; (但要注意,少数情况会遇到名称冲突)
  • J2: 不要继承常数
    不要将常数放在介面里,然後透过继承介面取得使用权。尽量用静态 (Static) 的引入叙述来取代
  • J3: 常数 vs. 列举 (Constants versus Enums)
    多善用 enum,不要用 public static final int 这种古老技巧

命名 (Names)

  • N1: 选择具描述性质的名称 (Choose Descriptive Names)
    命名因素在程序可读性上所占的比率达九成以上。好的名称描述了程序的结构
  • N2: 在适当的抽象层次选择适当的命名 (Choose Names at the Appropriate Level of Abstraction)
    选择能反映出类别或函式抽象层次的名称,不要把不同的层次混在一起
  • N3: 尽可能使用标准命名法 (uSE sTANDARD nOMENCLATURE wHERE pOSSIBLE)
    使用大家熟知的惯例或用法来进行命名 (e.g., ModemDecorator),这样的行为也叫做专案里的「普及语言 (Ubiquitous Language)」
  • N4: 非模棱两可的名称 (Unambiguous Names)
    选择不会让函式或变数意义模棱两可的名称,可使用长命名 (解释性价值更高)
  • N5: 较大范围的视野使用较长的名称 (Use Long Names for Long Scopes)
    变数或函式的视野范围小,可使用较短的命名,反之亦然
  • N6: 避免编码 (Avoid Encodings)
    现今的开发环境已经不必将型态或视野编码到名称里了 (e.g., 不要用匈牙利命名法)
  • N7: 命名应该描述可能的程序副作用 (Names Should Describe Side-Effects)
    名称应该描述所有事情,要从命名看得出程序的副作用 (e.g., getOos() vs. createOrReturnOos())

测试 (Tests)

  • T1: 不足够的测试 (Insufficient Tests)
    测试组应该包含所有可能失败的情况
  • T2: 使用涵盖率工具 (Use a Coverage Tool)
    工具会告诉你,那些地方并未被测试到
  • T3: 不要跳过简单的测试 (Don't Skip Trivial Tests)
    简单的测试很容易撰写,且测试文件是有价值的
  • T4: 被忽略的测试是对模棱两可的疑问 (An Ignored Test Is a Question about an Ambiguity)
    因为程序的需求不够明确,所以无法确定某个行为的细节
  • T5: 测试边界条件 (Test Boundary Conditions)
    特别注意测试边界条件
  • T6: 在程序错误附近进行详尽的测试 (Exhaustively Test Near Bugs)
    程序 Bugs 往往会聚集,当找到某个程序错误,最好对该函式做详尽的测试
  • T7: 失败的模式是某种启示 (Patterns of Failure Are Revealing)
    利用测试程序失败的模式来诊断程序问题 (e.g., 所有比 5 个字元长的测试都失败了?)
  • T8: 测试涵盖率模式可以是一种启示 (Test Coverage Patterns Can Be Revealing)
    查看在测试时执行过或未被执行过的程序码,有助了解测试为什麽失败
  • T9: 测试要够快速 (Tests Should Be Fast)
    当行程很赶时,缓慢的测试会被移除,所以要保持测试够快

感谢所有追踪到这边的各位读者们,您的支持与订阅是我创作的最大动力。希望各位开发者们都能够在未来持续发挥专业精神,撰写 Clean Code (无瑕的程序码),如同 Martin Flower 所承诺:「我将尽己所能把程序写到最好」


<<:  [Day 9] 漂亮的输入框 TextField 文本框

>>:  Day09:09 - User服务(4) - 前端 - JWT token、修改个人资料

[重构倒数第21天] - 五种重构Vue2专案的时候最常看到需要被改善的code

前言 该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系...

第 59 天 - 学会除了 --help 跟 -h 外,使用 man 查询指令使用方式

操作画面 : 主要 man 可以完全滑鼠,像读书一样分章节阅读 另外今天发现 forloop 1 到...

D29 - 「来互相伤害啊!」:天时地利

互殴之前当然要先有场地才行,让我们建立 Phaser 场景吧! 建立场景 首先建立 src\comp...

【Day20】SPI的实现

上一篇我们设计了 SPI 的状态机,那麽我们今天要来引用 SPI 状态机模块来实现整个 SPI 的模...

[Day_7]资料储存容器 (1) - tuple

Python的资料储存容器, 可以分为tuple、串列(list)、字典(dict)与集合(set)...