### Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
兩個鄉之間有雙向關聯,但其中一個class如今不再需要另一個class的特性。
去除不必要的關聯(association)。

**動機(Motivation)**
雙向關聯(bidirectional associations)很有用,但你也必須為它付出代價,那就是「維護雙向連接、確保對象被正確創建和刪除」而增加的復雜度。而且,由于很多程序 員并不習慣使用雙向關聯,它往往成為錯誤之源。
大量的雙向連接(two-way links)也很容易引發「僵尸對象」:某個對象本來已經該死亡了,卻仍然保留在系統中,因為對它的各項引用還沒有完全清除。
此外,雙向關聯也迫使兩個classes之間有了相依性。對其中任一個class的任何修 改,都可能引發另一個class的變化。如果這兩個classes處在不同的package中, 這種相依性就是packages之間的相依。過多的依存性(inter-dependencies)會造就緊耦合(highly coupled)系統,使得任何一點小小改動都可能造成許多無法預知的后果。
只有在你需要雙向關聯的時候,才應該使用它。如果你發現雙向關聯不再有存在價值,就應該去掉其中不必要的一條關聯。
**作法(Mechanics)**
- 找出『你想去除的指針」的保存值域,檢查它的每一個用戶,判斷是否可以去除該指針。
- 不但要檢查「直接讀取點」,也要檢查「直接讀取點」的調用函數。
- 考慮有無可能不通過指針取得「被引用對象」(referred object)。如 果有可能,你就可以對取值函數(getter)使用Substitute Algorithm,從而讓客戶在沒有指針的情況下也可以使用該取值函數。
- 對于使用該值域的所有函數,考慮將「被引用對象」(refered object)作為引數(argument)傳進去。
- 如果客戶使用了取值函數(getter),先運用Self Encapsulate Field 將「待除值域」自我封裝起來,然后使用Substitute Algorithm 對付取值函數,令它不再使用該(待除)值域。然后編譯、測試。
- 如果客戶并未使用取值函數(getter),那就直接修改「待除值域」的所有被引用點:改以其他途徑獲得該值域所保存的對象。每次修改后,編譯并測試。
- 如果已經沒有任何函數使用該(待除〕值域,移除所有「對該值域的更新邏輯」,然后移除該值域。
- 如果有許多地方對此值域賦值,先運用Self Encapsulate Field 使這些地點改用同一個設值函數(setter)。編譯、測試。而后將這個 設值函數的本體清空。再編譯、再測試。如果這些都可行,就可以將此值域和其設值函數,連同對設值函數的所有調用,全部移除。
- 編譯,測試。
**范例(Example)**
本例從 Change Unidirectional Association to Bidirectional 留下的代碼開始進行,其中Customer和Order之間有雙向關聯:
~~~
class Order...
Customer getCustomer() {
return _customer;
}
void setCustomer (Customer arg) {
if (_customer != null) _customer.friendOrders().remove(this);
_customer = arg;
if (_customer != null) _customer.friendOrders().add(this);
}
private Customer _customer; //譯注:這是Order-to-Customer link也是本例的移除對象
class Customer...
void addOrder(Order arg) {
arg.setCustomer(this);
}
private Set _orders = new HashSet();
//譯注:以上是Customer-to-Order link
Set friendOrders() {
/** should only be used by Order */
return _orders;
}
~~~
后來我發現,除非先有Customer對象,否則不會存在Order對象。因此我想將「從Order 到Customer的連接」移除掉。
對于本項重構來說,最困難的就是檢查可行性。如果我知道本項重構是安全的,那么重構手法自身十分簡單。問題在于是否有任何代碼倚賴_customer 值域的存在。 如果確實有,那么在刪除這個值域之后,我必須提供替代品。
首先,我需要研究所有讀取這個值域的函數,以及所有使用這些函數的函數。我能找到另一條途徑來供應Customer對象嗎——這通常意味將Customer對象作為引數(argument)傳遞給其用戶〔某函數〕。下面是一個簡化例子:
~~~
class Order...
double getDiscountedPrice() {
return getGrossPrice() * (1 - _customer.getDiscount());
}
~~~
改變為:
~~~
class Order...
double getDiscountedPrice(Customer customer) {
return getGrossPrice() * (1 - customer.getDiscount());
}
~~~
如果待改函數是被Customer對象調用的,那么這樣的修改方案特別容易實施,因為Customer對象將自己作為引數(argument)傳給函數很是容易。所以下列代碼:
~~~
class Customer...
double getPriceFor(Order order) {
Assert.isTrue(_orders.contains(order)); // see Introduce Assertion (267)
return order.getDiscountedPrice();
~~~
變成了:
~~~
class Customer...
double getPriceFor(Order order) {
Assert.isTrue(_orders.contains(order));
return order.getDiscountedPrice(this);
}
~~~
另一種作法就是修改取值函數(getter),使其在不使用_customer 值域的前提下返回一個Customer對象。如果這行得通,我就可以使用Substitute Algorithm 修改Order.getCustomer()函數算法。我有可能這樣修改代碼:
~~~
Customer getCustomer() {
Iterator iter = Customer.getInstances().iterator();
while (iter.hasNext()) {
Customer each = (Customer)iter.next();
if (each.containsOrder(this)) return each;
}
return null;
}
~~~
這段代碼比較慢,不過確實可行。而且,在數據庫環境下,如果我需要使用數據庫查詢語句,這段代碼對系統性能的影響可能并不顯著。如果Order class中有些函 數使用_customer值域,我可以實施Self Encapsulate Field 令它們轉而改 用上述的getCustomer() 函數。
如果我要保留上述的取值函數(getter),那么Order和Customer的關聯從接口上看雖然仍是雙向,但實現上已經是單向關系了。雖然我移除了反向指針,但兩個classes彼此之間的依存關系(inter-dependencies)仍然存在。
如果我要替換取值函數(getter),那么我就專注地替換它,其他部分留待以后處理。 我會逐一修改取值函數的調用者,讓它們通過其他來源取得Customer對象。每次修改后都編譯并測試。實際工作中這一過程往往相當快。如果這個過程讓我覺得很棘手很復雜,我會放棄本項重構。
一旦我消除了_customer值域的所有讀取點,我就可以著手處理「對此值域進行賦值動作」的函數了(譯注:亦即設值函數,setter)。很簡單,只要把這些賦值動作 全部移除,再把值域一并刪除,就行了。由于已經沒有任何代碼需要這個值域,所以刪掉它并不會帶來任何影響。
- 譯序 by 侯捷
- 譯序 by 熊節
- 序言
- 前言
- 章節一 重構,第一個案例
- 起點
- 重構的第一步
- 分解并重組statement()
- 運用多態(Polymorphism)取代與價格相關的條件邏輯
- 結語
- 章節二 重構原則
- 何謂重構
- 為何重構
- 「重構」助你找到臭蟲(bugs)
- 何時重構
- 怎么對經理說?
- 重構的難題
- 重構與設計
- 重構與性能(Performance)
- 重構起源何處?
- 章節三 代碼的壞味道
- Duplicated Code(重復的代碼)
- Long Method(過長函數)
- Large Class(過大類)
- Long Parameter List(過長參數列)
- Divergent Change(發散式變化)
- Shotgun Surgery(散彈式修改)
- Feature Envy(依戀情結)
- Data Clumps(數據泥團)
- Primitive Obsession(基本型別偏執)
- Switch Statements(switch驚悚現身)
- Parallel Inheritance Hierarchies(平行繼承體系)
- Lazy Class(冗贅類)
- Speculative Generality(夸夸其談未來性)
- Temporary Field(令人迷惑的暫時值域)
- Message Chains(過度耦合的消息鏈)
- Middle Man(中間轉手人)
- Inappropriate Intimacy(狎昵關系)
- Alternative Classes with Different Interfaces(異曲同工的類)
- Incomplete Library Class(不完美的程序庫類)
- Data Class(純稚的數據類)
- Refused Bequest(被拒絕的遺贈)
- Comments(過多的注釋)
- 章節四 構筑測試體系
- 自我測試代碼的價值
- JUnit測試框架
- 添加更多測試
- 章節五 重構名錄
- 重構的記錄格式
- 尋找引用點
- 這些重構準則有多成熟
- 章節六 重新組織你的函數
- Extract Method(提煉函數)
- Inline Method(將函數內聯化)
- Inline Temp(將臨時變量內聯化)
- Replace Temp with Query(以查詢取代臨時變量)
- Introduce Explaining Variable(引入解釋性變量)
- Split Temporary Variable(剖解臨時變量)
- Remove Assignments to Parameters(移除對參數的賦值動作)
- Replace Method with Method Object(以函數對象取代函數)
- Substitute Algorithm(替換你的算法)
- 章節七 在對象之間搬移特性
- Move Method(搬移函數)
- Move Field(搬移值域)
- Extract Class(提煉類)
- Inline Class(將類內聯化)
- Hide Delegate(隱藏「委托關系」)
- Remove Middle Man(移除中間人)
- Introduce Foreign Method(引入外加函數)
- Introduce Local Extension(引入本地擴展)
- 章節八 重新組織數據
- Self Encapsulate Field(自封裝值域)
- Replace Data Value with Object(以對象取代數據值)
- Change Value to Reference(將實值對象改為引用對象)
- Replace Array with Object(以對象取代數組)
- Replace Array with Object(以對象取代數組)
- Duplicate Observed Data(復制「被監視數據」)
- Change Unidirectional Association to Bidirectional(將單向關聯改為雙向)
- Change Bidirectional Association to Unidirectional(將雙向關聯改為單向)
- Replace Magic Number with Symbolic Constant(以符號常量/字面常量取代魔法數)
- Encapsulate Field(封裝值域)
- Encapsulate Collection(封裝群集)
- Replace Record with Data Class(以數據類取代記錄)
- Replace Type Code with Class(以類取代型別碼)
- Replace Type Code with Subclasses(以子類取代型別碼)
- Replace Type Code with State/Strategy(以State/strategy 取代型別碼)
- Replace Subclass with Fields(以值域取代子類)
- 章節九 簡化條件表達式
- Decompose Conditional(分解條件式)
- Consolidate Conditional Expression(合并條件式)
- Consolidate Duplicate Conditional Fragments(合并重復的條件片段)
- Remove Control Flag(移除控制標記)
- Replace Nested Conditional with Guard Clauses(以衛語句取代嵌套條件式)
- Replace Conditional with Polymorphism(以多態取代條件式)
- Introduce Null Object(引入Null 對象)
- Introduce Assertion(引入斷言)
- 章節十一 處理概括關系
- Pull Up Field(值域上移)
- Pull Up Method(函數上移)
- Pull Up Constructor Body(構造函數本體上移)
- Push Down Method(函數下移)
- Push Down Field(值域下移)
- Extract Subclass(提煉子類)
- Extract Superclass(提煉超類)
- Extract Interface(提煉接口)
- Collapse Hierarchy(折疊繼承關系)
- Form Template Method(塑造模板函數)
- Replace Inheritance with Delegation(以委托取代繼承)
- Replace Delegation with Inheritance(以繼承取代委托)
- 章節十二 大型重構
- 這場游戲的本質
- Tease Apart Inheritance(梳理并分解繼承體系)
- Convert Procedural Design to Objects(將過程化設計轉化為對象設計)
- Separate Domain from Presentation(將領域和表述/顯示分離)
- Extract Hierarchy(提煉繼承體系)
- 章節十三 重構,復用與現實
- 現實的檢驗
- 為什么開發者不愿意重構他們的程序?
- 現實的檢驗(再論)
- 重構的資源和參考資料
- 從重構聯想到軟件復用和技術傳播
- 結語
- 參考文獻
- 章節十四 重構工具
- 使用工具進行重構
- 重構工具的技術標準(Technical Criteria )
- 重構工具的實用標準(Practical Criteria )
- 小結
- 章節十五 集成
- 參考書目