2019年在大团队内部组织了一个 Study Group,主要目的是一起学习、一起分享、一起成长;上个季度我们的一项重点是完成了《Clean Code》这本书的拆解和分享,本文是对其中最后一章的一个总结。其实最后一章是对全书的总结,随意本文也可以近似看成是对全书的总结吧。
原始总结请参考 https://github.com/AlphaWang/alpha-book-clean-code/tree/master/17_smells_heuristics
17.1 注释(Comments)
C1: 不恰当的信息(Inappropriate Information)
注释只应该描述有关代码和设计的技术性信息。
反例:修改历史记录
C2: 废弃的注释(Obsolete Comment)
过时、无关、不正确的注释就是废弃的注释。
问题:造成误导
C3: 冗余注释(Redundant Comment)
注释应该谈及代码自身 没有(不能)提到的东西。
Comments should say things that the code cannot say for itself.
反例:
1
|
|
1 2 3 4 5 6 |
|
C4: 糟糕的注释(Poorly Written Comment)
如果注释值得写,那就值得好好写。
- 注意语法、拼写;
- 不画蛇添足、保持简洁。
C5: 注释掉的代码(Commented-Out Code)
看到注释掉的代码,就删掉它!
17.2 环境(Environment)
E1: 需要多步才能实现的构建(Build Requires More Than One Step)
You should be able to check out the system with one simple command and then issue one other simple command to build it.
1 2 3 |
|
E2: 需要多步才能做到的测试(Tests Require More Than One Step)
You should be able to run all the unit tests with just one command.
17.3 函数(Functions)
F1: 过多的参数(Too Many Arguments)
函数参数越少越好。
F2: 输出参数(Output Arguments)
Readers expect arguments to be inputs, not outputs.
F3: 标识参数(Flag Arguments)
有布尔值参数,说明函数做了不止一件事。
F4: 死函数(Dead Function)
永不被调用的方法应该丢弃。
17.4 一般性问题(General)
G1: 一个源文件中存在多种语言(Multiple Languages in One Source File)
G2: 明显的行为未被实现(Obvious Behavior Is Unimplemented)
Following The Principle of Least Surprise
, any function or class should implement the behaviors that another programmer could reasonably expect.
1
|
|
Reasonable expectations:
- “Monday” –> Day.MONDAY
- “monday” –> Day.MONDAY
- “Mon” – Day.MONDAY
G3: 不正确的边界行为(Incorrect Behavior at the Boundaries)
Don’t rely on your intuition. Look for every boundary condition and write a test for it.
G4: 忽视安全(Overridden Safeties)
Turning off certain compiler warnings may help you get the build to succeed, but at the risk of endless debugging sessions.
G5: 重复(Duplication)
DRY: Don’t Repeat Yourself. 这是本书最重要的规则之一
- 重复代码代表遗漏了抽象。
G6: 在错误的抽象层级上的代码(Code at Wrong Level of Abstraction)
1 2 3 4 5 |
|
- percentFull()方法应该放到子接口
BoundedStack
上。
G7: 基类依赖于派生类(Base Classes Depending on Their Derivatives)
In general, base classes should know nothing about their derivatives.
G8: 信息过多(TMI, Too Much Information)
- 类或模块中暴露的接口越少越好;
- 类中的方法越少越好;
- 函数知道的变量越少越好;
- 类拥有的实体变量越少越好;
G9: 死代码(Dead Code)
- 不被调用的方法;
- 不会进入的if/switch条件;
- 从不抛出异常的try-catch;
Tips: find dead code by IntelliJ
Analyze
—> Run Inspection by Name…
—> Unused declaration
G10: 垂直分隔(Vertical Separation)
垂直距离要短,
- 变量和函数应该在靠近被使用的地方定义。
- 本地变量应该在首次被使用的位置上面声明。
- 私有函数应该刚好在其首次被调用的位置下面定义。
G11: 前后不一致(Inconsistency)
If you do something a certain way, do all similar things in the same way.
HttpServletResponse response;
vs.HttpServletResponse httpResponse;
loadInterstellarVendorItem()
vs.getInterstellarVendorItem()
G12: 混淆视听(Clutter)
没有用到的变量;
从不调用的函数;
没有信息量的注释;
All these things are clutter and should be removed.
G13: 人为耦合(Artificial Coupling)
不互相依赖的东西就不该耦合。
反例:把普通的enum声明在特殊类中。
G14: 特性依恋 (Feature Envy)
see Martin Fowler’s Refactoring.
The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes.
1 2 3 4 5 6 7 8 9 10 11 |
|
The calculateWeeklyPay method envies the scope of HourlyEmployee. It “wishes” that it could be inside HourlyEmployee.
但事无绝对,下面这个reportHours方法如果移到HourlyEmployee类中,就会违反SRP原则。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
G15: 选择算子参数(Selector Arguments)
什么是selector arguments:用于选择函数行为的参数。
- boolean
- enum
- …
1 2 3 4 5 6 7 8 9 10 |
|
G16: 晦涩的意图(Obscured Intent)
1 2 3 4 5 6 |
|
这个函数看起来短小紧凑,但究竟是在做什么事情呢?
G17: 位置错误的权责(Misplaced Responsibility)
Question: PI常量应该放在Math类、Trigonometry类、还是Circle类?
- Following
The Principle of Least Surprise
. Code should be placed where a reader would naturally expect it to be.
G18: 不恰当的静态方法(Inappropriate Static)
恰当的静态方法:
1 2 3 4 5 |
|
不恰当的静态方法:
1
|
|
原因:有理由希望这个函数是多态的。(OvertimeHourlyPayCalculator, StraightTimeHourlyPayCalculator)
可以作为Employee方法的非静态函数。
G19: 使用解释性变量(Use Explanatory Variables)
让程序可读的有力方法之一就是将计算过程打散,用有意义的变量名存储中间值。
1 2 3 4 5 6 |
|
G20: 函数名称应该表达其行为(Function Names Should Say What They Do)
反例:
1
|
|
正例:
1 2 |
|
G21: 理解算法(Understand the Algorithm)
Before you consider yourself to be done with a function, make sure you understand how it works. It is not good enough that it passes all the tests. You must know that the solution is correct.
G22: 把逻辑依赖改为物理依赖(Make Logical Dependencies Physical)
Logical Dependency:
- The dependent module should not make assumptions about the module it depends upon.
Physical Dependency:
- it should explicitly ask that module for all the information it depends upon.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
PAGE_SIZE应该是HourlyReportFormatter的职责; 此处HourlyReporter被假定知道PAGE_SIZE,所以是逻辑依赖。
- We can physicalize this dependency by creating a new method in
HourlyReportFormatter
namedgetMaxPageSize()
. HourlyReporter
will then call that function rather than using thePAGE_SIZE
constant.
G23: 用多态替代 if-else 或 switch-case(Prefer Polymorphism to If/Else or Switch/Case)
反例: CartSectionHeaderAssembler
G24: 遵循标准约定(Follow Standard Conventions)
建议参考阿里巴巴Java开发手册:https://github.com/alibaba/p3c
G25: 用命名常量替代魔术数(Replace Magic Numbers with Named Constants)
反例:
- 86400
正例:
- int SECONDS_PER_DAY = 86400;
问题:是不是所有数字都需要替换成常量?
1 2 3 |
|
G26: 准确(Be Precise)
- 不要用
float
表示货币; - 如果所调用的方法可能返回null,就必须要做null检查;
G27: 结构优于约定(Structure over Convention)
switch/cases with nicely named enumerations are inferior to base classes with abstract methods.
G28: 封装条件(Encapsulate Conditionals)
如果 if 或者 while 语句没有上下文,那就很难理解其判断逻辑了。
反例
1
|
|
正例
1
|
|
G29: 避免否定性条件(Avoid Negative Conditionals)
Negatives are just a bit harder to understand than positives.
反例
1
|
|
正例
1
|
|
G30: 函数只该做一件事(Functions Should Do One Thing)
SRP原则。
G31: 掩蔽时序耦合(Hidden Temporal Couplings)
有时函数的执行次序很重要,这时就需要用某种机制(例如 bucket brigade)来确保其他程序员不能随意调整执行次序。
反例
1 2 3 4 5 6 7 8 9 10 11 |
|
正例
1 2 3 4 5 6 7 8 9 10 11 |
|
G32: 别随意(Don’t Be Arbitrary)
反例:
滥用内部类。本应是一个顶级类,却随意定义在另一个类内部作为内部类。
G33: 封装边界条件(Encapsulate Boundary Conditions)
要把处理边界条件的代码集中到一处,而不是散落在代码中。
反例:
1 2 3 4 |
|
正例:
1 2 3 4 5 |
|
G34: 函数应该只在一个抽象层级上(Functions Should Descend Only One Level of Abstraction)
反例:
1 2 3 4 5 6 7 |
|
这个方法混杂了至少两个抽象层级:
- The first is the notion that a horizontal rule has a size.
- The second is the syntax of the HR tag itself.
正例:
1 2 3 4 5 6 7 8 9 10 11 |
|
G35: 在较高层级放置可配置数据(Keep Configurable Data at High Levels)
如果你有一个常量值表示默认值或者配置值,不要把它埋在底层的函数中。
正例:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
G36: 避免传递浏览(Avoid Transitive Navigation)
Law of Demeter.不要让模块了解太多其写作者的信息。
反例:
1
|
|
正例:
1
|
|
17.5 Java
J1: 通过使用通配符避免过长的导入清单(Avoid Long Import Lists by Using Wildcards)
如果使用了来自同一包的两个或多个类,用通配符导入:import package.*
。
仅仅是为了减少代码行?
J2: 不要继承常量(Don’t Inherit Constants)
不要在子类中访问父类的常量。因为不方便找到常量究竟定义在哪个类里。
通过IDEA可以方便地找到呀。。。
J3: 常量 vs. 枚举(Constants versus Enums)
推荐用枚举。
17.6 名称(Names)
N1: 采用描述性名称(Choose Descriptive Names)
N2: 名称应与抽象层级相符(Choose Names at the Appropriate Level of Abstraction)
Don’t pick names that communicate implementation; choose names the reflect the level of abstraction of the class or function you are working in.
命名不要暴露实现细节,只用描述功能抽象。
N3: 尽可能使用标准命名法(Use Standard Nomenclature Where Possible)
如果用装饰者模式,就以Decorator结尾。
N4: 无歧义的命名(Unambiguous Names)
反例
1 2 3 4 5 6 7 8 9 |
|
正例
1 2 3 |
|
N5: 为较大作用范围选用较长名称(Use Long Names for Long Scopes)
如果变量作用范围很小,例如5行之内,那么取名为i
j
是没问题的;但是如果作为范围较大,则应该使用较长的名称。
N6: 避免编码(Avoid Encodings)
不要在名称中包含类型或作用域信息。
N7: 名称应该说明副作用(Names Should Describe Side-Effects)
反例
1 2 3 4 5 6 7 |
|
正例
1 2 3 |
|
17.7 测试(Tests)
T1: 测试不足(Insufficient Tests)
一套测试应该测到所有可能失败的东西。
只要还有没被测试探测过的条件,或是还有没被验证过的计算,那么测试就是不足的。
T2: 使用覆盖率工具(Use a Coverage Tool!)
IDE支持。
T3: 别略过小测试(Don’t Skip Trivial Tests)
小测试易于编写,其文档上的价值高于编写成本。
T4: 被忽略的测试就是对不确定事物的疑问(An Ignored Test Is a Question about an Ambiguity)
有时因为需求不明,我们可以把测试表为@Ignore
。
T5: 测试边界条件(Test Boundary Conditions)
边界判断错误的情形很常见。
T6: 全面测试相近的缺陷(Exhaustively Test Near Bugs)
80% 的软件缺陷常常生存在软件 20% 的空间里。
T7: 测试失败的模式有启发性(Patterns of Failure Are Revealing)
有时可以通过测试用例失败的模式来诊断问题所在。
T8: 测试覆盖率的模式有启发性(Test Coverage Patterns Can Be Revealing)
Looking at the code that is or is not executed by the passing tests gives clues to why the failing tests fail.
T9: 测试应该快速(Tests Should Be Fast)
A slow test is a test that won’t get run. When things get tight, it’s the slow tests that will be dropped from the suite. So do what you must to keep your tests fast.