代碼複用的規則

                     

對接口編程

"對接口編程"是面向對象設計(OOD)的第一個基本原則。它的含義是:使用接口和同類型的組件通訊,即,對於所有完成相同功能的組件,應該抽象出一個接口,它們都實現該接口。具體到JAVA中,可以是接口(interface),或者是抽象類(abstract class),所有完成相同功能的組件都實現該接口,或者從該抽象類繼承。我們的客戶代碼只應該和該接口通訊,這樣,當我們需要用其它組件完成任務時,只需要替換該接口的實現,而我們代碼的其它部分不需要改變!

當現有的組件不能滿足要求時,我們可以創建新的組件,實現該接口,或者,直接對現有的組件進行擴展,由子類去完成擴展的功能。




 

優先使用對象組合,而不是類繼承

"優先使用對象組合,而不是類繼承"是面向對象設計的第二個原則。並不是說繼承不重要,而是因爲每個學習OOP的人都知道OO的基本特性之一就是繼承,以至於繼承已經被濫用了,而對象組合技術往往被忽視了。下面分析繼承和組合的優缺點:

類繼承允許你根據其他類的實現來定義一個類的實現。這種通過生成子類的複用通常被稱爲白箱複用(white-box reuse)。術語"白箱"是相對可視性而言:在繼承方式中,父類的內部細節對子類可見。

對象組合是類繼承之外的另一種複用選擇。新的更復雜的功能可以通過組合對象來獲得。對象組合要求對象具有良好定義的接口。這種複用風格被稱爲黑箱複用(black-box reuse),因爲被組合的對象的內部細節是不可見的。對象只以"黑箱"的形式出現。

繼承和組合各有優缺點。類繼承是在編譯時刻靜態定義的,且可直接使用,類繼承可以較方便地改變父類的實現。但是類繼承也有一些不足之處。首先,因爲繼承在編譯時刻就定義了,所以無法在運行時刻改變從父類繼承的實現。更糟的是,父類通常至少定義了子類的部分行爲,父類的任何改變都可能影響子類的行爲。如果繼承下來的實現不適合解決新的問題,則父類必須重寫或被其他更適合的類替換。這種依賴關係限制了靈活性並最終限制了複用性。

對象組合是通過獲得對其他對象的引用而在運行時刻動態定義的。由於組合要求對象具有良好定義的接口,而且,對象只能通過接口訪問,所以我們並不破壞封裝性;只要類型一致,運行時刻還可以用一個對象來替代另一個對象;更進一步,因爲對象的實現是基於接口寫的,所以實現上存在較少的依賴關係。

優先使用對象組合有助於你保持每個類被封裝,並且只集中完成單個任務。這樣類和類繼承層次會保持較小規模,並且不太可能增長爲不可控制的龐然大物(這正是濫用繼承的後果)。另一方面,基於對象組合的設計會有更多的對象(但只有較少的類),且系統的行爲將依賴於對象間的關係而不是被定義在某個類中。

注意:理想情況下,我們不用爲獲得複用而去創建新的組件,只需要使用對象組合技術,通過組裝已有的組件就能獲得需要的功能。但是事實很少如此,因爲可用的組件集合並不豐富。使用繼承的複用使得創建新的組件要比組裝已有的組件來得容易。這樣,繼承和對象組合常一起使用。然而,正如前面所說,千萬不要濫用繼承而忽視了對象組合技術。

相關的設計模式有: Bridge、Composite、Decorator、Observer、Strategy等。

下面的例子演示了這個規則,它的前提是:我們對同一個數據結構,需要以任意的格式輸出。

第一個例子,我們使用基於繼承的框架,可以看到,它很難維護和擴展。

abstract class AbstractExampleDocument
{
    // skip some code ...
    public void output(Example structure)
        {
            if( null != structure )
                {
                this.format( structure );
                }
        }
    protected void format(Example structure);
}

第二個例子,我們使用基於對象組合技術的框架,每個對象的任務都清楚的分離開來,我們可以替換、擴展格式類,而不用考慮其它的任何事情。

class DefaultExampleDocument 
{
  // skip some code ...
  public void output(Example structure) 
  {
     ExampleFormatter formatter = 
       (ExampleFormatter) manager.lookup(Roles.FORMATTER);
     if( null != structure ) 
     {
       formatter.format(structure);
     }
  }
}

這裏,用到了類似於"抽象工廠"的組件創建模式,它將組件的創建過程交給manager來完成;ExampleFormatter是所有格式的抽象父類;




將可變的部分和不可變的部分分離

"將可變的部分和不可變的部分分離"是面向對象設計的第三個原則。如果使用繼承的複用技術,我們可以在抽象基類中定義好不可變的部分,而由其子類去具體實現可變的部分,不可變的部分不需要重複定義,而且便於維護。如果使用對象組合的複用技術,我們可以定義好不可變的部分,而可變的部分可以由不同的組件實現,根據需要,在運行時動態配置。這樣,我們就有更多的時間關注可變的部分。

對於對象組合技術而言,每個組件只完成相對較小的功能,相互之間耦合比較鬆散,複用率較高,通過組合,就能獲得新的功能。




減少方法的長度

