天天看點

跳出面向對象思想(一) 繼承

原文

簡述

我會在這篇這一系列文章中談談面向對象思想的幾個部分,并且給出對應的解決方案,這些解決方案有些是用面向過程的思路解決的,有些也還是停留在面向對象中。到最後我會給大家一個比較,然後給出結論。

上下文規範

在進一步地讨論這些概念之前,我需要跟大家達成一個表達上的共識,我會采用下面的文法來表達對象相關的資訊:

所有的大寫字母都是類或對象,小寫字母表示屬性或方法。

FOO:{ isLoading, _data, render(), _switch() }   這表示一個FOO對象,isLoading、_data是它的屬性,render()、_switch()是它的方法,加下劃線表示私有。

A -> B                                          這表示從A派生出了B,A是父類。

A -> B:{ [a, b, c(), d()], e, f() }             []裡面是父類的東西,e、f()是派生類的東西

B:{ [ A ], e, f() }                             省略了對父類的描述,用類名A代替,其他同上

B:{ [ A ], e, f(), @c() }                       省略了對父類的描述,函數前加@表示重載了父類的方法。

B:{ [ A,D ], e, f() }                           多繼承,B繼承了A和D

B<protocol>                                     符合某個protocol接口的對象。

<protocol>:{foo(), bar}                         protocol這個接口中包含foo()這個方法,bar這個屬性。

foo(A, int)                                     foo這個函數,接收A類和int類型作為參數。
           

來,我們談談對象

面向對象思想三大支柱:繼承、封裝、多态。這篇文章說的是繼承。當然面向對象和面向過程都會有好有壞,但是做決定的時候,更多地還是去權衡值得不值得放棄。關于這樣的立場問題,我都會給出非常明确的傾向,不會跟你們打太極。

如果說這個也好那個也好,那還發表毛個觀點,那叫沒有觀點。

繼承

繼承從代碼複用的角度來說,特别好用,也特别容易被濫用和被錯用。不恰當地使用繼承導緻的最大的一個缺陷特征就是高耦合。

在這裡我要補充一點,耦合是一個特征,雖然大部分情況是缺陷的特征,但是當耦合成為需求的時候,耦合就不是缺陷了。耦合成為需求的例子在後面會提到。

我們來看下面這個場景:

有一天,産品經理Yuki說:

我們不光首頁要有一個搜尋框,在進去的這個頁面,也要有一個搜尋框,隻不過這個搜尋框要多一些功能,它是可以即時給使用者搜尋提示的。

Casa接到這個任務,他研究了一下代碼,說:OK,沒問題~

Casa知道代碼裡已經有了一個現成的搜尋框,Casa立刻從

HOME_SEARCH_BAR

派生出

PAGE_SEARCH_BAR

嗯,目前事情進展到這裡還不錯:

HOME_SEARCH_BAR:{textField, search(), init()}
PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() }
           

過了幾天,産品經理Yuki要求:

使用者收藏的東西太多了,我們的app需要有一個本地搜尋的功能。

Casa輕松通過方法覆寫擺平了這事兒:

HOME_SEARCH_BAR:{textField, search()}
PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() }
LOCAL_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], @search() }
           

app上線一段時間之後,UED不知哪根筋搭錯了,決定要修改搜尋框的UI,UED跟Casa說:

把HOME_SEARCH_BAR的樣式改成這樣吧,裡面PAGE_SEARCH_BAR還是老樣子就OK。

Casa表示這個看似簡單的修改其實很蛋碎,

HOME_SEARCH_BAR

的樣式一改,

PAGE_SEARCH_BAR

LOCAL_SEARCH_BAR

都會改變,怎麼辦呢? 與其每個手工修一遍,Casa不得已隻能給

HOME_SEARCH_BAR

添加了一個函數:

initWithStyle()

HOME_SEARCH_BAR:{ textField, search(), init(), initWithStyle() }
PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() }
LOCAL_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], @search() }
           

于是代碼裡面就出現了各種init()和initWithStyle()混用的情況。

無所謂了,先把需求應付過去再說。

Casa這麼想。

有一天,另外一個team的leader來對Casa抱怨:

搞什麼玩意兒?為毛我要把LOCAL_SEARCH_BAR獨立出來還特麼連帶着把那麼多檔案都弄出來?我就隻是想要個本地搜尋的功能而已!!

這是因為

LOCAL_SEARCH_BAR

依賴于它的父類

HOME_SEARCH_BAR

,然而

HOME_SEARCH_BAR

本身也帶着API相關的對象,同時還有資料解析的對象。 也就是說,要想把

LOCAL_SEARCH_BAR

移植給另外一個TEAM,拔出蘿蔔帶出泥,差不多整個Networking架構都要移植過去。 嗯,Casa又要為了解耦開始一個不眠之夜了~

以上是典型的錯誤使用繼承的案例,雖然繼承是代碼複用的一種方案,但是使用繼承仍然是需要好好甄别代碼複用的方式的,不是所有場景的代碼複用都适用于繼承。

