簡述
我會在這篇這一系列文章中談談面向對象思想的幾個部分,并且給出對應的解決方案,這些解決方案有些是用面向過程的思路解決的,有些也還是停留在面向對象中。到最後我會給大家一個比較,然後給出結論。
上下文規範
在進一步地讨論這些概念之前,我需要跟大家達成一個表達上的共識,我會采用下面的文法來表達對象相關的資訊:
所有的大寫字母都是類或對象,小寫字母表示屬性或方法。
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層的時候,你就要好好考慮是否這個繼承的方案了
,第三層繼承正是濫用的開端。确定有必要之後,再進行更多層次的繼承。
是以我的态度是:
萬不得已不要用繼承,優先考慮組合