談談依賴注入與面向介面程式設計

發表於2016-04-16

依賴注入(Dependency Injection)

今天我們討論的內容核心是面向介面程式設計,我決定還是要從依賴注入開始講起,因為DI的思想可以說是面向介面程式設計思想的特殊表現,也可以說是與面向介面程式設計相輔相成。
先撇開讓人頭腦發暈的文字定義,我們還是用我們最忠實和夥伴——程式碼來了解依賴注入。我們先來一個粗略的例子,由淺入深:
我們有一個公交車類(Bus),每天早上6點鐘需要發車(work),為其分配對應的司機(Driver),看程式碼

在上面這段程式碼中,Bus物件的運作需要用到Driver物件,因而建立了一個Driver物件,我們稱Bus對Driver有一個依賴。這樣的強耦合關係會因為日後的變化而給我們帶來很多麻煩,不久將來張三師傅辭職了,我們需要修改Bus-work()的程式碼,也就是說在開發過程中非常不便於單元測試(一是不能方便地更換各種Driver物件,二是如果Driver這個職位建立是耗時操作或者高成本操作,我們並不能使用準備好的Driver實現快速重複測試)。 我們繼續:

以上這段程式碼我們通過init方法,為Bus物件傳入了一個Driver物件,像這種非自己主動初始化依賴,而從外部通過注入點注入依賴的方式,我們就稱為依賴注入,而例子中的這種注入的方法稱之為構造器注入。明顯的,這個場景中Bus和Driver的耦合因此輕了一層。說到解耦,並不是說Bus和Driver之間的依賴關係就不存在了,在Bus的範圍內看來,只是將依賴建立從編譯期間推遲到了執行期間,畢竟Bus無論如何也是需要Driver提供服務的。對此,這篇文章有一個非常形象的比喻,“依賴就像是系統中的plugin,主系統並不強依賴於任何一個外掛,但一旦外掛被載入,主系統就應該可以準確呼叫適當外掛的功能”。

類似這樣的注入方式還有

  • 屬性注入
  • 方法注入
  • 環境上下文注入
  • 子類重寫方法注入等

不同的只是注入的手段,思想還是一樣的。

輕輕地思考

例子說完了,那是不是說我們對所有的依賴都要這樣一視同仁,破壞程式的封裝性而減輕所有的依賴呢?不,這僅僅是讓我們認識依賴注入的思想。但是對於測試驅動開發(TDD),一定量的依賴抽取又是必須的。如果說實在不希望把那麼多的拉環暴露出來,又必須貫徹測試驅動開發,objc的這篇文章這麼說到:
“This can be done by declaring them in a class category in a separate header file. For example, if we’re dealing with Example.h, then create an additional header ExampleInternal.h. This will be imported only by Example.m and by test code.”
我們可以通過強大的Category,將注入的鍼口放在Category中,而對應的Category放在一個專門用來測試的header中,思考下這個Category中做了什麼?swizzle掉依賴所在的方法,並且執行依賴注入,當然這兩者是分開的。
看到這裡,是不是有點覺得DI完全就是為了單測服務?我以前也是這麼認為的,其實不然,這僅僅是一個簡單介紹DI思想的一個例子,層次不同,我們不能從中體驗到DI帶來的好處。

元件化

也是objc的那篇文章中提到一種叫做“pluggable排插思想”,用原話來說,如果一個類的initializer需要提供一個id的引數,說明我們需要為之提供一個遵守foo協議的物件才可以讓這個類運作起來,有沒有發現DI外衣下的面向介面思想的肉體?所以說更深層的,DI的一個目標是為了實現元件化架構,DI讓依賴更加明顯,DI劃定了元件的邊界和元件的組裝方式。

開閉原則(Open-Closed Principle)

這裡要帶入一個比較重要的思想——OCP,國內比較少筆墨對OCP思想的介紹和強調,他的原文解釋是Software entities should be open for extension,but closed for modification,對擴充套件開放,對修改關閉。也就是說我們對模組的設計,應該滿足將來在不可修改原始碼的情況下對模組的職能擴充套件,或者改變模組的行為。單單這句話就能表現出OCP可怕的地位,他迫使我們主動考慮了將來,使應用保證了核心程式碼的穩定性和對新需求的靈活性。

依賴獲取(Dependency Locate)

