【Code】《代码整洁之道》笔记-Chapter17-味道与启发

第17章 味道与启发

Martin Fowler在其大作《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)中指出了许多不同的“代码的坏味道”。下面的清单包括很多Martin指出的“坏味道”,还添加了更多我自己提出的“坏味道”,也包括我借以历练本业的其他珍宝与启发。

我借由遍览和重构几个不同的程序总结出这个清单。每次修改,我都问自己为什么要这样改,把修改的原因写下来,结果就得到相当长的清单,该清单列出了在读代码时让我闻起来不舒服的味道。

清单应按顺序阅读,并作为一种参考来使用。

17.1 注释

C1:不恰当的信息

让注释传达本该更好地在源代码控制系统、问题追踪系统或任何其他记录系统中保存的信息,是不恰当的。例如,修改历史记录只会用大量过时而无趣的文本搞乱源代码文件。通常,作者、最后修改时间、SPR数等元数据不该在注释中出现。注释只应该描述有关代码和设计的技术性信息。

C2:废弃的注释

过时、无关或不正确的注释就是废弃的注释。注释会很快过时。最好别编写将被废弃的注释。如果发现废弃的注释,最好尽快更新或删除。废弃的注释会远离它们曾经描述的代码,变成代码中无关和误导阅读者的浮岛。

C3:冗余注释

如果注释描述的是某种充分自我描述了的东西,那么注释就是多余的。例如:

i++; // increment i

另一个例子是除函数签名之外什么也没多说(或少说)的Javadoc:

/**
 * @param sellRequest
 * @return
 * @throws ManagedComponentException
 */
public SellResponse beginSellItem(SellRequest sellRequest) 
  throws ManagedComponentException

注释应该谈及代码自身没提到的东西。

C4:糟糕的注释

值得编写的注释,也值得好好写。如果要编写一条注释,就花时间保证写出最好的注释,字斟句酌,使用正确的语法和拼写,别闲扯,别画蛇添足,要保持简洁。

C5:注释掉的代码

看到被注释掉的代码会令我抓狂。谁知道它有多旧?谁知道它有没有意义?没人会删除它,因为大家都假设别人需要它或是有进一步计划。

那样的代码就这样腐烂掉,随着时间推移,它与系统越来越没关系。它调用不复存在的函数,它使用已改名的变量,它遵循已被废弃的约定,它污染了所属的模块,分散了想要读它的人的注意力。注释掉的代码纯属厌物。

看到注释掉的代码,就删除它!别担心,源代码控制系统还会记得它。如果有人真的需要,可以签出较旧的版本。别被它搞得死去活来。

17.2 环境

E1:需要多步才能实现的构建

构建系统应该是单步的小操作。不应该从源代码控制系统中一小点一小点签出代码。不应该需要一系列神秘指令或环境依赖脚本来构建单个元素。不应该四处寻找额外的小JAR、XML文件和其他系统所需的杂物。你应当能够用单个命令签出系统,并用单个指令构建它。

svn get mySystem 
cd mySystem
ant all
E2:需要多步才能做到的测试

你应当能够发出单个指令就可以运行全部单元测试。能够运行全部测试是如此基础和重要,应该快速、轻易和直截了当地做到。

17.3 函数

F1:过多的参数

函数的参数量应该少。没参数最好,一个次之,两个、三个再次之。三个以上的参数非常值得质疑,应坚决避免。(参见3.6节。)

F2:输出参数

输出参数违反直觉,因为读者期望参数用于输入而非输出。如果函数非要修改什么东西的状态,就修改它所在对象的状态好了。(参见3.7节。)

F3:标识参数

布尔值参数大声宣告函数做了不止一件事。它们令人迷惑,应该被消灭掉。(参见3.6.2节。)

F4:死函数

永不被调用的函数应该被丢弃。保留死函数纯属浪费,别害怕删除死函数,记住,源代码控制系统还会记得它。

17.4 一般性问题

G1:一个源文件中存在多种语言

