天天看點

Change Bidirectional Association to Unidirectional(将雙向關聯改為單向)

兩個classes之間有雙向關聯,但其中一個class如今不再需要另一個class的特性.

去除不必要的關聯(association).

動機

雙向關聯(bidirectional associations)很有用,但你也必須為它付出代價,那就是[維護雙向連結,確定對象被正确建立和删除]而增加的複雜度.而且,由于很多程式員并不習慣使用雙向關聯,它往往成為錯誤之源.

大量的雙向連接配接(two-way links)也很容易引發[僵屍對象]:某個對象本來已經該死亡了,卻仍然保留在系統中,因為對它的各項引用還沒有完全清除.

此外,雙向關聯也迫使兩個classes之間有了相依性.對其中任一個class的任何修改,都可能引發另一個class的變化.如果這兩個classes處在不同的package中,這種相依性就是packages之間的相依.過多的依存性(inter-dependencies)會造成就緊耦合(highly coupled)系統,使得任何一點小小改動都可能造成許多無法預知的後果.

隻有在你需要雙向關聯的時候,才應該使用它.如果你發現雙向關聯不再有存在價值,就應該去掉其中不必要的一條關聯.

作法

1. 找出[你想去除的指針]的儲存值域,檢查它的每一個使用者,判斷是否可以去除該指針.

不但要檢查[直接讀取點],也要檢查[直接讀取點]的調用函數.

考慮有無可能不通過指針函數取得[被引用對象](referred object).如果有可能,你就可以對取值函數(getter)使用Substitute Algorithm(139).進而讓客戶在沒有指針的情況下也可以使用該取值函數.

對于使用該值域的所有函數,考慮将[被引用對象](referred object)作為引數(argument)傳進去.

2. 如果客戶使用了取值函數(getter),先運用Self Encapsulate Field(171)将[待除值域]自我封裝起來,然後使用Subsitute Algorithm(139)對付取值函數,令它不再使用該(待除)值域.然後編譯,測試.

3. 如果客戶并未使用取值函數(getter),那就直接修改[待除值域]的所有被引用點:改以其他途徑獲得該值域所儲存的對象.每次修改後,編譯并測試.

4. 如果已經沒有任何函數使用該(待除)值域,移除所有[對該值域的更新邏輯],然後移除該值域.

如果有許多地方對此值域指派,先運用Self Encapsulate Field(171)使這些地點改用同一個設值函數(setter).編譯,測試.而後将這個設值函數的本體清空.再編譯,再測試.如果這些都可行,就可以将此值域和其設值函數,連同對設值函數的所有調用,全部移除.

5. 編譯,測試.

本例從Change Unidirectional association to Bidirectional(197)留下的代碼開始進行,其中Customer和Order之間有雙向關聯:

class Order...

   Customer getCustomer() {

      return _customer;

   }

   void setCustomer(Custoemr 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() {

       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(139)修改Order.getCustomer()函數算法.我有可能這樣修改代碼:

Customer getCustomer() {

   Iterator iter = Customer.getInstance().iterator();

   while(iter.hasNext()) {

      Customer each = (Customer)iter.next();

      if(each.containsOrder(this) return each;

   }

   return null;

}

這段代碼比較慢,不過确實可行.而且,在資料庫環境下,如果我需要使用資料庫查詢語句,這段代碼對系統性能的影響可能并不顯著.如果,Order class中有些函數使用_customer值域,我可以實施Self Encapsulate Field(171)令它們轉而改用上述的getCustomer()函數.

如果我要保留上述的取值函數(getter),那麼Order和Customer的關聯從接口上看雖然仍然是雙向,但實作上已經是單向關系了.雖然我移除了反向指針,但兩個classes彼此之間的依存關系(inter-dependencies)仍然存在.

如果我要替換取值函數(getter),那麼我就專注地替換它,其他部分留待以後處理.我會逐一修改取值函數的調用者.讓它們通過其他來源取得Customer對象.每次修改後都編譯并測試.實際工作中這一過程往往相當快.如果這個過程讓我覺得很棘手很複雜,我會放棄本項重構.

一旦我消除了_customer值域的所有讀取點,我就可以着手處理[對此值域進行指派動作]的函數了.很簡單,隻要把這些指派動作全部移除,再把值域一并删除,就行了.由于已經沒有任何代碼需要這個值域,是以删掉它并不會帶來任何影響.