上面我們理解了依賴注入的基礎思想,讓依賴顯式化,為依賴提供合適的注入點(鍼口),提升程式的靈活性。帶來的結果就是當我們需要更換依賴的時候不需要對使用服務的類(姑且叫作客戶類)作程式碼修改,將提供服務的類(服務類)由注入點注入到客戶類中,耦合的確輕了一層,也符合OCP原則,ok現在我們往外跳一層,在例項化客戶類的角色上下文中,需要例項化服務類進而完成對客戶類注入,服務類的更變必然導致此處程式碼的修改,這時OCP又要站出來打差評。
此時有必要講下依賴獲取。既然有注入,當然也應該有獲取,但這兩者並不是先後執行的兩個過程,而是相同目的的同一種操作,換句話說,我們讓客戶類由被動注入轉換成主動獲取,繼續貫徹的仍然是依賴注入思想。
DL就是在系統中配置一個獲取點,客戶類依賴於服務類的介面而不直接依賴服務類,客戶類根據自身需要從獲取點主動獲取服務類為其提供服務。理解了DI,對DL的概念肯定是迎刃而解。
我們思考下,客戶類只知道獲取點,按照道上的規矩交貨的對方的身份完全不需要去了解,有沒有發現面向介面(POP)的內體又暴露了一點?

更高階的依賴注入

認識完DI的另一種方式依賴獲取後,做依賴注入的辦法就不僅僅侷限於上文列舉的幾種最基本的依賴注入方式。目前比較主流的有配置檔案依賴注入反射依賴注入,例如java中強大的Spring和移植到.NET平臺的Spring.NET,.NET中自己的Autofac,他們是結合配置檔案和反射工作的,而oc中的objection我看了下是通過key-Value記憶體容器來做的DI,如果我自己做的話,還可以使用runtime target-action方式(類似於其他語言的反射),而重型專案中需不需要用到NSInvocation筆者缺乏這方面的經驗不敢獨斷。
下面還是用一個簡單的例子來增強對通過配置檔案做依賴獲取的認識:
最近有看qq瀏覽器莊延軍老師關於記憶體管理的公開課,就用手Q瀏覽器更換主題打一個例子吧:

以上,我們只需要在執行方法(-work())中拿到themeFactory,對介面進行渲染即可,而原本有可能出現依賴的地方——ThemeFactoryAnimator已經不依賴於外部注入,而僅僅依賴於我的theme.plist配置檔案,也可以說我們將多型封裝到了這個“獲取點”內,因此主題的改變對映到了配置檔案中對應內容的改變,但是這個更換主題系統目前就利用DI變得符合OCP原則了嗎?不是的,雖然依賴的改變已經對映到了客戶類封裝的外部——配置檔案中,可是我們還是無法避免if-else結構的存在,我們可以不修改程式碼自由更換主題,可是如果又開發出了一套新的主題呢?這個系統對於未來還是無能為力,這一part的重點是依賴獲取,至於怎麼消除這種缺陷?看完這篇文章也許你就自然明白了。

面向介面程式設計(Protocol-oriented programming)

我們是時候談談面向介面了,如果對筆者上面說的還沒能很好理解沒關係,思想的認識需要時間去沉澱、矯正,出來的才是真理。
首先我們怎樣定義介面:“介面泛指實體把自己提供給外界的一種抽象化物,用以由內部操作分離出外部溝通方法,使其能被修改內部而不影響外界其他實體與其互動的方式”,換句話說,在我們程式的世界裡,介面的作用就是用於定義一個或一組規則,實現對應介面的實體需要遵守對應的這些規則。也可以說是對“同類事物”的抽象表示,而“同類事物”的界定就看是否實現了同一個介面,譬如有一個Animal介面和一個NightWorking介面,公雞實現了Animal介面,貓頭鷹實現了Animal介面和NightWorking介面,還有一個實現了NightWorking介面的路燈,在Animal的範疇下,我們可以稱公雞和貓頭鷹是同類事物,而在NightWorking的範疇下,我們可以稱貓頭鷹和路燈是同類事物。。。。相對的東西真恐怖,不知道筆者什麼時候會跟什麼東西被劃分為同類。。。

面向介面程式設計(編碼)

面向介面比較抽象,也比較廣泛,它不僅僅是指一個定性的東西,我們可以從POP為程式帶來的一個一個優越性為切入點研究,下面繼續是一個簡單的例子,讓我們來感受下POP思想的初衷:
這次還是拿交通工具來說,

當然物理學上能通過轉化其他物質發出可見光的也不一定叫發光體,已經畢業了就容我不按規矩來吧。以上,因為我們按規矩辦事,製造出來的飛機從來沒有自爆過。而馬匹也重來不需要當機重啟。感覺很有道理!如果不久將來我們著手創造時空穿梭機,我們第一步工作,就是要讓其遵守實現Transportation介面等,如果我們要求這個穿梭機還能幫我們敲程式碼,我們繼續讓其遵守objcAble介面。

面向介面程式設計(架構)

不知不覺文章篇幅已經比較大了,讓我們來再往上爬一層,讓POP應用於更大的一個領域,甚至改變架構,雖然上一part已經算是一種架構思想,但是筆者更希望表現的是他在編碼應用中的優越性,而這一part將賦予POP在大型專案中不可撼動的地位。