当今的现代编程环境允许在单个源文件中存在多种不同语言。例如,Java源文件可能还包括XML、HTML、YAML、Javadoc、英文、JavaScript等语言。再如,JSP文件可能还包括HTML、Java、标签库语法、英文注释、Javadoc、XML、JavaScript等。往好处说是令人迷惑,往坏处说就是粗心大意、驳杂不精。

理想的源文件包括且只包括一种语言。现实中,我们可能会不得不使用多于一种语言,但应该尽力缩小源文件中额外语言的数量和范围。

G2:明显的行为未被实现

遵循“最小惊异原则”,函数或类应该实现其他程序员有理由期待的行为。例如,考虑一个将日期名称翻译为表示该日期的枚举的函数。

Day day = DayDate.StringToDay(String dayName);

我们期望字符串Monday翻译为Day.MONDAY,也期望常用缩写形式能被翻译出来,还期望函数忽略大小写。

如果明显的行为未被实现,读者和用户就不能再依靠他们对函数名称的直觉。他们不再信任原作者,不得不阅读代码细节。

G3:不正确的边界行为

代码应该有正确行为,这话看似明白。问题是我们很难明白正确行为有多复杂。开发者常常写出他们以为能工作的函数,信赖自己的直觉,而不是努力去证明代码在所有的角落和边界情形下都真正能工作。

没什么可以替代谨小慎微。每种边界条件、每种极端情形、每个异常都代表了某种可能搞乱优雅而直白的算法的东西。别依赖直觉。追索每种边界条件,并编写测试。

G4:忽视安全

切尔诺贝利核电站崩塌了,因为电厂经理一再忽视安全机制。遵守安全规则就不便于做试验,结果就造成试验未能运行,全世界都目睹了首个民用核电站的大灾难。

忽视安全相当危险。手工控制serialVersionUID可能有必要,但总会有风险。关闭某些编译器警告(或者全部警告!)可能有助于构建成功,但可能会陷于无穷无尽的调试中。关闭失败测试、告诉自己过后再处理,这和假装刷信用卡不用还钱一样存在安全隐患。

G5:重复

有一条本书提到过的最重要的规则之一,你应该非常严肃地对待它,实际上,每位编写有关软件设计的作者都提到过这条规则,Dave Thomas和Andy Hunt称之为DRY原则(Don’t Repeat Yourself,别重复自己)。Kent Beck将它列为极限编程核心原则之一,并称之为“一次,也只一次”。Ron Jeffries将这条规则列在第二位,地位仅次于通过所有测试。

每次看到重复代码,都代表遗漏了抽象。重复的代码可能成为子程序或干脆是另一个类。将重复代码叠放进类似的抽象,增加了你的设计语言的词汇量。其他程序员可以用到你创建的抽象设施。编码变得越来越快,错误越来越少,因为你提升了抽象层级。

重复最明显的形态是你不断看到明显一样的代码,就像是某位程序员疯狂地用鼠标不断复制粘贴代码。可以用单一方法来替代之。

较隐蔽的形态是在不同模块中不断重复出现、检测同一组条件的switch/caseif/else链。可以用多态来替代之。

更隐蔽的形态是采用类似算法但具体代码行不同的模块。这也是一种重复,可以使用模板方法模式或策略模式来修正。

的确,过去15年内出现的多数设计模式都是消除重复的有名手段。Codd范式(Codd Normal Forms)是消除数据库规划中的重复的策略。面向对象自身也是组织模块和消除重复的策略。毫不出奇,结构化编程也是。

重点已经在那里了。尽可能找到并消除重复。

G6:在错误的抽象层级上的代码

创建分离较高层级一般性概念与较低层级细节概念的抽象模型,这很重要。有时,我们创建抽象类来容纳较高层级概念,创建派生类来容纳较低层级概念。这样做的时候,需要确保分离完整。所有较低层级概念放在派生类中,所有较高层级概念放在基类中。

例如,只与细节实现有关的常量、变量或工具函数不应该在基类中出现,基类应该对这些东西一无所知。

