章節概述#
本章引入 Value Object 模式,並為 Dollar 實作 equals() 方法。作者藉此示範 TDD 的第三種實作策略——Triangulation(三角測量),以及如何從設計模式推導出需要的測試。
Value Object 模式#
整數的特性是:對它加 1,原始值不會改變,你使用的是新值。而一般物件不是這樣——修改合約的保障範圍,合約本身就改變了。
我們的 Dollar 正在被當作 Value Object 使用。Value Object 的約束是:實例變數一旦在建構子中設定,就永遠不會改變。
Value Object 最大的好處是不用擔心 aliasing(別名)問題。如果我持有一個 $5 物件,我可以保證它永遠是 $5。如果需要 $7,必須建立一個全新的物件。
補充: Aliasing 是作者職涯中最惡劣的 bug 來源之一——改變一張支票的金額,卻不小心連帶改變了另一張支票的金額,因為它們指向同一個物件。
Value Object 的兩個影響#
Value Object 模式帶來兩個必然要求:
- 所有操作必須回傳新物件——Chapter 2 已經完成
- 必須實作
equals()——因為一個 $5 跟另一個 $5 應該是相等的
此外,如果要把 Dollar 當作 hash table 的 key,還需要實作 hashCode()。作者將此記到待辦清單,等到真正需要時再處理。
測試 equals()#
先測試最基本的情況——$5 應該等於 $5:
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
}紅燈亮起。最快讓它通過的假實作:
public boolean equals(Object object) {
return true;
}補充:
true其實是5 == 5,也就是amount == 5,也就是amount == dollar.amount。作者刻意不立刻泛化,而是要示範第三種策略——Triangulation。
Triangulation(三角測量)策略#
Triangulation 的概念來自無線電測向:如果兩個已知位置的接收站都能測量到信號的方向,就有足夠資訊計算出信號的距離和方位。
類比到 TDD:只有在擁有兩個以上的例子時,才泛化程式碼。在只有一個例子時,我們暫時忽略測試與 model 之間的重複。
所以我們需要第二個例子——$5 不等於 $6:
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
}現在必須泛化 equals() 了:
public boolean equals(Object object) {
Dollar dollar = (Dollar) object;
return amount == dollar.amount;
}測試通過。
重點: Triangulation 是三種策略中最保守的一種。作者坦言自己只在完全不確定如何重構時才使用它。如果能直接看到如何消除重複並得出泛化解法,他就直接做。但當設計靈感枯竭時,Triangulation 提供了一個從不同角度思考問題的機會——你試圖支援哪些變異軸?讓其中一些變化,答案可能就會變得清晰。
待辦清單更新#
equals() 暫時完成了,但還有兩個相關情境尚未處理:
- 與
null比較 - 與其他類型的物件比較
這些是常見操作,但目前還不是必須的,所以先加到待辦清單。
現在有了 equality,就可以在下一章將 amount 設為 private——這是好的實例變數該有的樣子。
本章小結#
本章的要點回顧:
- 注意到設計模式(Value Object)隱含了一個操作需求(equals)
- 為該操作撰寫測試
- 用最簡單的方式實作它
- 沒有立刻重構,而是先撰寫更多測試
- 泛化程式碼以同時涵蓋兩個測試案例(Triangulation)
- 介紹了第三種策略:Triangulation——在有兩個以上例子時才泛化