代码整洁之道
第一章 整洁代码
1.1 什么是整洁的代码?
资深程序员告诉你什么是整洁的代码,下面详细介绍了Bjarne Stroustrup的描述,其他描述就不一一列出了,在此只记录几个关键点。
我喜欢高效优雅的代码。代码逻辑应当直接了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。 ------Bjarne Stroustrup, C++语言发明者, C++ Programming Language作者
- 整洁的代码简单直接 ------
Grady Booch
- 整洁的代码总是看起来像是某位特别在意它的人写的,几乎没有改进的余地 ------
*Michael Feathers*
- 代码让编程语言看起来像是专门为解决那个问题而存在 ------
*Ward Cunningham*
1.2 破窗理论
窗户破损了的建筑让人觉得似乎无人照管。于是别人再也不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。
1.3 童子军军规
光把代码写好可不够,必须时时保持代码整洁
“让营地比你来时更干净”
第二章 有意义的命名
软件中的命名,如变量、函数、参数、类、包、目录和文件名等等,下面介绍简单的几条规则。
-
名副其实, 看到名称能很容易理解其用处
-
避免误导,避免留下掩藏代码本意的错误线索,如有些情况下使用字母l数字1,字母o数组0容易混淆
-
做有意义的区分,如下示例复制字符数组,若将参数名改为source和destination就形象很多
public static void copyChars(char a1[], char a2[]) { for(int i = 0; i < a1.length; i++) { a2[i] = a1[i]; } }
-
使用读得出来的名称,如构建数createTree(), 别用一连串的只是自己看起来好像明白的首字母缩写
-
使用可搜索的名称, 名称长短应与其作用域的大小相对应
-
避免使用编码,如匈牙利语标记法、成员前缀(当类足够小时可消除对前缀的需要)
-
避免思维映射, 不应让读者把你的名称翻译为他们熟悉的名称
-
类名和对象名应该是名词或名词短语,方法名应当是动词或动词短语(如属性访问器get、修改器set、断言is)
-
别扮可爱(自己体会)
-
别用双关语
-
添加有意义的语境
第三章 函数
如何写好函数?
3.1 编写技巧
-
函数要短小,20行封顶最佳,函数要只做一件事
-
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。通常都是自顶向下读代码:向下规则,如有些函数中引出下一个函数
-
若switch语句选择太多(违反了单一职责原则),可将switch语句封装在抽象工厂底下,不让任何人看到
-
使用描述性的名称
沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码”
函数越短小、功能越集中,就越便于取个好名字
3.2 函数参数
-
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数
-
标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。
-
如果函数看来需要两个、三个或三个以上的参数,就说明其中一些参数应该封装为类了
如下面两个声明的差别
Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);
-
动词与关键词,对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。如下
void write(String name); void writeField(String name); void assertExcepedEqualsActual(... exceped, ... actual);
3.3 使用异常替代返回错误码
使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。
抽离 Try/Catch 代码块,另外形成函数,如下
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
函数应该只做一件事,错误处理就是一件事
Error.java依赖磁铁,返回错误码通常暗示某处有个类或是枚举,定义了所有错误码,这样的类就是一块依赖磁铁(dependency magnet),不方便添加和修改。而使用异常代替错误码,新异常就可以从异常类派生出来,无需重新编译或者重新部署
3.4 其他
- 别重复自己,重复可能是软件中一切邪恶的根源。要高效利用代码,避免代码冗余
- 结构化编程,每个函数、函数中的每个代码块都应该只有一个入口,一个出口,就是每个函数只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。 小函数中结构化规则益处不大,往往在大函数中才会发觉明显的好处
第四章 注释
“别给糟糕的代码加注释–重新写吧。” ---- Brian W.Kernighan 与 P.J.Plaugher
程序员应当负责将注释保持在可维护、有关联、精确的高度。作者同意此,但他更主张把力气用在写清楚代码上,直接保证无需编写注释
有时多花几秒钟,只需创建一个描述与注释所言同一事物的函数即可,此时代码就能解释大部分意图
4.1 好注释
- 法律信息,列如版权及著作权声明等
- 提供信息的注释,更好的方式是尽量利用函数名传达信息
- 对意图的解释
- 阐述(如将晦涩难懂的参数或返回值的意义翻译为某种可读形式,更好的方式是尽量让参数或者返回值自身足够清楚)
- 警示 (警告其他程序员会出现某种后果的注释)
- 公共API中的Javadoc
4.2 坏注释
- 喃喃自语 ,无必要的注释
- 多余的注释,如简单函数头部位置的注释
- 误导性注释
- 循规式注释
- 日志式注释(记录修改)
- 废话注释 (用整理代码的决心替代创造废话的冲动吧)
- 位置标记 (//Actions)
- 括号后面的注释 (如循环方便查看语句块,}结束于哪一个)
- 归属与署名 (/* Added by Rick */)
- 注释掉的代码 (别这么干!相信自己,可以删!)
- 信息过多
- 不明显的联系
- 函数头 (短函数不需要太多描述,好的函数名重于好的注释)
第五章 格式
好的代码格式可以让自己看起来舒畅,让别人读起来舒心。下面主要从垂直横向格式来介绍,其他的简单说明下
5.1 垂直格式
-
向报纸学习 报纸的排版,大纲。若报纸只是一片长文或者是杂乱的故事碎片则没有读者对它感兴趣
-
概念间垂直方向上的区隔
适当的空白行能够让代码赏心悦目,往下读代码时,你的目光总会停留于空白行之后那一行
-
垂直方向上的靠近
紧密相关的代码应该互相靠近
-
垂直距离
变量声明应尽可能靠近其使用位置
实体变量应该在类的顶部声明
相关函数的顺序应按其逐级调用的自然顺序排列
概念相关的代码应放到一起。相关性越强,彼此之间的距离就该越短。
5.2 横向格式
-
每一行代码长度应尽量不需拖动滚动条,45字符最好(与显示屏和字符大小相关)
-
格式化工具
-
水平对齐
作者更喜欢不对齐的声明和赋值,因为它们指出了重点。如果有较长的列表需要对齐处理,那问题就是在列表的长度上而不是对齐上
~个人喜欢较少量的声明对齐*_*
-
有效适当的缩进
5.3 团队规则
每个团队都应该有自己规定或遵循的编码准则,这样才能更高效的交流和完成任务
第六章 对象和数据结构
6.1 数据、对象的反对称性
在讨论前先看看下面两种形式的代码
过程式形状代码
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.1415926;
public double area(Object shape) throws NoSuchShapeException {
if(shape instanceof Square) {
Square s = (Square)shape;
return s.side * s.side;
} else if(shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
} else if(shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
多态式形状代码
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.1415926;
public double area(){
return PI * radius * radius;
}
}
仔细看这两种定义的本质,它们是截然对立的。这说明了对象与数据结构之间的二分原理:
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类
- 反过来描述就是,过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类
6.2 得墨忒耳律
得墨忒耳律(The Law of Demeter)认为,模块不应该了解它所操作对象的内部情形。
方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。
如下列代码违反了此定律
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
此类代码常被称为火车失事,因为它看起来就像一列火车
6.3 数据传送对象
最为精炼的数据结构,是一个只有公共变量、没有函数的类。这种数据结构有时被称为数据传送对象,或DTO(Data Transfer Objects)。DTO是非常有用的数据结构,尤其在与数据库通信、或解析套接字传递的消息之类场景中。
6.4 总结
对象暴露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。数据结构暴露数据,没有明显的行为。便于向既有数据结构中添加新行为,同时也难以向既有函数添加新数据结构
第七章 错误处理
错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。本章讲述了雅致处理错误代码的一些技巧和思路
-
使用异常而非返回码
-
先写Try- Catch-Finally语句,异常的妙处之一是,它们在程序中定义了一个范围
-
使用不可控异常
可控异常的代价就是违反开闭原则。如果你在方法中抛出可控异常,而catch语句在三个层级之上,你就得在catch语句和抛出异常之处的每个方法签名中声明该异常。这意味着对软件中较低层级的修改,都将波及较高层级的签名。假设某个位于最底层的函数被修改为抛出一个异常,最终的得到的就是一个从软件最底端贯穿到最高端的修改链。
-
给出异常发生的环境说明。应创建信息充分的错误消息,并和异常一起传递出去
-
依调用者需要定义异常类
做相对标准的处理。实际上,将第三方API打包是一个良好的实践手段。当你打包了一个第三方API,你就降低了对它的依赖
-
定义常规流程
特例模式(Special Case Pattern,即创建一个类或者配置一个对象,用来处理特例
-
别返回null值,别传递null值,可用assert断言代替
public class MetricsCalculator { public double xProjection(Point p1, Point p2) { return (p2.x - p1.x) * 1.5; } } //假如传入null值,可更改为 public class MetricsCalculator { public double xProjection(Point p1, Point p2) { if(p1 == null || p2 == null) { throw InvalidArgumentException("Invalid argement for MetricsCalculator"); } return (p2.x - p1.x) * 1.5; } } //替代方案可以使用一组断言 public class MetricsCalculator { public double xProjection(Point p1, Point p2) { assert p1 != null : "p1 should not be null"; assert p2 != null : "p2 should not be null"; return (p2.x - p1.x) * 1.5; } }
第八章 边界
保持软件边界整洁
-
使用第三方代码
-
浏览和学习边界
通过编写测试来遍历和理解第三方代码,称为学习性测试(learning tests)
在学习性测试中,我们如在应用中那样调用第三方代码。我们基本上都是在通过核对试验来检测自己对那个API的理解程度
第九章 单元测试
有了一套运行通过的测试,任何需要用到代码的人都能方便地使用这些测试
9.1 TDD三定律
TDD(Test - Driven - Development)即测试驱动开发。TDD要求在编写生产代码前先编写单元测试
- 定律一 在编写不能通过的单元测试前,不可编写生产代码
- 定律二 只可编写刚好无法通过测试的单元测试,不能编译也算不通过
- 定律三 只可编写刚好足矣通过当前失败测试的生产代码
9.2 保持测试整洁
脏测试等同于----如果不是坏于的话----没测试,测试代码和生产代码一样重要。
单元测试让代码可扩展、可维护、可复用
整洁的测试要有高度的可读性
-
每个测试都应该拆分为三个环节,构造测试数据、操作测试数据、检验操作是否得到预期结果。即构造–操作–检验(Build–Operate–Check)。
-
单个断言是个好准则,(单个测试中的断言数量应该最小化)
9.3 F.I.R.S.T
整洁的测试还遵循以下5条规则
- 快速(fast)测试应该够快(运行··
- 独立(independent)测试应该相互独立
- 可重复(repeatable)测试应当可在任何环境中重复通过
- 自足验证(self-validating)测试应该有布尔值输出
- 及时(timely)测试应及时编写
第十章 类
遵循标准的java约定,类应该从一组变量列表开始。如果有共有静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有共有变量。
公共函数应该跟在变量列表之后。我们更喜欢把由某个公共函数调用的私有工具函数和紧跟随在该公共函数后面,这符合自顶向下原则。
-
类应该短小
- 单一职责原则(类或模块应有且只有一条加以修改的理由)
- 高内聚(意味着类中的方法和变量相互依赖、互相结合成一个逻辑整体)如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性
- 保持内聚性就会得到许多短小的类
-
为了修改而组织
在整洁的系统中,我们对类加以组织,以降低修改的风险
类应当符合开闭原则(OCP),对扩展开放,对修改关闭
由于本人水平有限,还未有实习经历,对java框架接触也不多,所以就整理了此书前十章的内容,但愿几年后能完成剩下章节的整理。虽说只是前十章的内容,但都是前辈们多年的经验,所有精华汇聚于此,希望大家有所收益!!!
本文所有内容均来自《代码整洁之道》[美]Robert C. Martin,译者,韩磊。最终版权归此书作者及出版方所有。若有侵权敬请告知。