復合優于繼承

第16條:復合優于繼承


前言

本條內容的繼承不包括接口繼承。

1.什么事復合

復合就是在你的類中添加一個私有域,引用一個類的實例,使被引用類成為引用類的一個組件。

2. 繼承的缺點

(1)繼承不容易控制,使用不當容易導致軟件非常脆弱,特別是繼承不再同一個包下的類。
(2)繼承打破了父類的封裝性,有的時候父類的內部實現改變,可能會導致子類遭到破壞。
舉個比書上簡單一點的例子,比如我們有個類,他包含一個集合,我們要并對外提供了兩個api,分別是add(String str)和addAll(List<String> strs),具體的類如下:

public class MyObject {
  private List<String> list = new ArrayList<>();

  public void add(String ele) {
    list.add(ele);
  }
  
  public void addAll(List<String> elements) {
    for(String ele : elements) {
      add(ele);
    }
  }
}

然后我們需要記錄這個類的從創建到銷毀,一共添加過多少元素,如果我們想要用繼承的方式,并且在不知道具體內部實現的前提之下,我們可能會這樣寫:

public class MyChildObject extends MyObject  {
  private int addedEleNum = 0;
  
  @Overried
  public void add(String ele) {
    addedEleNum++;
    super.add(ele);
  }
  
  @Overried
  public void addAll(List<String> elements) {
    addedEleNum += elements.size();
    super.addAll(elements);
  }
}

很明顯,這樣做是得不到我們想要的結果的,想要得到我們想要的結果,我們一般需要查看MyObject的具體實現,這就打破了封裝性,好吧,看了具體實現之后我們知道怎么做了,那就是不覆蓋addAll()方法。那問題又來了,如果在下一個版本中,MyObject的addAll()方法改了呢,改成想下面這樣的:

public void addAll(List<String> elements) {
  for(String ele : elements) {
    list.add(ele);
  }
}

這樣的話MyChildObject又不能正常工作了,OMG。導致子類不能正常工作的原因還有很多,甚至父類中新添加一個類似add()的方法都會導致子類不能正常工作。所以這樣的子類是異常脆弱的。
so,可以被繼承的類要么在同一個包內(在同一個程序員的控制之下),要么是專門為繼承而實際,并提供了很好的文檔說明。

(3)看了第二點,你可能會覺得,我繼承的時候只要不覆蓋父類的方法不就可以了么?確實,相對于覆蓋確實安全一些,不過這不是絕對安全,當父類新增了一個方法,并且方法名和和參數都和父類相同,但返回值不同,那么子類將無法通過編譯。如果返回值也相同的話,又回到了第二個問題。同樣導致了子類不健壯。

3.復合的優點

上面說到繼承的缺點就是復合的優點。

4.復合的正確使用姿勢

在這里需要先解釋一下“轉發“的概念,轉發就是,你先復合一個類,然后在復合的類中實現所有被復合類的公有方法(api),實現的方式就是在相應的方法中調用被復合類的方法,并且不能被添加其他方法。比如為上面的MyObject寫一個轉發類:

public class ForwardingMyObject {
  private MyObject mObject;
  
  //這里使用依賴注入的方式來得到被復合類的引用
  //目的是提高可測試性和靈活性
  public ForwardingMyObject(Myobject object) {
    this.mObject = object;
  }
  
  public void add(String ele) {
    mObject.add(ele);
  }

  public void addAll(List<String> elements) {
    mObject.addAll(elements);
  }
}

轉發類就是上面提到的,專門為繼承而設計的類
現在來闡述復合的正確使用姿勢:
(1)為想要被繼承的類設計一個轉發類。
(2)繼承這個轉發類。
(3)覆蓋想要覆蓋的方法,或者添加想要添加的方法。
將例子寫完,我們來快樂的繼承ForwardingMyObject吧:

public class MyChildObject extends ForwardingMyObject {
  private int count = 0;  

  public MyChildObject(MyObject object) {
    super(object);
  }

  @Override
  public void add(String ele) {
    count ++;
    super.add(ele);
  }

  @Override
  public void addAll(List<String> elements) {
    cout += elements.size();
    super.addAll(elements);
  }
}

為什么不直接在在轉發類中直接實現計數功能?這樣好麻煩!
好吧,我承認,上面的例子太簡單,不利于解釋這個問題,主要是為了便于理解,那我們繼續。
首先問個問題,我們在設計一個類的api的時候是直接在類中寫一堆public的方法么?
什么?是的,好吧,你這種沒追求的程序員快滾去睡覺吧,我不想和你聊天T_T。
我們在設計一個類的api的時候一般都是先將類的接口寫出來,然后在用這個類來實現這個接口。
行,明白這里我們就來看書里的栗子吧,這里我把set改成list,聯系下上文中的栗子:
轉發類

public class ForwardingList<E> implements List<E> {
  private List<E> mList;
  public ForwardingList(List<E> list) {
    this.mList = list;
  }
  @Override
  public void add(int location, E object) {
    mList.add(location, object);
  }
  @Override
  public boolean add(E object) {
    return mList.add(object);
  }
  //其他的一些api
  ...
}

包裝類(相當于上面例子中的MyChildObject):

public class InstrumentedList<E> extends ForwardingList<E> {

  private int addCount = 0;

  public InstrumentedList(List<E> list) {
    super(list);
  }

  @Override
  public boolean add(E object) {
    addCount++;
    return super.add(object);
  }

  @Override
  public void add(int location, E object) {
    addCount++;
    super.add(location, object);
  }

  @Override
  public boolean addAll(@NonNull Collection<? extends E> collection) {
    addCount += collection.size();
    return super.addAll(collection);
  }

  @Override
  public boolean addAll(int location, @NonNull Collection<? extends E> collection) {
    addCount += collection.size();
    return super.addAll(location, collection);
  }

  public int getAddCount() {
    return addCount;
  }
}

看到好處了么?現在我們寫的InstrumentedList是一個真正List,不僅僅只是名字里有List而已!這意味著任何需要List作為參數的地方都可以把他傳遞過去!
不僅如此,它實現了一個傳入List的構造方法,也就是說只要是實現了List的類都可以傳遞進去,什么ArrayList呀LinkedList都可以傳進去并統計add了多少個元素。十分靈活!

5.復合的缺點

不適合用于回調框架。

6. 總結

簡而言之,繼承的功能十分強大,但也存在諸多問題,因為他違背了封裝原則。只有當子類和超類確實存在子類型關系時,使用繼承才是恰當的。即便如此,如果子類和超類存在不同的包中,并且超類并不是為繼承而設計的,那么繼承將導致脆弱性,可以用復合和轉轉發機制來代替繼承,尤其是當存在適當的接口可以實現包裝類的時候。包裝類不僅比子類更加健壯,而且功能也更加強大。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 文中的實驗代碼我放在了這個項目中。 以下內容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 946評論 0 6
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,142評論 0 62
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,766評論 18 399
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,948評論 18 139
  • 在寫移動端web項目時,經常會用到REM這個單位。但是不同REM和PX的相對基準值是不同的(像我們在IPhone6...
    歡樂相隨閱讀 531評論 0 0