【譯】再見,面向對象編程(一)

Charles Scalfanijava

原文: https://medium.com/@cscalfani...

我使用面嚮對象語言編程已經十幾年了。我是用的第一個OO語言是C++,而後是Smalltak,最後是.NET和Java。程序員

我迫切地想從面向對象的三大支柱,集成,封裝和多態上獲得收益。編程

我急於從這個到我面前的新領地獲得對於重用的承諾。微信

我對於將現實對象映射到類的想法很是興奮,並但願整件事能平滑遷移。ide

我想太多了。測試

繼承,第一個跌落的支柱

最先,繼承看起來是面向對象範式的最大收益。全部對新手灌輸的關於形狀繼承的簡化例子看起來邏輯上很合理。this

clipboard.png

我照單全收而且發現了新東西。spa

香蕉猴子雨林問題

我帶着信仰和須要解決的問題,開始構建類繼承和寫代碼,一切都很好。.net

我永遠不會忘記那一天,當我打算開始從一個現有類使用繼承來重用的時候,這是我一直在等待的時刻。翻譯

一個新項目來了,我想到在我上一個工程裏的那個類。

沒問題,重用來搞定。我從老工程裏找到那個類並拷過來使用。
可是。。。 不僅是那個類。我須要父類。但。。 但先這樣。
啊。。。等下。。。看起來咱們須要這個父類的父類。。。而後。。。 咱們須要全部父類。好吧。。好吧。。我來解決這個。沒問題。
我去。如今不能編譯。爲何?哦,我知道了。。。 這個對象包含了其餘對象。因此我也須要那些。 沒問題。
等等。。。我不僅是須要那個對象。我須要對象的父類和他父類的父類,而後每一個包含的對象和他們全部的父類。。。
暈。

Joe Armstrong,Erlang之父曾說過:

面嚮對象語言的問題是他們隱式的包含了他們周圍的環境。你須要一個香蕉可是你獲得的是一個拿着香蕉的大猩猩和整個雨林。

香蕉猴子雨林解決方案

我能夠經過不寫太深的繼承來解決這個問題。但複用的關鍵就是繼承,任何我對這個機制上的限制都直接限制了重用,是吧?
是的。
因此可憐的面向對象程序員,who’s had a healthy helping of the Kool-aid, to do?
組合和委託,後面說這個。

鑽石問題

如下問題早晚會遇到,取決於使用的語言。

clipboard.png

大部分OO語言不支持這個,儘管這個看起來符合邏輯。讓OO語言支持這個有多難?
想象下如下僞代碼:

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier inherits from Scanner, Printer {
}

注意Scanner類和Printer類都實現了一個start功能。
因此Copier累繼承了哪個start功能?Scanner?仍是Printer?不可能兩個都實現。

鑽石問題的解決方案

方案很簡單。不要這麼作。
是的。大部分OO語言不讓你這麼作。
可是,若是個人建模就是這樣呢?我須要個人重用!
那麼你必須使用組合和委託。

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier {
  Scanner scanner
  Printer printer
  function start() {
    printer.start()
  }
}

注意如今Copier類包含了Printer和Scanner的實例。他將start功能委託給Printer類的實現。他也能夠簡單委託給Scanner。
這個問題也讓繼承範式開始出現問題。

脆弱的基類問題

因此如今我保證個人繼承關係比較扁平,並不會出現環狀引用。沒有鑽石問題。
如今一切正常,直到。。。
一天,個人代碼運行正常,但後一天就不工做了。我沒有變動個人代碼。
那麼,這多是個bug。。。 但等下。。。 有些東西確實變了。。。
但那個變更不在個人代碼裏。這個變更是在我繼承的類裏面。
爲何基類的變更會致使個人代碼有問題?
咱們先設想有個基類(我用Java寫的,你不懂Java應該也能夠比較容易的理解):

import java.util.ArrayList;
 
public class Array
{
  private ArrayList<Object> a = new ArrayList<Object>();
 
  public void add(Object element)
  {
    a.add(element);
  }
 
  public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      *a.add(elements[i]); // this line is going to be changed*
  }
}

重要:注意註釋的那段代碼。這段代碼後面的變動會破壞邏輯。

這個類的接口有兩個功能, add()和addAll()。add()會加一個單獨的元素, addAll()會調用add方法來增長多個元素。

這是衍生類:

public class ArrayCount extends Array
{
  private int count = 0;
 