这条规则对于源文件、组件和模块也适用。良好的软件设计要求分离位于不同层级的概念,将它们放到不同容器中。这些容器有时是基类或派生类,有时是源文件、模块或组件。无论哪种情况,分离都要完整。较低层级概念和较高层级概念不应混杂在一起。看看下面的代码:

public interface Stack {
  Object pop() throws EmptyException;
  void push(Object o) throws FullException;
  double percentFull();
  class EmptyException extends Exception {}
  class FullException extends Exception {}
}

函数percentFull位于错误的抽象层级。尽管存在许多在其中“充满”(fullness)概念有意义的Stack的实现,但也有其他不能知道自己有多满的实现存在。所以,最好将该函数放在类似于BoundedStack之类的派生接口中。

你或许会认为,如果栈无边界,实现可以返回0,但问题是,不存在真的无边界的栈。你不能真的避免在做以下检查时出现OutOfMemoryException异常:

stack.percentFull() < 50.0.

实现返回0的函数可能是在撒谎。

要点是你不能就错误放置的抽象模型撒谎。孤立抽象是软件开发者最难做到的事之一,而且一旦做错就没有快捷的修复手段。

G7:基类依赖派生类

将概念分解到基类和派生类的最普遍的原因是,较高层级基类概念可以不依赖较低层级派生类概念。这样,如果看到基类而提到派生类名称,就可能发现了问题。通常来说,基类对派生类应该一无所知。

当然也有例外。有时,派生类数量严格固定,而基类中拥有在派生类之间选择的代码。在有限状态机的实现中这种情形很多见。然而,在那种情况下,派生类和基类紧密耦合,总是在同一个jar文件中部署。一般情况下,我们会想要把派生类和基类部署到不同的jar文件中。

将派生类和基类部署到不同的jar文件中,确保基类jar文件对派生类jar文件的内容一无所知,我们就能把系统部署为分散和独立的组件。修改了这些组件时,不必重新部署基组件就能部署它们。这意味着修改产生的影响极大地降低了,而维护系统也变得更加简单。

G8:信息过多

设计良好的模块有着非常小的接口,让你事半功倍。设计低劣的模块有着广阔、深入的接口,你不得不事倍功半。设计良好的接口并不提供许多需要依赖的函数,所以耦合度也较低。设计低劣的接口提供大量你必须调用的函数,耦合度较高。

优秀的软件开发人员要学会限制类或模块中暴露的接口数量。类中的方法越少越好,函数知道的变量越少越好,类拥有的实体变量越少越好。

隐藏你的数据,隐藏你的工具函数,隐藏你的常量和你的临时变量。不要创建拥有大量方法或大量实体变量的类,不要为子类创建大量受保护变量和函数。尽力保持接口紧凑。通过限制信息来控制耦合度。

G9:死代码

死代码就是不执行的代码。可以在检查不会发生的条件的if语句体中找到,可以在从不抛出异常的try语句的catch块中找到,可以在从不被调用的小工具方法中找到,也可以在永不会发生的switch/case条件中找到。

死代码的问题是过不久它就会发出“坏味道”。时间越久,味道就越酸臭。这是因为,在设计改变时,死代码不会随之更新。它还能通过编译,但并不会遵循较新的约定或规则。编写它的时候,系统是另一番模样。如果你找到死代码,就体面地埋葬它,将它从系统中删除掉。

G10:垂直分隔

变量和函数应该在靠近被使用的地方定义。本地变量应该正好在其首次被使用的位置上面声明,垂直距离要短。本地变量不该在距离其被使用之处几百行以外的位置声明。

私有函数应该刚好在其首次被使用的位置下面定义。私有函数属于整个类,但我们还是要限制调用和定义之间的垂直距离。找一个私有函数,应该只是从其首次被使用处往下一点儿的位置那么简单。

G11:前后不一致

从一而终。这可以追溯到最小惊异原则。小心选择约定,一旦选中,就小心持续遵循。

如果在特定函数中用名为response的变量来持有HttpServletResponse对象,则在其他用到HttpServletResponse对象的函数中也用同样的变

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值