代码整洁之道 pt2(第4章-第6章)
4 注释
什么也比不上放置良好的注释来的有用。什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的只是更有破坏性。
只有代码能忠实地告诉你它在做的事。那是唯一真正准确的信息来源。所以尽管有时也需要注释,但是也该多花心思尽量减少注释量。
4.1 注释不能美化糟糕的代码
写注释的常见动机之一是糟糕的代码的存在。
带有少量注释的症结而又表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你写出的糟糕的代码的注释,不如花时间清理那堆糟糕的代码。
4.2 用代码来阐述
// Check to see if the employee is eligible for full benefits
if ((employee.flags && HOURLY_FLAG) && (emplee.age > 65))
if (employee.isEligibleFullBenefits())
比较上面两种写法,明显下面一种写法会更规范。很多时候,简单到只需要创建一个描述了与注释所言同一事物的函数即可。
4.3 好注释
有些知识是必需的,也是有利的。不过,唯一真正好的做法是你想办法不去写注释。
4.3.1 法律信息
有时,公司代码规范要求编写与法律相关的注释。例如版权及著作权声明等。
// Copyright (c) 2003,2004,2005 By Object Mentor,....
// ....
这类注释不应是合同或法典,可以注释中指向一份标准许可或其他外部文档,而不必把所有条款都放到注释中。
4.3.2 提供信息的注释
有时,用注释来提供基本信息也有其用处。例如用在解释某个抽象方法的返回值等。
4.3.3 对意图的解释
有时,注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。
4.3.4 阐释
有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也是有用的。但通常,更好的方法是尽量让参数或返回值本身就足够清楚;但如果参数或返回值是某个标准库的一部分,或者你不能修改的代码,帮助阐释其含义的代码就会很有用。
4.3.5 警示
有时,用于竟是其他程序员可能会出现某种后果的注释也是有用的。
4.3.6 TODO注释
有时,有理由用//TODO行驶在源代码中放置要做的工作列表。
TODO是一种程序员认为应该做,但由于某些原因目前还没做的工作,它可能是要提醒删除某个不必要的特性,或是要求他人注意某个问题等,但无论TODO的目的如何,他都不是在系统中留下糟糕代码的借口。
4.3.7 放大
注释可以用来放大某种看来不合理之物的重要性。
4.3.8 javadoc
javadoc是一款描述良好的API。
4.4 坏注释
大多数注释都属此类。通常坏注释都是糟糕的代码的支撑或借口,或者是对错误决策的修正,基本上等于程序员自说自话。
4.4.1 喃喃自语
如果只是因为你觉得应该或者因为过程需要就添加注释,那就是无谓之举。如果决定写注释,就要花必要的时间确保写出最好的注释。
唯有检视系统其他部分的代码,弄清事情原委。任何迫使读者查看或其它模块的注释,都没能与读者沟通好,不值所费。
4.4.2 多余的注释
对于多余的注释,读它并不比读代码更容易,不如代码精确,误导读者接受不精确的信息,而不是正确地理解代码。
4.4.3 误导性注释
有误导性的信息,放在比代码本身更难阅读的注释里面,有可能会影响导致其他阅读者和使用者。
4.4.4 循规式注释
所谓每个函数都要有javadoc或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释突然让代码变得散乱,满口胡言,令人迷惑不解。
4.4.5 日志式注释
日志式注释即有人在每次编辑某块代码时,记录每次修改的日志。这种冗长的记录只会让代码模块变的凌乱不堪,应当全部删除。
4.4.6 废话注释
废话注释会使我们读代码时,不自主的将注意力停留在它们上面,当代码修改之后,这类注释就变作了谎言一堆。用整洁的代码替代废话注释会更好。
4.4.7 可怕的注释
/** The name **/
private String name;
/** The version **/
private String version;
/** The licenceName **/
private String licenceName;
/** The version **/
private String info;
一股脑的复制粘贴不加思考,提供错误的信息的注释就是可怕的注释。
4.4.8 能用函数或变量时就别用注释
// does the module from the global list <mod> depend on the subsystem we are part of?
if (smodule.getDependSubsystem().contains(subSysMod.getSubSystem()))
例如,以上注释可以通过变量改变为无注释版本:
ArrayList moduleDependees = smodule.getDependSubsystem();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
4.4.9 位置标记
// Actions //
把特定函数放置在这种标记栏下,多数时候实属无理,鸡零狗碎,理当删除—特别是尾部无意义的斜杠
如果标记栏不多,他就会显而易见。所以,尽量少用标记栏,只在特别有价值的时候用。如果滥用就会在无背景的噪音中被忽略。
4.4.10 括号后面的注释
在括号后面放置特殊的注释,尽管对于含有深度嵌套的长函数可能有意义,但只会对函数本身的结构带来混乱。
4.4.11 归属与署名
源代码控制系统非常善于记住是谁在何时添加了什么,没必要用署名签名注释搞乱了代码。
4.4.12 注释掉的代码
直接把代码注释掉是讨厌的做法,别这样干。
将代码注释掉只会给后来的人增加复杂、毫无意义的工作量,代码控制系统完全可以帮我们记住不要的代码。
4.4.13 HTML注释
源代码注释中的HTML标记是一种讨厌的东西。
4.4.14 非本地信息
**假如一定要写注释,请确保它描述了离他最近的代码。**别在本地注释的上下文环境中给出系统级的信息。
4.4.15 信息过多
别再注释中添加又取得历史性话题或无关的细节描述。
4.4.16 不明显的联系
注释及其描述的代码之间的来呢西应该显而易见,如果你不嫌麻烦要写注释,至少让读者能看到注释和代码,并且理解注释所谈何物。
4.4.17 函数头
短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释好。
4.4.18 非公共代码中的javadoc
虽然Javadoc对于公共API非常有用,但对于不打算做公共用途的代码就令人厌恶了。
5 格式
应该保持良好的代码格式,应该选用一套管理代码格式的简单规则,然后贯彻这些规则。
团队中应该一致同意采用一套简单的个事规则,所有成员都应该遵循这套规则。
5.1 格式的目的
代码格式很重要。代码格式不可忽略,必须严肃对待。代码格式关乎沟通,而沟通是专业开发者的头等大事。
功能代码随时可能会被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。
原始代码修改之后很久,其代码风格和可读性仍会影响代码的可维护性和可扩展性。
5.2 垂直格式
短文件通常比长文件易于理解。
5.2.1 向报纸学习
源文件应该像报纸一样,名称简单一目了然(名称本身应该足以告诉我们是否在正确的模块中)。源文件最顶部应该给出高层次的概念和算法,细节向下渐次展开,直到找到原文件中最底层的函数和细节。
5.2.2 概念间垂直方向上的区隔
几乎所有代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。
5.2.3 垂直方向上的靠近
空白行隔开了概念,靠近的代码行则暗示了他们之间的紧密关系。
5.2.4 垂直距离
关系密切的概念应该互相靠近。
- 避免使用protected变量,不要把关系模切的概念放到不同的文件中。
- 对于关系密切、放置于同一源文件中的概念,它们之间的区隔应该成为对彼此的易懂度影响有多重要的衡量标准。
- 应该避免迫使读者在源文件和类中跳来跳去。
变量声明—变量声明应尽可能靠近其使用位置。
- 因为函数很短,本地变量应该在函数的顶部出现。
- 循环中的控制变量应该总是在循环语句中声明。
- 在较长函数中,变量也可能在某个代码块顶部,或在循环之前声明。
实体变量—实体变量应该在累的顶部声明。
- 一般不会增加变量的垂直距离,因为在设计良好的类中,他们如果不是被该类的所有方法所用,也会被大多数方法所用。
- 在C++中实体变量一般放在底部,但在Java中,惯例是放在类的顶部。
相关函数—若某个函数调用了另外一个,则应该把他们放到一起,而且调用者应该尽可能放在被调用者上面。
- 概念相关。概念相关的代码应该放在一起。代码的相关性越强,彼此之间的距离就该越短。
5.2.5 垂直顺序
一般而言,我们想自上向下展示函数调用依赖顺序。被调用的函数应该放在执行调用的函数下面。
像报纸文章一般,我们期望最重要的概念先出现,并期望以包括最少细节的方式表述它们,而期望底层细节最后出现。
5.3 横向格式
应该尽力保持代码的行短小。死守80个字符的上线有些僵化,代码宽度可以达到100字符或120字符,再多就不合适了。
5.3.1 水平方向上的区隔与靠近
使用空格将彼此尽力相关的事物连接到一起,也用空格字符把相关性较弱的事物隔离开。
- 在赋值操作符周围加上空格字符,以达到强调的目的。
int lineSize = line.length();
- 不在函数名和左圆括号之间加空格,这是因为函数与其参数密切相关,如果隔开,就会显得互无关系。把函数调用括号中的参数一一隔开,强调逗号,表示参数是互相分离的。
private void measureLine(String line, int lineCount) {
...
}
- 乘法之间不加空格,因为乘法具有较高优先级;加减法运算项之间用空格隔开,因为加法和减法的优先级较低。
private static double determinant(double a, double b, double c) {
return b*b - 4*a*c;
}
5.3.2 水平对齐
Java中的代码水平对齐应该是这样的
public class FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
protected Long requestParsingTimeLimit;
...
}
而非
public class FitNesseExpediter implements ResponseSender {
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
private Response response;
protected Long requestParsingTimeLimit;
...
}
5.3.3 缩进
要想让代码的范围式继承结构课件,需要依据源代码行在继承结构中的位置对代码行做缩进处理。
对文件顶层的语句(例如类声明),根本不缩进。类中的方法相对该类缩进一个层级,方法的实现相对方法声明缩进一个层级。代码块的实现相对于其容器代码块缩进一个层级,以此类推。
//bad code 没有缩进时
public void delete(Page page) {try { deletePageAndAllReferences(page); } catch (Exception e){ logError(e); }}
// 加了缩进后
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e){
logError(e);
}
}
违反缩进规则。对于短小的if语句、while循环或小函数中也不应该违反缩进规则,一旦这么做了节能会出现范围层级坍塌到一行的情况。
// bad code
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e){ logError(e); }
}
5.3.4 空范围
有时,while或for语句的语句体为空,也需要确保空范围体的缩进,并用括号包围起来。
//bad code
while (dis.read(buf, 0, readBufferSize) != -1)
;
5.4 团队规则
- 每个程序员都有自己喜欢的格式规则,但如果在团队中工作,就是团队说了算。
- 一组开发者应当认同一种格式风格,每个成员应该采用大家都认同的那种风格。
- 好的软件系统是由一系列读起来不错的代码文件组成的。它们需要拥有一致和顺畅的风格。
- 绝对不要用各种不同的风格来编写源代码,这样会增加代码的复杂度。
6 对象和数据结构
将一个变量设置为私有(private)有一个理由:不想让别人依赖这些变量,我们还可以自由修改器类型个实现。
但为什么还会给私有变量添加赋值器和取值器,让它们如同公共变量一般呢?
6.1 数据抽象
即便变量是私有的,但通过变量取值器和赋值器使用私有变量,则其实现便被暴露了。
隐藏实现并非只是在变量之间加上一个函数层。隐藏实现关乎抽象!类并不简单地用取值器和赋值器将其变量推向外界,而是暴露抽象接口,以便用户无需了解数据的实现就能够操作数据本体。
以两个抽象方法为例
public interface Vehicle {
double getFuelTankCapacityInGallons();
double getGallonsOfGasoline();
}
public interface Vehicle {
double getPercentFuelRemaining();
}
前者采用聚香手段与机动车的燃料层同通信,而后者采用百分比抽象,能确定前者都是一些变量存取器,而无法得知后者中的数据形态。
不应该暴露数据细节,而应该以抽象形态表述数据。但并不意味着只是用接口或赋值器、取值器就万事大吉。要以最好的方式呈现某个对象包含的数据,需要严肃的思考。
随意乱加取值器和赋值器是最坏的选择。
6.2 数据、对象的反对称性
对象与数据结构:对象把数据隐藏于抽象之后,暴露操作数据的函数;而数据结构暴露其数据,没有提供有意义的函数。但这两种定义的本质其实是对立的。
对象与数据结构之间的二分原理
过程式代码(使用数据结构的代码) 便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。
反过来同理
过程式代码难以添加新数据结构,因为必须修改所有函数;面向对象代码难以添加新函数,因为必须修改所有类。
所以,面向对象难以完成的事,对于过程式代码却较容易,反之亦然。
- 对于想要添加新数据类型而不是新函数的时候,对象和面向对象就比较合适。
- 对于想要添加新函数而不是数据类型的时候,过程式代码和数据结构就更适合。
6.3 德墨忒尔律
模块不应了解它所操作操作对象的内部情形。对象应该隐藏数据,暴露操作。不应通过存储器暴露其内部结构。
德墨忒尔律认为,类C的方法f只应该调用以下对象的方法:
- C;
- 由f创建的对象;
- 作为参数传递给f的对象;
- 由C的是实体变量持有的对象;
只跟朋友谈话,不与陌生人谈话:方法不应调用由任何函数返回的对象方法。
以下方法违反了该定律,因为他调用了getOptions()返回值的getScratchDir()方法,又调用了getScratchDir()返回值的getAbsolutePath()方法。
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
6.3.1 火车失事
连串的调用通常被认为是肮脏的风格,这类代码通常被称为火车失事,应该避免。
上述问题最好做类似如下的切分:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePayh();
6.3.2 混杂
混杂结构,即一半是对象,另一半是数据结构。这种结构拥有执行操作的函数,也有公共变量或公共访问器及改值器。
无论处于怎样的初衷,公共访问器及改值器都把私有变量公开化,诱导外部函数以过程式程序使用数据结构的方式使用这些变量。
混杂增加了添加新函数的难度,也增加了添加新数据结构的难度,应该避免创造这种结构。
6.4 数据传送对象
最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象(DTO)。
DTO是非常有用的结构,尤其是在与数据库通信或解析套接字传递的消息之类的场景中,在应用程序代码里一系列将原始数据数据转换为数据库翻译过程中,他们往往是排头兵。
6.4.1 Active Record
Active Record是一种特殊的DTO形式。他们是拥有公共变量的数据结构,但通常也会拥有类似save和find这样可浏览方法。一半是对数据库表或其他数据源的直接翻译。
6.5 总结
对象暴露行为,隐藏数据,便于添加新对象类型而无须修改既有行为,同时难以在既有对象中添加新行为;数据结构报录数据,没有明显的行为,便于向既有数据结构添加新行为,同时难以向既有函数添加新数据结构。
在任何系统中,我们有时会希望能够灵活地添加新数据类型,所以更喜欢在这部分使用对象。另一些时候我们希望能灵活地添加新行为,这是我们更喜欢使用数据类型和过程。