Digest of Working Effectively with Legacy Code
Part I - The Mechanics of ChangePermalink
Chapter 1 - 修改代码的 4 个理由Permalink
修改代码的 4 个理由以及其侧重点:
理由/侧重点 | Code Structure | New Behavior | Old Behavior | Resource Usage |
---|---|---|---|---|
添加 feature | Change | Add | - | - |
修正 bug | Change | - | Change | - |
refactoring | Change | - | - | - |
optimizing | - | - | - | Change |
Chapter 2 - 修改代码的 2 种方式Permalink
修改代码的 2 种方式:
- Edit-and-Pray
- Cover-and-Modify
那第一种也不是说不行,但肯定不如第二种保险。
如何 Edit-and-PrayPermalink
请阅读 Chapter 6
如何 Cover-and-Modify: 要 Unit Test 而不是 Regression TestPermalink
Unit Test 和 Regression Test 虽然都是 test,但是它俩不是一个维度的概念。Unit Test 强调的是形式、是组成单位,Regression Test 强调的是性质。简单说,所有检测 “引入的新代码后,旧的 behavior 有没有改变” 的测试都是 Regression Test,它可以是 Unit Test、Integration Test、UI Test、etc.,或是它们的组合。
一般来说 Regression Test 会比较费时,对频繁的修改不够效率。Unit Test 颗粒明显要小很多,效率更高。如何判断一个 Unit Test 快不快?作者的理论是:耗时超过 0.1 秒的都不算快!考虑到 Java 的 application 可能有上千个 class,这个标准也说得通。
严格来说,Unit Test 有这么些要求:
- 要跑得快
- 不应该与 DB 交互
- 不应该有 network 通信
- 不应该调用 File System
- 不应该依赖特定的运行环境 (比如配置文件)
但实际情况往往无法做到这么严格。
依赖 (Dependency) 是 Unit Test 的障碍Permalink
表现在:
- 难以在测试中初始化 target 对象
- 难以在测试中调用 target 方法
如何 Cover-and-Modify: 流程Permalink
The Legacy-Code-Change algorithm:
- Identify change points. (确定改动点)
- Find test points. (找出测试点)
- Break dependencies. (解依赖)
- Write tests. (写测试)
- Make changes and refactor. (修改代码、重构)
Chapter 3 - 解依赖的两个目的:Sensing and SeparationPermalink
这一章是作者模糊表达的重灾区!写得弯弯绕绕的,真的有那么复杂吗?!
啥叫 sensing?给翻译翻译!Permalink
结合后面作者的用词,“to sense through object/method” 的意思其实就是 “搞清楚 object/method 具体干了啥”,具体说来就是:
- “搞清楚 object/method 计算了哪些 value” (这是书上的意思)
- “搞清楚 object/method 产生了哪些 side effect” (这是我个人的扩展)
所以简单来说,“sensing” 就是 “搞清楚” 的意思 (MD,搞 OO 的这帮人一天到晚在这嚼词汇也不是一天两天了)。
那我们说要 “sense the effect of change” 的意思就是 “搞清楚 change 对 target object/method 的 computed value/side effect 的影响”。
那我们为啥需要 sensing?这不明显着嘛:test 需要知道这些 computed value/side effect 啊,不然它咋测试?
那 sensing 和 break dependency 有啥关系?我个人的理解是:不需要 break dependency 也可以 sensing 啊,你读代码算不算 sensing?也算啊,对不对?所以这里我认为更准确的陈述是:“break dependency 是 sensing 的一种辅助手段”,因为有时候 dependency 部分的代码不太好 sense (或者说:target object/method 调用 dependency 产生的 computed value/side effect 不太好 sense) (这里涉及到一个 “你到底是 sense through dependency 还是 sense through target object/method” 的问题)
那什么是 fake collaborator?我个人的理解是:“fake collaborator 是一个具体的 break dependency 的技术,用来辅助 sensing”。
SeparationPermalink
Separation 比较简单,它主要就是解决前面说的两个问题:
- 难以在测试中初始化 target 对象
- 难以在测试中调用 target 方法
If dependency makes it impossible to set up a test harness, we need to separate the code from that dependency.
本书的大部分章节其实都是在解决 separation 的问题。
Fake CollaboratorPermalink
这一小节从技术角度好理解,注意 mock objects 是 “能够对 internal behavior 做 assertion 的” 高级 fake object 就可以了。
Chapter 4 - SeamPermalink
Seam 本意是 “接缝”,比如两块布的连接处、两片金属的焊接处,这里指:
A seam is a place where you can alter behavior in your program without editing in that place.
注意它定义的落脚点是 “place”,后面的 “seam types” 其实就是在介绍 “有哪些 places”:
- Preprocessing Seam: 比如用
#ifdef
来控制 behavior (的地方) - Link Seam: 通过控制 linking 的 library 来控制 behavior (的地方)
- Object Seam: 简单来说就是尽可能地利用多态,方便传入 fake/mock objects (的地方)
Preprocessing Seam 举例Permalink
可以看 Chapter 19
Link Seam 举例Permalink
用 python 举个例子。假设你的 prod 环境只有 orjson
,你的 test 环境只有 json
(或者在 prod/test 上都安装 orjson
和 json
,但是在启动脚本里提供不同的 classpath),你可以用:
try:
import orjson
except Exception:
import json
来动态 import,从而控制 caller 的 hehavior。
Object Seam 举例Permalink
用 java 举个例子:
public foo() {
Bar b = new Bar();
b.doSomething();
}
// 改写成:
public foo(Bar b) {
b.doSomething();
}
后面一种写法就能利用 Bar
的多态,使得我们能传入一个 Bar b = new MockBar()
来方便测试。
Chapter 5 - Tools (略)Permalink
这一章没啥用,略过。
Part III - Dependency-Breaking TechniquesPermalink
Part III 就一章,我也是醉了。对的,我就是要先写 Chapter 25,因为这些技能在 Part II 没有被完全 cover 到,等把 Part II 写完再写 Part III 就显得支离破碎的,不如先写了。
Chapter 25 - Dependency-Breaking TechniquesPermalink
技能树Permalink
灾难的 Chapter 25!一共 24 个技能,作者发了疯按技能名字的 alphabet 排序!然后这些名字还是作者自己起的,所以这个顺序本质上是随机的!大哥你好歹分一下类吧?有的技巧是修改测试目标类、有的是修改 dependency,大部分是 object seam 的范畴、小部分是 link seam/preprocessing seam。你就大喇喇地 24 条一起扔那儿,这样好吗?这样不好!
我大概分了一下:
- Link Seam / Preprocessing Seam: 我基本用不上,放一边
- 边缘辅助技能:这三个超简单,但又不好分类,姑且叫它边缘辅助
- “逃避可耻但有用”:逃离 dependency 但无法彻底消灭 dependency
- “我成替身了”:通过 substitution (包括 fake/mock) 消灭 dependency;分三步:
- 把 dependency 纳入 object seam 里,否则没法做替换
- 如果 dependency 的替身不好做,我们就虚化、弱化 dependency class 的结构
- 给 dependency 做替身,然后在 object seam 里替换 dependency
Link Seam / Preprocessing SeamPermalink
(C/C++)Permalink
简单说或就是用 #include <dependency.h>
中的 function declaration,但是测试用 #include <fake.c>
中的 function definition。
Permalink
通过 linking 替换 library。
(C++)Permalink
将 Target Class 改写成 template,然后 template 的 parameter 就是 seam。
- 感觉像是个 Strategy Pattern
(Interpreted Language)Permalink
有的 Interpreted Language 允许 function/class definition 的覆盖 (类似 REPL 中用一个新的 def foo()
覆盖原有的 def foo()
),所以可以用 fake definition 来测试。
边缘辅助Permalink
Permalink
封装成组的 global 的 variables/function,感觉像是减小依赖的 “散度” (从 “依赖多个 global variables/functions” 减小到 “依赖于一个 global object”)。
后续还可以接一个
Permalink
如果你依赖的 dependency call 本质是个 static,那我们就可以省去实例化 dependency class
Permalink
如果你依赖的 dependency call 已经是个 static,但是很笨重,我想要替换掉它,该怎么办?
// Target Class + Target Method
class Application {
public void doSomething() {
Service.foo(); // 笨重
}
}
// Dependency class
class Service {
public static void foo() {
// pass
}
}
注意 Java 的 static method 不能被 override,所以直接 fake/mock 是没法用的。可以考虑原地 tp,在 dependency class 里加一个 member method 来 delegate 这个 static method,然后再用 “我成替身了” 系技能来做替换。比如:
// 改写成:
class Application {
public void doSomething() {
this.service.foobar(); // 后续替换掉 service instance
}
}
class Service {
public static void foo() {
// pass
}
public void foobar() {
foo(); // 原地 tp
}
}
“逃避可耻但有用”Permalink
这几个技能适用的场景是:Target Class 的 constructor 有 dependency,但 Target Method 并不依赖那些 dependency。我们的目的就是 “把 Target Method 从 Target Class 挪到一个新的、不需要 dependency 的新 class 中”,然后测试这个新的 class。但是这个技能系的问题是:原来的 Target Class 中仍然有 dependency,你要全局测试的话还是逃不脱。
Permalink
简单说就是 M2C (Method to Class),做法是:
- 把 Target Method 整体平移到一个 New Class 里
- Target Method 参数列表不变
- Target Method 原来用到的 Target Class 的 member 平移成 New Class 的 member
- 现在只需要实例化 New Class 就能测试了
额外优点:
- 对冗长的 Target Method 是一次 refactor 的机会,比如可以考虑 “要不要再细分计算步骤”、”要不要对用到的 local variable 建 class” 之类的
- New Class 可以考虑转型成 Strategy 或者 Visitor Pattern
Permalink
比如说的 Target Method 有 10 行是有 dependency 的,我想甩掉这个 dependency,可以把这 10 行 extract 成一个新的 method,然后再做一个 Target Class 的 subclass 替换掉这个有 dependency 的 class,然后测试 subclass。举个例子:
// 原代码:
class TargetClass {
public void targetMethod() {
int i = dependency.call(); // 笨重
this.bussinessLogic(i);
}
}
// 改写成:
class TargetClass {
public void targetMethod() {
int i = this.callDependency();
this.bussinessLogic(i);
}
public int callDependency() {
return dependency.call(); // 笨重
}
}
// 真正被测试的类:
class FakeTargetClass extends TargetClass {
@Override
public int callDependency() {
return fakeValue;
}
}
/ Permalink
和
这两个技能本质是一样的,区别在于你是准备用 Target Class 的名字当实现类的名字、还是抽象类的名字,和
“我成替身了” 将 dependency 纳入 object seamPermalink
Permalink
用 factory method 作为 object seam:
// 原代码:
class Foo {
public Foo(Baz baz) {
this.bar = new Bar();
this.baz = baz;
}
}
// 改写成:
class Foo {
public Foo(Baz baz) {
this.bar = makeBar();
this.baz = baz;
}
public static Bar makeBar() {
return new Bar()
}
}
有了这个 Bar
的 factory method 之后,原代码的逻辑不变,但现在我们可以 subclass Foo
然后 override 这个 factory method 来替换掉 new Bar()
这个 dependency (比如用 Bar
的 fake/mock;甚至直接用 null,如果它不影响测试的话)。
虽然我们并没有把 Bar bar
放到参数列表里,但是这个 makeBar()
相当于就是个 object seam。
Permalink
用 lazy getter 作为 object seam:
// 原代码:
class Foo {
public Foo(Baz baz) {
this.bar = new Bar();
this.baz = baz;
}
}
// 改写成:
class Foo {
public Foo(Baz baz) {
this.bar = null;
this.baz = baz;
}
public Bar getBar() {
if (this.bar == null) {
this.bar = new Bar();
}
return this.bar
}
}
有了这个 lazy getter 之后,原代码的逻辑不变,但现在我们可以 subclass Foo
然后 override 这个 lazy getter 来替换掉 new Bar()
这个 dependency (比如用 Bar
的 fake/mock;甚至直接用 null,如果它不影响测试的话)。
虽然我们并没有把 Bar bar
放到参数列表里,但是这个 getBar()
相当于就是个 object seam。
/ Permalink
最直接的 object seam 就是参数列表。
更地道一点的话,可以保留原 signature 的 constructor/method (for back compatibility),然后新加一个 parameterized constructor/method。举例:
// 原代码:
class Foo {
public Foo(Baz baz) {
this.bar = new Bar();
this.baz = baz;
}
}
// 改写成:
class Foo {
public Foo(Baz baz) {
Bar bar = new Bar();
this(bar, baz);
}
public Foo(Bar bar, Baz baz) {
this.bar = bar;
this.baz = baz;
}
}
Permalink
如果 language 有 first-class function 的话,或者有 callable object 的话,我们可以把 dependency call 的函数体通过参数列表传给 parameterized constructor 或者 parameterized method。
Permalink
我们对 global reference (包括 singleton instance) 都是直接 access,没有经过 object seam,如果这个 global reference 是个 dependency,我们就需要把它放到 seam 里然后替换掉。我们可以用一个 getter 来做这个 seam:
Global G = new Global();
// 原代码:
import static com.global.G;
class Foo {
public foo() {
G.doSomething();
}
}
// 改写成:
import static com.global.G;
class Foo {
public foo() {
this.getGlobal().doSomething();
}
public Global getGlobal()) {
return G;
}
}
Permalink
“supersede” 可以简单理解成 “replace”,但这个 “super-“ 前缀暗含了一层 “A supersedes B because A is superior” 的意思,有一点中文 “取代” 的意思。
这是个万不得已才使用的技能:
- 如果我的 dependency class 无法 subclass 怎么办?(比如
final class
) - 如果我的 dependency method 无法 override 怎么办?(比如有的 language 不允许 override 被 constructor 调用的 virtual function)
迫不得已的话只能:
- 在 Target Class 内忍痛做 dependency class 的实例化 (不可避)
- 但是我可以给 Target Class 加一个 setter 在 dependency class 的实例化之后替换掉 dependency instance
这个 setter 就是 dependency instance 的 object seam。
“我成替身了” 虚化、弱化 dependency classPermalink
虚化、弱化 dependency class 是为了方便我们更好地做替身 (fake/mock):
- 虚化就是提取 dependency 的 interface/abstract class,然后通过 implementation/subclass 来做替身
- 弱化是用一个新的 simple dependency class 来代替原来的 complex dependency class,降低做替身的工作量
/ Permalink
针对很难实例化的 parameter 提取一个 interface 便于我们创建 fake/mock…… (这么简单的技术这需要一个新建一个概念吗?)
至于这两个技能的区别,举个例子:
是从class Bar
出发,得到interface IBar
+class Bar
是从class Bar
出发,得到interface Bar
+class BarImpl
- 决定是使用哪个技能完全取决于你起始的这个
Bar
是适合做类名还是接口名
吐槽:
- 就这么个简单的破玩意儿还用得着专门写好几页?!
- 我就姑且认为这俩是 interchangeable 了
Permalink
书上的例子:
// 原代码:
public class Dispatcher {
public void populate(HttpServletRequest request) {
String [] values = request.getParameterValues(this.pageStateName);
if (values != null && values.length > 0) {
String value = values[0];
}
// pass
}
}
假设 HttpServletRequest
是一个烦人的 dependency,且假设我们只使用了这么一小段,那么我们可以给 HttpServletRequest
写一个 adapter,使得 Target Class Dispatcher
只依赖于这个 adapter 而不直接依赖 HttpServletRequest
:
// 新添代码:
interface ParameterSource {
String getByName(String name)
}
class FakeParameterSource implements ParameterSource {
private String value;
public String getByName(String name) {
return value;
}
}
class ServletParameterSource implements ParameterSource {
private HttpServletRequest request;
public String getByName(String name) {
String [] values = request.getParameterValues(name);
if (values != null && values.length > 0) {
return values[0];
}
return null;
}
}
注意要修改原来的方法签名:
// 改写成:
public class Dispatcher {
public void populate(ParameterSource source) {
String value = source.getByName(this.pageStateName)
// pass
}
}
这个改动就是把 HttpServletRequest
这个 dependency 弱化成了 ParameterSource
。
Permalink
中文版在 P14 的脚注特别强调了一下翻译,”primitivize” 不是说 “要转换成 primitive type” 的意思。译者翻译成了 “朴素化”,因为他觉得 “简化” 这个词又太模糊了,我表示同意。
那啥叫 “朴素化”?比方说 parameter 是一个复杂的 class,有 10 个 attributes,但其实我 Target Method 只用了其中 3 个,那我可以就用这 3 个 attributes 做一个新的中间层 class,然后让 Target Method 改成依赖这个中间层 class。代码举例:
// 原代码:
class TargetClass {
public void targetMethod(Parameter p) {
// uses p.a, p.b, p.c
}
}
class Parameter {
// pass
}
// 新添代码:
class PrimitivizedParameter {
public PrimitivizedParameter(Parameter p) {
this.a = p.a;
this.b = p.b;
this.c = p.c;
}
}
// 改写成:
class TargetClass {
public void targetMethod(Parameter p) {
PrimitivizedParameter pp = p.primitivize();
targetMethod(pp);
}
public void targetMethod(PrimitivizedParameter pp) {
// uses pp.a, pp.b, pp.c
}
}
class Parameter {
public PrimitivizedParameter primitivize() {
return new PrimitivizedParameter(this);
}
}
这个改动就是把 Parameter
这个 dependency 弱化成了 PrimitivizedParameter
。
普通技
“我成替身了” 在 object seam 里替换 dependencyPermalink
Permalink
可以宽泛地认为 fake/mock 用的就是这个技能。
对 dependency 做 subclass 然后 override 复杂的部分,用这个 subclass 作为 dependency 的替身。
如果有对 dependency 做
Permalink
注意这个技能和
是在 Target Class 中加 setter 是在 singleton dependency class 中加 static setter
这个技能起源于一个想法:在 singleton dependency class 中用 static setter 直接替换掉 singleton instance。比如:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// 原始想法:
// 新添加的代码:
public static void setTestingInstance(Singleton newInstance) {
instance = newInstance;
}
}
但这个想法有个问题:你这个 newInstance
应该是什么类型?
- 首先这里是个 singleton,是不应该有多个 instance 的,所以
newInstance
不可能是Singleton
类型 - 那
newInstance
就只能是Singleton
的子类,但这又引入了新的问题:- 如果
Singleton
不允许继承呢? - 如果
Singleton
允许继承,那我为什么不直接用 呢?
- 如果
为了继续这个想法,我们可以再结合一个
// 原代码:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 改写成:
public class Singleton implements ISingleton {
private static Singleton instance = null;
private Singleton() {}
public static ISingleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// 新添加的代码:
public static void setTestingInstance(ISingleton newInstance) {
instance = newInstance;
}
}
然后我就可以这样来做 fake/mock:
class FakeSingleton implements ISingleton {
// pass
}
Singleton.setTestingInstance(new FakeSingleton());
Part II - Changing SoftwarePermalink
Chapter 6 - 旧代码没有 test,我也没时间写,但现在我需要修改它Permalink
假设只针对 OO 的情况。你有一个 TargetClass
,需要修改其中的 targetMethod()
,添加一段新的代码逻辑:
TargetClass
没有 test case,我们当前对原来的 method 也不打算测试- 但是我们对新添加的代码逻辑想做测试
(新生方法)Permalink
将新逻辑添加到一个全新的 TargetClass.newMethod()
里,只在 TargetClass.targetMethod()
里添加一个调用,最大程度减小 intrusion (侵入量)。后续 TargetClass.targetMethod()
和 TargetClass.newMethod()
可以独立测试:
class TargetClass {
public void targetMethod() {
// original logic
}
}
// 改写成:
class TargetClass {
public void targetMethod() {
this.newMethod();
// original logic
}
}
(新生类)Permalink
如果我们用了 TargetClass.newMethod()
,但是问题是:如果 TargetClass
很难创建 instance 该怎么办?(我们也不打算用 mock 啥的)
我们就把代码 wrap 到 NewClass.targetMethod()
里,然后只测试 NewClass
就好了,就这么简单 (TargetClass
保持不动)。
class TargetClass implements ITarget {
public void targetMethod() {
// original logic
}
}
// 改写成:
class TargetClass implements ITarget {
public void targetMethod() {
// original logic
}
}
class NewClass implements ITarget {
public void targetMethod() {
// new logic
}
}
改完后,我们还需要修改原来 TargetClass
的 caller,可能的情况有:
- 用
NewClass
完全代替TargetClass
(即将NewClass
看做TargetClass
的 decorator; 参见 Middleman Patterns: Adapter / Proxy / Decorator) - 持有一个
ITarget
的 collection
然后作者又啰哩吧嗦说了一大段,无非是说这种做法可以引发你对 design 的思考:新的代码逻辑是不是应该独立于 TargetClass
?有两个类是不是更好?要不要提取公共 interface?
如果逻辑上没有必要设计成两个类,那 Sprout Class 这个方法就会带来它最大的缺点:对 Class Hierarchy 的严重破坏。
另外还有一个问题我很好奇:你 TargetClass
的 caller 改动了,如何测试 caller?书上可没说……
(外覆方法)Permalink
将 TargetClass.targetMethod()
的逻辑转移到 TargetClass.foo()
,然后添加新的逻辑到 sprout method TargetClass.bar()
,重新构建 TargetClass.targetMethod()
:
class TargetClass {
private void foo() {
// original logic
}
private void bar() {
// new logic
}
public void targetMethod() {
this.foo()
this.bar()
}
}
- I usually use
when the code I have in the existing method communicates a clear algorithm to the reader. - I move toward
when I think that the new feature I’m adding is as important as the work that was there before.- In that case, after I’ve wrapped, I often end up with a new high-level algorithm (instead of a long text of instructions).
(外覆类)Permalink
和 NewClass
是 extends TargetClass
或者组合一个 TargetClass
instance,然后 overwrite targetMethod()
。本质还是一个 decorator pattern。
Chapter 7 - It Takes Forever to Make a Change (略)Permalink
我完全不知道为什么要有这么一章?为什么是这个标题?为什么要放到这个位置?我就简单把本章看做是后续章节的一个指路环节好了。略。
Chapter 8 - How Do I Add a Feature? (可以略)Permalink
TDDPermalink
8.1 是一个 TDD 的示例 walk-through
Programming by DifferencePermalink
8.2 给了一个 Programming by Difference 的示例 walk-through,但是又没有说啥叫 Programming by Difference…… (MD,我发现这种写文章的鬼手法,OO 界也是重灾区,就是喜欢搞描述性的概念,w/o definition at all)
所谓 Programming by Difference 就是:
In defining derived classes, we only need to specify what’s different about them from their base classes.
在本章的 context 下,它意味着将新 feature 完全反映在子类与父类的差异上。(这不是理所当然的吗?还需要新建个概念?)
然后书上提了一句注意考虑 LSP,这一点倒是很 reasonable。
Chapter 9 - 实例化 Target Class 会遇到的困难Permalink
Case 1: constructor 需要的 parameter 很难实例化Permalink
可以使用的技能:
- null枪打鸟
- 试试 null 值?
- 试试 null object?(参考 PPP chapter 17. Null Object 模式)
“我成替身了” 技能系都可以用
Case 2: constructor 中直接 new 了一个很难实例化的 object (书上称之为 Hidden Dependency)Permalink
其实最常见的 Hidden Dependency 就是就是在 constructor 中直接 new 其他的 object,而不是把这个 object 创建好再传入 constructor。
所以首先应该把这个 dependency object 放到 Target Class 的 object seam 里,再考虑替换。
Case 3: constructor 中直接 new 了一个 “onion object”Permalink
所谓 “onion object” 是指多层 “object inside object” 的情况,就像一个洋葱,可以一直剥一直剥。
基本可以看做是 Case 2 但不方便使用
class Foo {
public Foo() {
this.bar = new Bar();
this.baz = new Baz(this.bar);
this.qux = new Qux(this.baz);
this.quux = new Quux(this.qux); // 笨重
}
}
这里 quux
就是个 onion-object。考虑 quux
很不好实例化的情况,你会发现:
- 只 parameterize
bar
和baz
并不能解决问题 - 全员 parameterize
bar
、baz
、qux
和quux
的话:- 参数列表过长
- construction 的难度增大
此时只能:
- 要么
- 要么
Case 4: constructors、member methods、static methods 都依赖于一个笨重的 Singleton InstancePermalink
注意这里的 scenario:
- 书上有用 “global variable” 这个词,但这里肯定不是简单的 int、string 全局变量,它们很好处理;书上这里指的是一个笨重的 global instance,最常见的就是 singleton
- 然后这个 “笨重” 体现在:a) 要么它很难实例化;b) 要么它的 behavior 很耗时 (比如访问 DB 或是网络通信)
- 书上还用了 “extensive” 这个词,但这里不是说我会依赖很多个 global instances,而是说 constructors、member methods、static methods 都会依赖某一个 global instance
“extensive” 带来的问题:
- 如果只是 constructors 或 member methods 依赖这个 global instance,我们可以
- 如果只有 static methods 依赖这个 global instance,我们可以
- 但现在是 constructors、member methods、static methods 全都依赖这个 global instance,你总不能把它们全都 parameterize 了吧?(影响可读性、可维护性、封装 etc)
举个例子就知道 parameterize 技术对这个 scenario 的不足:
// 原代码:
class Foo {
public Foo() {
// ...
Singleton.getInstance().initialize(); // 笨重
}
public void bar(int i) {
// ...
Singleton.getInstance().doSomething(i); // 笨重
}
public static void baz(int j) {
// ...
Singleton.getInstance().doSomethingElse(j); // 笨重
}
}
// 不适合的 parameterize:
class Foo {
public Foo(ISingleton singleton) { // parameterize constructor
// ...
this.singleton = singleton
}
public void bar(int i) {
// ...
this.singleton.doSomething(i);
}
public static void baz(int j, ISingleton singleton) { // parameterize method
// ...
singleton.doSomethingElse(j);
}
}
解决方法:
- 如果 Singleton 允许继承,用
- 否则就用
Case 5: Horrible #include
Dependency (略)Permalink
Case 6: constructor 有一个 “onion-parameter”Permalink
注意和 Case 3 区分。现在的 scenario 是:
class Foo {
public Foo(Bar bar) {
// pass
}
}
class Bar {
public Bar(Baz baz) {
// pass
}
}
class Baz {
public Baz(Qux qux) {
// pass
}
}
class Qux {
public Qux(Quux quux) {
// pass
}
}
// pass
我现在要测试 Foo
,然后发现 quux
、Qux
、Baz
、Bar
全都要 new 一个,其中任何一个不方便实例化的话,Foo
就不方便实例化。
解决方法:
- null枪打鸟
Case 7: constructor 的 parameter 不适合用 Permalink
主要出现在 parameter 位于 class hierarchy 底端时。比如 constructor 需要一个 Bar4
,但是它的 hierarchy 是这样的:
Bar <- Bar1 <- Bar2 <- Bar3 <- Bar4
若是为 Bar4
做一个 interface,最终的效果可能是:
IBar
^
Bar <- Bar1 <- Bar2 <- Bar3 <- Bar4
或者:
IBar <- IBar1 <- IBar2 <- IBar3 <- IBar4
^ ^ ^ ^ ^
Bar <- Bar1 <- Bar2 <- Bar3 <- Bar4
波及的范围有点大……所以此时还是直接用
Chapter 10 - 测试 Target Method 会遇到的困难Permalink
Quest 0: 不实例化 Target Class 来测试 Target MethodPermalink
Quest 1: 如何测试 private method?Permalink
首先考虑改成 public 是否合适 (是个人就能想到;但一般我是不会这么搞的)。不行的话只能改成 protected 或 package 权限,然后考虑用
我个人的意见是:private method 应该是开发人员自己 unit test 要保证的内容。
Quest 2: Target Method 需要的 parameter 很难初始化Permalink
其实 Chapter 9 - Case 1 的技术都能用。
这里讨论一种额外的 scenario,即 parameter 的类型是一个 framework 的 class:
- 可能有容器负责了它的实例化,所以它自己干脆没有 constructor
- 它已经有 interface 了,但你 fake/mock 这个 interface 会很麻烦 (比如说它是一个非常多方法的 interface)
此时常用的技术是有下面两个。
Permalink
如果你只是需要这个 parameter 的两个 field,我们完全可以 overload 这个 Target Method,比如:
// 原代码:
class Foo {
public void foo(Bar bar) {
// uses bar.a and bar.b
}
}
// 改写成:
class Foo {
public void foo(Bar bar) {
this.foo(bar.getA(), bar.getB());
}
public void foo(int a, int b) {
// uses a and b
}
}
(略)Permalink
Quest 3: 如何测试 Side Effect?Permalink
Permalink
这个问题其实很重要。因为:
- 如果是 Target Method 自己 直接 造成的 Side Effect,那作为开发者你自己应该清楚如何测试
- 如果我 Target Class 是一个 JDBC,Target Method 是 insert 了一条 record,我作为 JDBC 的开发者我肯定知道这个要怎么测
- 如果是 Target Method 新建了一个 local variable,然后是这个 local variable 造成的 Side Effect,那么我应该考虑 把这个 local variable 提升为 Target Class 的 member
- 比如我 Target Class 是一个 DAO,Target Method 自己创建了一个 JDBC connection,然后 insert 了一条 record
- 那么 DAO 的 Unit Test 是无法 access 这个 JDBC connection 的,也就无法测试它
- 如果你 DAO 是持有这个 JDBC connection,别的不说,DAO 的 Unit Test 至少能 fake/mock 这个 JDBC connection
- 题外话:我觉得这应该叫 Testability-Oriented Design,隶属 defensive programming 的范畴
Permalink
Command/Query Separation is a design principle first described by Bertrand Meyer.
- A
is a method that can modify the state of the object but that doesn’t return a value. - A
is a method that returns a value but that does not modify the object. 应该是 idempotent 的
CQS: A method should be a
这个 principle 没法做法绝对哈,比如我有一个 lookup 的方法:
class DAO {
privaite DBConnection conn;
private RecordCache cache;
public Record lookup(Table table, Field field, int i) {
// this is a command
this.updateLookupCounter(table, field, i);
// this is a query
Record r = this.cache.get(table, field, i);
if (r != null) {
return r;
}
// this is a query
Record r = this.conn.select(table, field, i);
// this is a command
this.cache.put(table, field, i, r);
return r;
}
}
你无论怎么 separation,这个 lookup method 始终是一个
- 这么做也能保持你 extract 出来的
和 methods 处于相同的抽象层次
细分之后,有助于我们确定 是谁引入的 Side Effect?,然后我们就能用上一小节的方法来测试。
Chapter 11 - 修改时应该测试哪些 methods? (可以略)Permalink
还能有啥?这值得专门写一章?
你修改了一个 method,那么这个 method 的所有 caller 你都要检查;如果你这个 method 是父类的一个 method,那么所有的子类你都要检查。复杂的情况就画调用图呗,画 class hierarchy 呗,还能有啥招数?
Chapter 12 - 续•修改时应该测试哪些 methods?Permalink
这一章比上一章有用。
Interception Point / Pinch PointPermalink
假设现在我做了一个修改,不管我是画了调用图还是 class hierarchy,这个图都可以看做 “change’s effect” 传播的一个路径图,书上叫 “effect sketch”。
- change point 就是你做出修改的地方,相当于是 effect sketch 的 source node
- interception point 就是指 effect sketch 上的一个 node, where 你决定要测试这个 effect 的正确性
- 有的时候并不是你修改了哪个 method 就能测试哪个 method 的,比方说你修改了一个 private method,此时离 change point 最近的一个 interception point 应该是 class 内调用了这个 private method 的一个 public method
有时候不见得离 change point 最近的 interception point 就是最优的选择,比如说我现在要修改 5 个离散的 classes 的 5 个 public methods,然后我又没有时间去处理这 5 个 classes 各自的 dependency,但是我发现这 5 个 public method 有一个共同的 caller,那我可以选择把 interception point (从 change point 前线) 战术撤退到这个 caller。
- pinch point 相当于是 effect sketch 中的 critical point,在 pinch point 拦截可以一次测试多个改动
- 但是同时要注意,太后方的 pinch point 的测试可能会很笨重
Chapter 13 - How to Write Tests (略)Permalink
Chapter 14 - Dependencies on Libraries (略)Permalink
这一章,是一章哦,只有 1.5 pages,我都不知为作者为什么要这么写。然后这 1.5 pages 我还没看懂他要表达啥意思……
Chapter 15 - 如何重构充斥着 API Calls 的代码 (可以略)Permalink
其实也没啥,说的都是业界 best practice。
啥 “Wrap the API”,不就是写 DAO 嘛,通过 DAO 暴露限定的 high-level API,隐藏 library 的 low-level API。
啥 “Responsibility-Based Extraction”,不就是 “模块化”、”封装” 嘛。
我都懒得吐槽了。
Chapter 16 - 如何读代码 (没啥内容)Permalink
- 画图
- 代码段 (比如一个很长的方法) tagging
- 草稿式重构
- 删掉无用代码
Chapter 17 - 如何读懂架构 (没啥内容)Permalink
Chapter 18 - 测试类咋命名 / 测试代码放哪儿 (略)Permalink
这一章为什么不和 Chapter 13 放一起?
Chapter 19 - 如何修改 non-OO 的代码 (略)Permalink
一个利用 Chapter 4 的 Preprocessing Seam 和 Link Seam 的 C/C++ 例子的 walk-through
Chapter 20 - 如何重构一个 big class (可以略)Permalink
Chapter 21 - 如何重构大量重复的代码 (可以略)Permalink
Chapter 22 - 如何重构一个 monster methodPermalink
extract 小方法时注意:
- 尽量将隔离出的局部逻辑与 monster method 整体的 dependency 分离
- 引入 seam 以方便测试
Coupling CountPermalink
代码段的逻辑复杂程度的一个度量:coupling count (耦合数),即传入值的数量加上传出值的数量。比如:
int maximum = max(a, b); // coupling count == 3
Record r = db.select(table, field, i); // coupling count == 4
db.insert(r); // cooupling count == 1
- coupling count 越小的代码段,extraction 越安全
- 如果一段代码的 coupling count 是 0,那么它就是一个不接收参数的
- 如果一段代码的 coupling count 是 0,那么它就是一个不接收参数的
Comments