一: 上下文規范
在進一步地討論這些概念之前,我需要跟大家達成一個表達上的共識,我會采用下面的語法來表達對象相關的信息:
所有的大寫字母都是類或對象,小寫字母表示屬性或方法。
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類型作為參數。
二:對象
面向對象思想三大支柱:繼承、封裝、多態。這篇文章說的是繼承。當然面向對象和面向過程都會有好有壞,但是做決定的時候,更多地還是去權衡值得不值得放棄。關于這樣的立場問題,我都會給出非常明確的傾向,不會跟你們打太極。
如果說這個也好那個也好,那還發表毛個觀點,那叫沒有觀點。
2.1: 繼承
繼承從代碼復用的角度來說,特別好用,也特別容易被濫用和被錯用。不恰當地使用繼承導致的最大的一個缺陷特征就是高耦合。
在這里我要補充一點,耦合是一個特征,雖然大部分情況是缺陷的特征,但是當耦合成為需求的時候,耦合就不是缺陷了。耦合成為需求的例子在后面會提到。
我們來看下面這個場景:
-
a. 有一天,產品經理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() }
-
b. 過了幾天,產品經理Yuki要求:
用戶收藏的東西太多了,我們的app需要有一個本地搜索的功能。
Casa輕松通過方法覆蓋擺平了這事兒:
HOME_SEARCH_BAR:{textField, search()} PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() } LOCAL_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], @search() }
-
c. 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這么想。
-
d. 有一天,另外一個
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大要點:
4.1 父類只是給子類提供服務,并不涉及子類的業務
Object并不影響Model, View, Controller的執行邏輯和業務
Object為子類提供基礎服務,例如內存計數等
ApiManager并不影響其他的Manager
ApiManager只是給派生的Manager提供服務而已,ApiManager做的只會是份內的是,對于子類做的事情不參與。
4.2: 層級關系明顯,功能劃分清晰,父類和子類各做各的。
Object并不參與MVC的管理中,那些都只是各自派生類自己要處理的事情
DetailManager, ListManager, CityManager都只是處理各自業務的對象
ApiManager并不應該涉足對應的業務。
4.3: 父類的所有變化,都需要在子類中體現,也就是說此時耦合已經成為需求
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層的時候,你就要好好考慮是否這個繼承的方案了,第三層繼承正是濫用的開端。確定有必要之后,再進行更多層次的繼承。
所以我的態度是:萬不得已不要用繼承,優先考慮組合
轉載自casa大神的文章跳出面向對象思想(一) 繼承