繼承是緊耦合的一種模式,主要的展現就在于牽一發動全身。
  • 第一種類型的問題是改了一處,到處都要改,但解決方案還算友善,多添加一個特定的函數(initWithStyle())就好了。隻是代碼裡面難看一點。
  • 第二種類型的問題是代碼複用的時候,要跟着把父類以及父類所有的相關依賴也複制過去,高耦合在複用的時候造成了備援。

對于這樣的問題,業界其實早就給出了解決方案:用組合替代繼承。将Textfield和search子產品拆開,然後通過定義好的接口進行互動,一般來說可以選擇Delegate模式來互動。

解決方案:

<search_protocol>:{search()}

SEARCH_LOGIC<search_protocol>

SEARCH_BAR:{textField, SEARCH_LOGIC<search_protocol>}

HOME_SEARCH_BAR:{SearchBar1, SearchLogic1}
PAGE_SEARCH_BAR:{SearchBar2, SearchLogic1}
LOCAL_SEARCH_BAR:{SearchBar2, SearchLogic2}
           

這樣一來,搜尋框和搜尋邏輯分别形成了兩個不同的元件,分别在

HOME_SEARCH_BAR

,

PAGE_SEARCH_BAR

LOCAL_SEARCH_BAR

中以不同的形态組合而成。

textField

SEARCH_LOGIC<search_protocol>

之間通過delegate的模式進行資料互動。 這樣就解決了上面提到的兩種類型的問題。 大部分我們通過代碼複用來選擇繼承的情況,其實都是變成組合比較好。 是以我在團隊中一直在推動使用組合來代替繼承的方案。 那麼什麼時候繼承才有用呢?

糾結了一下,貌似實在是沒什麼地方非要用繼承不可的。但事實上使用繼承,我們得要厘清楚層次,使用繼承其實是如何給一類對象劃分層次的問題。在正确的繼承方式中,父類應當扮演的是底層的角色,子類是上層的業務。舉兩個例子:

Object -> Model
Object -> View
Object -> Controller

ApiManager -> DetailManager
ApiManager -> ListManager
ApiManager -> CityManager
           

這裡是有非常明确的層次關系的,我在這裡也順便提一下使用繼承的3大要點:

父類隻是給子類提供服務,并不涉及子類的業務邏輯
Object并不影響Model, View, Controller的執行邏輯和業務  
Object為子類提供基礎服務,例如記憶體計數等

ApiManager并不影響其他的Manager  
ApiManager隻是給派生的Manager提供服務而已,ApiManager做的隻會是份内的是,對于子類做的事情不參與。
           
層級關系明顯,功能劃厘清晰,父類和子類各做各的。
Object并不參與MVC的管理中,那些都隻是各自派生類自己要處理的事情

DetailManager, ListManager, CityManager都隻是處理各自業務的對象  
ApiManager并不應該涉足對應的業務。
           
父類的所有變化,都需要在子類中展現,也就是說此時耦合已經成為需求
Object對類的描述,對記憶體引用的計數方式等,都是普遍影響派生類的。  
ApiManager中對于網絡請求的發起,網絡狀态的判斷,是所有派生類都需要的。  
此時,牽一發動全身就已經成為了需求,是适用繼承的
           

此時我們回過頭來看為什麼

HOME_SEARCH_BAR

PAGE_SEARCH_BAR

LOCAL_SEARCH_BAR

采用繼承的方案是不恰當的:

  • 他們的父類是

    HOME_SEARCH_BAR

    ,父類不隻提供了服務,也在一定程度上影響了子類的業務邏輯。派生出的子類也是為了要做搜尋,雖然搜尋的邏輯不同,但是互相涉及到搜尋這一塊業務了。
  • 子類做搜尋,父類也做搜尋,雖然處理邏輯不同,但是這是同一個業務,與父類在業務上的聯系密切。在層級關系上,

    HOME_SEARCH_BAR

    和其派生出的

    LOCAL_SEARCH_BAR

    PAGE_SEARCH_BAR

    其實是并列關系,并不是上下層級關系。
  • 由于這裡所謂的父類和子類其實是并列關系而不是父子關系,且并沒有需要耦合的需求,相反,每個派生子類其實都不希望跟父類有耦合,此時耦合不是需求,是缺陷。

總結

可見,代碼複用也是分類别的,如果當初隻是出于代碼複用的目的而不區分類别和場景,就采用繼承是不恰當的。我們應當考慮以上3點要素看是否符合,才能決定是否使用繼承。就目前大多數的開發任務來看,繼承出現的場景不多,主要還是代碼複用的場景比較多,然而通過組合去進行代碼複用顯得要比繼承麻煩一些,因為組合要求你有更強的抽象能力,繼承則比較符合直覺。然而從未來可能産生的需求變化和維護成本來看,使用組合其實是很值得的。另外,

當你發現你的繼承超過2層的時候,你就要好好考慮是否這個繼承的方案了

,第三層繼承正是濫用的開端。确定有必要之後,再進行更多層次的繼承。

是以我的态度是:

萬不得已不要用繼承,優先考慮組合

繼續閱讀