無論是哪種架構方式,層次關係肯定是撇不開的,並且層次關係也代表著一種架構的主心骨,無論業務分層,功能分層,還是角色分層,存在於各個位置的依賴關係都需要我們去正視,而POP的目的正是為了化解這些強依賴,打破上層例項化下層去為其提供服務的強耦合,在大型專案中,一層的變化可能會聯動1+N層,這樣的變化是致命的,正如上文我們提到過的,讓一個實體由依賴另一個實體,轉變成依賴一個介面,將被依賴實體的變化隔絕於介面之外。

補充一句,這裡的介面指代的並不是上一part中實體化的”介面”,而是相對意義上的介面,一種思想!

iOS面向介面程式設計架構 實現無耦合開發方式

不知道大家看了“面向介面程式設計(編碼)”後,有沒有發現日常OC編碼中似乎隨處可見介面程式設計的痕跡?——侵蝕了我們專案各個模組的代理模式,代理模式的工作原理就是,一方使用protocol(介面)劃定一個或一組規則,成為其代理的角色必須遵守這一系列規則,最後根據規則去辦事,好處依然是那麼明顯,主體並不需要與代理溝通,代理也不需要做多餘的培訓,直接上崗,從這裡又強化了一遍介面即一種由內部操作分離出外部溝通方法,而核心就是一系列規則,通過介面工作,比直接訪問屬性或者方法穩健得多。

而這一part中我們的主題並不是這個,為了思想上的昇華,這裡給出一個簡單例子,這裡例子參考龐海礁師兄文章例子變換而來,講到那種相對意義上的介面思想。

上例中筆者藉助了OC的一個輕量級的DI框架objection,服務實現者甲方獨立編寫服務實現,而後將服務通過objection繫結到protocol之上,去看看服務使用者,乙方利用objection通過protocol拿到服務類例項,根據protocol中定義的規則,馬上就實現了服務。不需要import,不需要例項化,高度解耦,並且符合OCP原則。objection的原理就是上文提到的key-value記憶體對映表,對於大型專案,多小組分專案開發再合併的生產線,POP是必不可少的。

如果說我們在輕型開發中不想使用框架,我們也可以談談自己實現POP+DI,利用起OC的利器——runtime。其實在上例已經埋下伏筆,這次我們的乙方可以這樣做:

就是這樣,甲乙雙發約定了以介面名+Obj字串的規則去定義服務類,乙方做DL時只需要配合runtime,也是輕而易舉。

那麼如果服務類例項化需要引數呢?

配置檔案能解決這個問題,上文有提到Spring框架做DI的原理就是反射+xml,一般來說大部分支援反射機制語言的DI框架原理都是相似的,這裡說下筆者瞭解的兩種主流注入原理,構造器注入和屬性注入,記得上文也提到過著兩種注入方式,筆者強調過那只是一種思想,不是定性的一種方法,ok來看下那些DI框架是怎樣做的。

  • 構造器注入
    在進行依賴獲取的時候,DI框架通過反射機制得到待建立類的構造方法,然後根據構造器所需引數的型別或者順序,在DI容器節點中尋找,然後提供引數,建立例項。
  • 屬性注入
    同樣的,在進行DL時,通過反射得到待建立型別的所有屬性,然後根據屬性在DI容器節點中進行匹配,有則建立提供,無則跳過。
最後利用詞條做個區域性總結:
  • 依賴注入+介面程式設計
  • 呼叫者無須關心物件任何實現,只需按照介面規則呼叫服務
  • 在系統分析和架構中,分清層次和依賴關係,每個層次不是直接向其上層提供服務(即不是直接例項化在上層中),而是通過定義一組介面,僅向上層暴露其介面功能,上層對於下層僅僅是介面依賴,而不依賴具體類。
  • 服務使用端由對物件的依賴轉變成對介面的依賴,這樣甚至可以在服務提供物件還未存在之前編碼(分子專案開發)

End

依賴注入只是一種思想,其實也就是一個過程,依賴注入用到了面向介面的程式設計思想,面向介面的架構實現用到了依賴注入的執行方式。而面向介面程式設計和麵向物件程式設計並不是平級的,它並不是比物件導向程式設計更先進的一種獨立的程式設計思想,而是附屬於物件導向思想體系,屬於其中一部分。或者說,它是物件導向程式設計體系中的思想精髓之一。

同時我要讚歎POP的強大,對於未來的未知事物,我們先認知這個東西的行為(使用介面來實現這個行為),再認知這種行為的具體(使用具體的程式碼實現這個介面)。

這篇文章中有提到廣義的”介面”也有專指的”介面”,讀後具體的理解和認識就靠自己用時間去慢慢沉澱了。

寫在最後

在我對依賴注入理解得比較淺的時候,只是淺層地理解這種思想的存在,並沒有相關開發經驗足以支撐我深入對DI的思考和感受,網上的文章全部都僅僅侷限在那幾個淺層的例子,並沒有繼續深入挖掘解釋,全靠一位師兄為我講解,所以我希望有篇文章可以聚集大家思考討論,同時為他人提供學習的途徑。心中有疑惑又無能為力的感覺的確非常痛苦。謝謝!

相關文章