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

一: 上下文規范

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

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

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_BARLOCAL_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. 有一天,另外一個teamleader來對Casa抱怨:

    搞什么玩意兒?為毛我要把LOCAL_SEARCH_BAR獨立出來還特么連帶著把那么多文件都弄出來?我就只是想要個本地搜索的功能而已!!

    這是因為LOCAL_SEARCH_BAR依賴于它的父類HOME_SEARCH_BAR,然而HOME_SEARCH_BAR本身也帶著API相關的對象,同時還有數據解析的對象。 也就是說,要想把LOCAL_SEARCH_BAR移植給另外一個TEAM,拔出蘿卜帶出泥,差不多整個Networking框架都要移植過去。 嗯,Casa又要為了解耦開始一個不眠之夜了~

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

繼承是緊耦合的一種模式,主要的體現就在于牽一發動全身。

  • 第一種類型的問題是改了一處,到處都要改,但解決方案還算方便,多添加一個特定的函數(initWithStyle())就好了。只是代碼里面難看一點。
  • 第二種類型的問題是代碼復用的時候,要跟著把父類以及父類所有的相關依賴也復制過去,高耦合在復用的時候造成了冗余。

對于這樣的問題,業界其實早就給出了解決方案:用組合替代繼承。將Textfieldsearch模塊拆開,然后通過定義好的接口進行交互,一般來說可以選擇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中以不同的形態組合而成。 textFieldSEARCH_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大神的文章跳出面向對象思想(一) 繼承

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,818評論 18 139
  • 1. 安裝 Github 查看是否安裝git: $ git config --global user.name "...
    Albert_Sun閱讀 13,697評論 9 163
  • 有必要先來了解什么是SSH和什么是SSH key。 SSH Secure Shell (SSH) 是一個允許兩臺電...
    faner閱讀 47,457評論 1 48
  • 共享資源要進行合理的運行,不然就是在浪費共享資源,比如滴滴打車就是在合理的使用共享資源,以為它的理念更合理。
    王秀君閱讀 217評論 0 0
  • 夜色朦朧,桃李滿堂紅 秋色匆匆,夏末枯槁懵 若你抬頭 便會望見掛滿枝頭的青石榴 倘若世界沒有你 那么遇見也便不會有...
    米駱駝閱讀 650評論 0 1