《代码整洁之道》读书笔记
本文最后更新于:4 个月前
代码整洁之道
整洁代码
整洁之道
代码是我们最终用来表达需求的那种语言,代码永存;
时时保持代码整洁,稍后等于永不(Later equals never);
整洁代码力求集中,每个函数、每个类和每个模块都全神贯注于一件事;
整洁代码简单直接,从不隐藏设计者的意图;
整洁代码应当有单元测试和验收测试。它使用有意义的命名,代码通过其字面表达含义;
消除重复代码,提高代码表达力。
有意义的命名
避免误导
“一组账号”别用
accountList
表示,List
对程序员有特殊含义,可以用accountGroup
、bunchOfAccounts
、甚至是accounts
;不使用区别较小的名称,
ZYXControllerForEfficientHandlingOfStrings
和ZYXControllerForEfficientStorageOfStrings
难以辨别;不使用小写l、大写O作变量名,看起来像常量1、0。
做有意义的区分
不以数字系列命名(a1、a2、a3),按照真实含义命名;
Product/ProductInfo/ProductData
意思无区别,只统一用一个;别写冗余的名字,变量名别带
variable
、表名别带table
。
使用读得出来的名称
-
genymdhms
(生成日期,年、月、日、时、分、秒)肯定不如generation timestamp
(生成时间戳)方便交流。
使用可搜索的名称
- 单字母名称和数字常量很难在上下文中找出。名称长短应与其作用域大小相对应,越是频繁出现的变量名称得越容易搜索(越长)。
命名时避免使用编码
把类型和作用域编码进名称里增加了解码负担。意味着新人除了了解代码逻辑之外,还需要学习这种编码语言;
别使用匈牙利语标记法(格式:**[Prefix]-BaseTag-Name** 其中BaseTag是数据类型的缩写,Name是变量名字),纯属多余。例如,
szCmdLine
的前缀sz
表示“以零结束的字符串”;不必用
m_
前缀来表明成员变量;接口和实现别在名称中编码。接口名
IShapeFactory
的前导”I”是废话。如果接口和实现必须选一个编码,宁可选实现,ShapeFactoryImp
都比对接口名称编码来的好。
避免思维映射
不应当让读者在脑中把你的名称翻译为他们熟知的名称。例如,循环计数器自然有可能被命名为
i
或j
或k
,但千万别用字母l
;专业程序员了解,明确是王道,编写能方便他人理解的代码。
类名、方法名
- 类名应当是名词或名词短语,方法名应当是动词或动词短语。
命名不要耍宝幽默
- 言到意到,意到言到,不要在命名上展示幽默感。
每个概念用一个词
fetch
、retrieve
、get
约定一个一直用即可。
尽管使用计算机科学术语
- 只有程序员才会读你的代码,不需要按照问题所在邻域取名称。
别用双关语
add
方法一般语义是:根据两个值获得一个新的值。如果要把单个值加入到某个集合,用insert
或append
命名更好,这里用add
就是双关语了。
添加有意义的语境
- 很少有名称能自我说明,需要用良好命名的类、函数、或者命名空间来放置名称,给读者提供语境,如果做不到的话,给名称添加前缀就是最后一招了。
函数
越短越好
短小,20行封顶;
if/else/while
语句的代码块应该只有一行,该行应该是一个函数调用语句;函数的缩进层级不应该多于一层或两层。
一个函数只做一件事
如果函数只是做了该函数名下同一抽象层上的步骤,则函数只做了一件事;
要判断函数是否不止做了一件事,就是要看是否能再拆出一个函数;
每个函数一个抽象层级
- 向下规则:让代码拥有自顶向下的阅读顺序。每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。
switch语句
- 把switch埋在较低的抽象层级,一般可以放在抽象工厂底下,用于创建多态对象。
使用描述性的名称
函数越短小、功能越集中,就越便于取个好名字;
别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好,要比描述性的长注释好;
别害怕花时间取名字。
函数参数
参数越少越好,0参数最好,尽量避免用三个以上参数;
参数越多,编写组合参数的测试用例就越困难;
别用标识参数,向函数传入
bool
值是不好的,这意味着函数不止做一件事。可以将此函数拆成两个;如果函数需要两个、三个或者三个以上参数,就说明其中一些参数应该封装成类了;
将参数的顺序编码进函数名,减轻记忆参数顺序的负担,例如
assertExpectedEqualsActual(expected, actual)
。
副作用(函数在正常工作任务之外对外部环境所施加的影响)
检查密码并且初始化
session
的方法命名为checkPasswordAndInitializeSession
而非checkPassword
,即使违反单一职责原则也不要有副作用;避免使用”输出参数”,如果函数必须修改某种状态,就修改所属对象的状态吧。
设置(写)和查询(读)分离
-
1
2
3
4if(set("username", "unclebob"))
{
...
}含义模糊不清。应该改为:
1
2
3
4if (attributeExists("username"))
{
setAttribute("username", "unclebob");
}
使用异常代替返回错误码
返回错误码会要求调用者立刻处理错误,从而引起深层次的嵌套结构;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if (deletePate(page) == E_OK)
{
if (xxx() == E_OK)
{
if (yyy() == E_OK)
{
log();
}
else
{
log();
}
}
else
{
log();
}
}
else
{
log();
}所以需要用
try catch
异常机制;1
2
3
4
5
6
7
8
9
10try
{
deletePage();
xxx();
yyy();
zzz();
} catch (Exception e)
{
log(e->getMessage());
}try/catch
代码块丑陋不堪,所以最好把try
和catch
代码块的主体抽离出来,单独形成函数。1
2
3
4
5
6
7try
{
do();
} catch (Exception e)
{
handle();
}
不要写重复代码
- 重复是软件中一切邪恶的根源。当算法改变时需要修改多处地方。
结构化编程
只要函数保持短小,偶尔出现的
return
、break
、continue
语句没有坏处,甚至还比单入单出原则更具有表达力。goto
只有在大函数里才有道理,应该尽量避免使用。并不需要一开始就按照这些规则写函数,没人做得到。想些什么就写什么,然后再打磨这些代码,按照这些规则组装函数。
注释
若编程语言足够有表现力,我们就不需要注释;
注释总是一种失败,因为我们无法找到不用注释就能表达自我的方法;
代码在演化,注释却不总是随之变动,会变得越来越不准确。
用代码来阐述
创建一个与注释所言同一事物的函数即可,
1
2// check to see if the employee is eligible for full benefits
if ((employee.falgs & HOURLY_FLAG) && (employee.age > 65))应替换为
1
if (employee.isEligibleForFullBenefits())
好注释
法律信息,并且只要有可能就指向标准许可或者外部文档,而不是放全文;
提供基本信息,如解释某个抽象方法的返回值;
对意图的解释,反应了作者某个决定后面的意图;
阐释。把某些晦涩的参数或者返回值的意义翻译成可读的形式(更好的方法是让它们自身变得足够清晰,但是类似标准库的代码我们无法修改);
1
if (b.compareTo(a) == 1) //b > a
警示。
// don't run unless you have some time to kill
;TODO
注释;放大 一些看似不合理之物的重要性。
坏注释
自言自语;
多余的注释。把逻辑在注释里写一遍不能比代码提供更多信息,读它不比读代码简单。一目了然的成员变量别加注释,显得很多余;
误导性注释;
遵循规矩的注释。每个函数都加注释、每个变量都加注释是愚蠢的;
日志式注释。有了代码版本控制工具,不必在文件开头维护修改时间、修改人这类日志式的注释;
能用函数或者变量表示就别用注释;
1
2
3
4// does the module from the global list <mod>
// depend on the subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())可以改为:
1
2
3ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))位置标记。标记多了会被我们忽略掉;
///////////////////// Actions //////////////////////////
右括号注释;
1
2
3
4
5
6
7
8
9
10
11
12try
{
while ()
{
if ()
{
...
} // if
...
} // while
...
} // try如果你想标记右括号,其实应该做的是缩短函数
署名
/* add by rick */
源代码控制工具会记住你,署名注释跟不上代码的演变;注释掉的代码。会导致看到这段代码其他人不敢删除,使用版本控制系统,可以大胆删除需要注释的代码;
信息过多。别在注释中添加有趣的历史话题或者无关的细节;
没解释清楚的注释。注释的作用是解释未能自行解释的代码,如果注释本身还需要解释就太遗憾了;
短函数的函数头注释。为短函数选个好名字比函数头注释要好;
非公共API函数的
javadoc/phpdoc
注释。
格式
垂直格式
短文件比长文件更易于理解。平均200行,最多不超过500行的单个文件可以构造出色的系统;
像报纸一样排版,由略及详,层层递进;
区隔: 封包声明、导入声明、每个函数之间,都用空白行分隔开,空白行下面标识着新的独立概念,表示一个思路的开始
靠近: 紧密相关的代码应该互相靠近,例如一个类里的属性之间别用空白行隔开;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class ReporterConfig
{
//The class name of the reporter listener
private String m_className;
//The properties of the reporter listener
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property)
{
m_properties.add(property);
}
}
///////////////////////对比////////////////////////////////////
public class ReporterConfig
{
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property)
{
m_properties.add(property);
}
}变量声明应尽可能靠近其使用位置:循环中的控制变量应该总是在循环语句中声明;
成员变量应该放在类的顶部声明,不要四处放置;
如果某个函数调用了另外一个,就应该把它们放在一起。我们希望底层细节最后展现出来,不用沉溺于细节,所以调用者尽可能放在被调用者之上;
执行同一基础任务的几个函数应该放在一起。
水平格式
一行代码不必死守80字符的上限,偶尔到达100字符不超过120字符即可;
区隔与靠近: 空格强调左右两边的分割。*赋值运算符两边加空格,函数名与左圆括号之间不加空格,乘法运算符在与加减法运算符组合时不用加空格(ab - c)**;
不必水平对齐。例如声明一堆成员变量时,各行不用每一个单词都对齐,代码自动格式化工具通常会把这类对齐消除掉;
1
2
3
4
5
6
7public class FitNesseExpediter implements ResponseSender
{
private Socket socket;
private InputStream input;
private OutputStream output;
private Request request;
}短小的
if
、while
、函数里最好也不要违反缩进规则,不要这样:if (xx == yy) z = 1
;while
语句为空,最好分行写分号;1
2while(1)
;
团队规则
- 团队绝对不要用各种不同的风格来编写源代码,这样会增加其复杂度。
对象和数据结构
数据抽象
- 对象:暴露行为(接口),隐藏数据(私有变量) ;
- 数据结构:没有明显的行为(接口),暴露数据。如
DTO
(Data Transfer Objects)、Entity
;
数据,对象的反对称性
使用数据结构便于在不改动现在数据结构的前提下添加新函数;使用对象便于在不改动既有函数的前提下添加新类;
使用数据结构难以添加新数据结构,因为必须修改所有函数;使用对象难以添加新函数,因为必须修改所有类;
万物皆对象只是个传说,有时候我们也会在简单数据结构上做一些过程式的操作。
Law of Demeter
模块不应该了解它所操作对象的内部情形;
class C
的方法f
只应该调用以下对象的方法:C
在方法
f
里创建的对象作为参数传递给方法
f
的对象C
持有的对象
方法不应调用 由任何函数返回的对象 的方法。下面的代码违反了demeter定律:
1
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
一个简单例子是,人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。
错误处理
- 错误处理很重要,但是不能凌乱到打乱代码逻辑。
使用异常而不是返回错误码
如果使用错误码,调用者必须在函数返回时立刻处理错误,但这很容易被我们忘记;
错误码通常会导致嵌套
if else
。
先写try-catch语句
- 当编写可能会抛异常的代码时,先写好
try-catch
再往里堆逻辑。
在catch里尽可能的记录
- 在
catch
里尽可能的记录错误信息,记录失败的操作以及失败的类型
依调用者定义异常类
- 对错误分类有很多方式。可以依其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?
别返回null值
返回
null
值只要一处没检查null
,应用程序就会失败;当想返回
null
值的时候,可以试试抛出异常,或者返回特例模式的对象。
别传递null值
在方法中传递
null
值是一种糟糕的做法,应该尽量避免;在方法里用
if
或assert
过滤null
值参数,但是还是会出现运行时错误,没有良好的办法对付调动者意外传入的null
值,恰当的做法就是禁止传入null
值。
边界
将第三方代码干净利落地整合进自己的代码中
避免公共API返回边界接口,或者将边界接口作为参数传递给API。将边界保留在近亲类中;
不要在生产代码中试验新东西,而是编写测试来理解第三方代码;
避免我们的代码过多地了解第三方代码中的特定信息。
学习性测试是一种精确试验,帮助我们增进对API的理解。
单元测试
TDD(Test-driven development)三定律
First Law: You may not write production code until you have written a failing unit test.
Second Law: You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
Third Law: You may not write more production code than is sufficient to pass the currently failing test.
保持测试整洁
脏测试等同于没测试,测试代码越脏生产代码越难修改;
测试代码和生产代码一样重要;
整洁的测试代码最应具有的要素是:整洁性。测试代码中不要有大量重复代码的调用。
每个测试一个断言
每个测试函数有且仅有一个断言语句;
每个测试函数中只测试一个概念。
整洁的测试依赖于FIRST规则
fast: 测试代码应该能够快速运行,因为我们需要频繁运行它;
independent: 测试应该相互独立,某个测试不应该依赖上一个测试的结果,测试可以以任何顺序进行;
repeatable: 测试应可以在任何环境中通过;
self-validating: 测试应该有
bool
值输出,不应通过查看日志来确认测试结果,不应手工对比两个文本文件确认测试结果;timely: 及时编写测试代码。单元测试应该在生产代码之前编写,否则生产代码会变得难以测试。
类
类的组织
以下针对JAVA语言,其他语言类似,变量在前,方法在后,公有在前,私有在后。
公共静态常量
私有静态变量
私有实体变量
公共函数
私有工具函数
如果测试需要调用一个函数或变量,可以设为保护类型。
类应该短小
对于函数我们计算代码行数衡量大小,对于类我们使用权责来衡量;
类的名称应当描述其权责。类的命名是判断类长度的第一个手段,如果无法为某个类命以准确的名称,这个类就太长了。类名包含模糊的词汇,如
Processor
、Manager
、Super
,这种现象就说明有不恰当的权责聚集情况;单一权责原则(Single Responsibility Principle,SRP): 类或者模块应该有一个权责——只有一条修改的理由(A class should have only one reason to change.);
系统应该由许多短小的类而不是少量巨大的类组成;
类应该只有少量的实体变量,如果一个类中每个实体变量都被每个方法所使用,则说明该类具有最大的内聚性。创建最大化的内聚类不太现实,但是应该以高内聚为目标,内聚性越高说明类中的方法和变量互相依赖、互相结合形成一个逻辑整体;
保持内聚性就会得到许多短小的类。如果你想把一个大函数的某一小部分拆解成单独的函数,拆解出的函数使用了大函数中的4个变量,不必将4个变量作为参数传递到新函数里,仅需将这4个变量提升为大函数所在类的实体变量,但是这么做却因为实体变量的增多而丧失了类的内聚性,更好多做法是让这4个变量拆出来,拥有自己的类。将大函数拆解成小函数往往是将类拆分为小类的时机。
为了修改而组织
类应当对扩展开放,对修改封闭(开放闭合原则);
在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性。
系统
将系统的构造与使用分开
- 软件系统应将起始过程和之后的运行逻辑分开。
分解main
将全部构造过程搬迁到main或者被称之为
main
的模块中,涉及系统其余部分时,假设所有对象都已经正确构造;依赖注入(DI),控制反转(IoC)是分离构造与使用的强大机制。
迭代
表达力
作者把代码写的越清晰,其他人理解代码就越快;
太多时候我们深入于要解决的问题中,写出能工作的代码之后,就转移到下一个问题上,没有下足功夫调整代码让后来者易于阅读。多少尊重一下我们的手艺,花一点点时间在每个函数和类上。
尽可能少的类和方法
为了保持类和函数的短小,我们可能会早出太多细小的类和方法;
类和方法数量太多,有时是由毫无意义的教条主义导致的。
以上4条规则优先级依次递减。重要的是测试、消除重复、表达意图
并发编程
为什么要并发编程
并发总能改进性能;
编写并发程序无需修改设计;
在采用
Web
或EJB
容器的时候,理解并发问题并不重要。
防御并发代码问题的原则与技巧
遵循单一职责原则。分离并发代码与非并发代码;
限制临界区数量、限制对共享数据的访问;
避免使用共享数据,使用对象的副本;
线程尽可能地独立,不与其他线程共享数据。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!