  @Override
  public void add(Object element)
  {
    super.add(element);
    ++count;
  }
 
  @Override
  public void addAll(Object elements[])
  {
    super.addAll(elements);
    count += elements.length;
  }
}

ArrayCount類是Array類的一個具體實現。惟一的行爲區別是ArrayCount保存了元素的數量(count)。
讓咱們看下兩個類的細節。
Array add()添加一個元素到本地的ArrayList。
Array addAll()爲每一個元素循環調用本地的ArrayList。

ArrayCount add()調用父類的add()而且增長數量count。
ArrayCount addAll()調用父類的addAll()而後根據元素的數量增長數量count。

目前看起來都正常。

如今打破邏輯了。基類註釋的代碼變動成如下這樣:

public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      add(elements[i]); // this line was changed
  }

基類全部者關心的部分,功能仍是按設想同樣運轉正常。而且全部自動化測試仍然能夠經過。
但全部者顯然沒有關注到派生類。因此派生類的做者被粗暴干擾了。
如今ArrayCount addAll()調用父類的addAll(),其內部調用add()的邏輯已經被派生類覆蓋了。
這樣會致使數量count在每次派生類調用add()時增長,而後在派生類調用addAll()時再被增長一次。

這被計數了兩次。

若是是這樣,而且已經發生了,派生類的做者必須知道積累是被如何實現的。他們必須在每次基類變動時被通知到,由於這可能會致使派生類在不可預見的狀況下工做。

太糟了!這個巨大的問題永久影響了繼承範式的穩定性。

脆弱的基類解決方案

此次同樣,包含和委託能夠解決。
使用包含和委託,咱們從白盒編程轉化成黑盒編程。白盒編程時,咱們須要關注基類的實現。
黑盒編程時,因爲咱們沒法經過覆蓋基類方法的方式來注入代碼,咱們能夠徹底忽略其實現。咱們只須要關心接口。

這個趨勢有點危險。。。
繼承應該是重用最重要的手段。
OO語言沒有設計成讓包含和委託方便使用。他們是被設計成讓繼承方便易用。
若是你像我同樣,你會開始對這個繼承的問題開始驚奇。但更重要的是,這會讓你對於繼承的信心開始動搖。

繼承問題

每次當我進入一家新公司,我都會對於找個地方放我公司文檔的地方開始糾結,好比,員工手冊。
我是建一個目錄叫「文檔」而後在裏面建個目錄叫「公司」?
或者我建一個目錄叫「公司」而後在裏面建個目錄叫「文檔」?
均可以。可是哪個是正確的? 是最好的?
目錄繼承的想法是基類(父母)更加通用,派生類(子類)會更加具體。並且咱們本身會在繼承鏈上作更加具象化的版本。(看上面形狀繼承的例子)
但當一個父類和子類能夠互相調換位置時,這個模型明顯哪裏出了問題。

繼承問題解決方案

如今的問題是。。。
分類繼承不工做了。
因此繼承方式好在哪裏?
包含。
若是你看下現實世界,你能夠看到包含(或排他全部權)繼承處處都是。
而你找不到的是分類繼承。讓那個先等一會。面向對象範式來源於於現實世界,對象被另外一個對象填入。但他使用了一個有問題的模型。分類繼承,沒有現實世界的基礎。
現實世界使用的是包含繼承。一個容器包含繼承的很好的例子是你的襪子。他們在襪子的抽屜裏,而後被你衣服的抽屜包進去,而後又被你的臥室包含,而後又被你的房子包含。
你硬盤的目錄是另外一個容器包含繼承的例子。他們保存文件。
因此咱們如何對他們分類?
若是你考慮下公司目錄,其實我放在哪裏沒什麼太大關係。我能夠把他們放在一個叫「文檔」的目錄或放在一個叫「東西」的目錄。
我分類的方式是使用tag標籤。我使用如下標籤來給文件打標:

文檔
公司
手冊

標籤沒有順序或繼承。(這也解決了鑽石問題)
tag與接口相似,你能夠有多種類型與文檔關聯。
看到這麼多問題,看起來繼承範式已經完了。
再見,繼承。


微信公衆號「麥芽麪包」,id「darkjune_think」開發者/科幻愛好者/硬核主機玩家/業餘翻譯家/書蟲交流Email: zhukunrong@yeah.net

相關文章
相關標籤/搜索