通常,我們的方法應該只有儘量少的幾行,太長的方法會難以理解,而且,如果方法太長,則應該重新設計。對此,可以總結爲以下原則:

  • 三十秒原則:
    如果另一個程序員無法在三十秒之內瞭解你的函數做了什麼(What),如何做(How)以及爲什麼要這樣做(Why),那就說明你的代碼是難以維護的,必須得到提高;
  • 一屏原則:
    如果一個函數的代碼長度超過一個屏幕,那麼或許這個函數太長了,應該拆分成更小的子函數;
  • 一行代碼儘量簡短,並且保證一行代碼只做一件事

那種看似技巧性的冗長代碼只會增加代碼維護的難度。






消除case / if語句

要儘量避免在代碼中出現判斷語句,來測試一個對象是否某個特定類的實例。通常,如果你需要這麼做,那麼,重新設計可能會有所幫助。我在工作中遇到這樣的一個問題:我們在使用JAVA做XML解析時,對每個標籤映射了一個JAVA類,採用SAX(簡單的XML接口API:Simple API for XML)模型。結果,代碼中反覆出現了大量的判斷語句,來測試當前的標籤類型。爲此,我們重新設計了DTD(文檔類型定義:Document Type Definition),爲每個標籤增加了一個固定的屬性:classname,而且重新設計了每個標籤映射的JAVA類的接口,統一了每個對象的操作: addElement(Element aElement); //增加子元素
addAttribute(String attName, String attValue); //增加屬性;

則徹底消除了所有的測試當前的標籤類型的判斷語句。每個對象通過 Class.forName(aElement.attributes.getAttribute("classname")).newInstence(); 動態創建,




減少參數個數

有大量參數需要傳遞的方法,通常很難閱讀。我們可以將所有參數封裝到一個對象中來完成對象的傳遞,這也有利於錯誤跟蹤。

許多程序員因爲,太多層的對象包裝對系統效率有影響。是的,但是,和它帶來的好處相比,我們寧願做包裝。畢竟,"封裝"也是OO的基本特性之一,而且,"每個對象完成儘量少(而且簡單)的功能",也是OO的一個基本原則。






類層次的最高層應該是抽象類

在許多情況下,提供一個抽象基類有利做特性化擴展。由於在抽象基類中,大部分的功能和行爲已經定義好,使我們更容易理解接口設計者的意圖是什麼。

由於JAVA不允許"多繼承",從一個抽象基類繼承,就無法再從其它基類繼承了。所以,提供一個抽象接口(interface)是個好主意,一個類可以實現多個接口,從而模擬實現了"多繼承",爲類的設計提供了更大的靈活性。




儘量減少對變量的直接訪問

對數據的封裝原則應該規範化,不要把一個類的屬性暴露給其它類,而是應該通過訪問方法去保護他們,這有利於避免產生波紋效應。如果某個屬性的名字改變,你只需要修改它的訪問方法,而不是修改所有相關的代碼。




子類應該特性化,完成特殊功能

如果一個子類只是使一個組件變成組件管理器,而不是實現接口功能,或者,重載某個功能,那麼,就應該使用一個外部的容器類,而不是創建一個子類。

建議:類層次結構圖,不要太深;

例如:下面的接口定義了組件的功能:發送消息;類Transceiver實現了該接口;而其子類Pool只是管理多個Transceiver對象,而沒有提供自己的接口實現。建議使用組合方式,而不是繼承!

public interface ITransceiver{
  public abstract send(String msg);
}
public class Transceiver implements ITransceiver {
  public send(String msg){
    System.out.println(msg);
  }
}
//使用繼承方式的實現
public class Pool extends Transceiver{
  private List  pool = new Vector();
  public void add(Transceiver aTransceiver){
    pool.add(aTransceiver);
}
public Transceiver get(int index){
    pool.get(index);
}
}
//使用組合方式的實現
public class Pool {
  private List  pool = new Vector();
  public void add(Transceiver aTransceiver){
    pool.add(aTransceiver);
}
public Transceiver get(int index){
    pool.get(index);
}
}






拆分過大的類

如果一個類有太多的方法(超過50個),那麼它可能要做的工作太多,我們應該試着將它的功能拆分到不同的類中,類似於規則四。




作用截然不同的對象應該拆分

在構建的過程中,你有時會遇到這樣的問題:對同樣的數據,有不同的視圖。某些屬性描述的是數據結構怎樣生成,而某些屬性描述的是數據結構本身。最好將這兩個視圖拆分到不同的類中,從類名上就可以區分出不同視圖的作用。

類的域、方法也應該有同樣的考慮!



儘量減少對參數的隱含傳遞

兩個方法處理類內部同一個數據(域),並不意味着它們就是對該數據(域)做處理。許多時候,該數據(域)應該作爲方法的參輸入數,而不是直接存取,在工具類的設計中尤其應該注意。例如:

public class Test{
  private List  pool = new Vector();
  public void testAdd(String str){
    pool.add(str);
}
public Object testGet(int index){
    pool.get(index);
}
}

兩個方法都對List對象pool做了操作,但是,實際上,我們可能只是想對List接口的不同實現Vector、ArrayList等做存取測試。所以,代碼應該這樣寫:

public class Test{
  private List  pool = new Vector();
  public void testAdd(List  pool, String str){
    pool.add(str);
}
public Object testGet(List  pool, int index){
    pool.get(index);
}
}
發佈了36 篇原創文章 · 獲贊 0 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章