架構(gòu) - iOS組件化設(shè)計與開發(fā)

前言

首先我覺得”組件”在這里不太合適,因為按我理解組件是指比較小的功能塊,這些組件不需要多少組件間通信,沒什么依賴,也就不需要做什么其他處理,面向?qū)ο缶湍芨愣?。而這里提到的是較大粒度的業(yè)務(wù)功能,我們習(xí)慣稱為”模塊”,指較大粒度的業(yè)務(wù)模塊。

為什么需要進行組件化

【1】產(chǎn)品閉環(huán)已經(jīng)確定,就需要實施組件化來應(yīng)對A輪之后的業(yè)務(wù)擴張。
【2】將項目中的各個模塊按照基礎(chǔ)組件,功能組件,業(yè)務(wù)組件劃分成一個個單獨的模塊,以使得各個模塊間可以單獨開發(fā)、測試、組合運行。
【3】出現(xiàn)一些相對獨立的業(yè)務(wù)功能模塊,而團隊的規(guī)模也會隨著項目迭代逐漸增長。
為了更好的分工協(xié)作,團隊會安排團隊成員各自維護一個相對獨立的業(yè)務(wù)組件。這個時候我們引入組件化方案,一是為了解除組件之間相互引用的代碼硬依賴,二是為了規(guī)范組件之間的通信接口; 讓各個組件對外都提供一個黑盒服務(wù),而組件工程本身可以獨立開發(fā)測試,減少溝通和維護成本,提高效率。
【4】相同模塊重復(fù)開發(fā)。
進一步發(fā)展,當(dāng)團隊涉及到轉(zhuǎn)型或者有了新的立項之后,一個團隊會開始維護多個項目App,而多個項目App的需求模塊往往存在一定的交叉,而這個時候組件化給我們的幫助會更大,我只需要將之前的多個業(yè)務(wù)組件模塊在新的主App中進行組裝即可快速迭代出下一個全新App。

組件化的期望:

一個團隊維護一到兩個獨立App,每個獨立App除開包含一些產(chǎn)品相關(guān)的非獨立模塊集之外,還需要用一些獨立的業(yè)務(wù)組件進行組裝。 而不管是產(chǎn)品的非獨立模塊集、還是獨立業(yè)務(wù)組件都需要底層公共庫和基礎(chǔ)庫的支持。如下圖所示:

在最理想的情況下,這些子工程直接應(yīng)該只存在上層到下層的依賴,即業(yè)務(wù)模塊對底層基礎(chǔ)模塊的依賴,業(yè)務(wù)工程之間盡可能不出現(xiàn)橫向依賴。

模塊設(shè)計原則

  • 越底層的模塊,應(yīng)該越穩(wěn)定,越抽象,越具有高復(fù)用度。
  • 不要讓穩(wěn)定的模塊依賴不穩(wěn)定的模塊, 減少依賴。
    比如 B 模塊依賴了 A 模塊,如果 B 模塊很穩(wěn)定,但是 A 模塊不穩(wěn)定,那么B模塊也會變的不穩(wěn)定了
  • 提升模塊的復(fù)用度,自完備性有時候要優(yōu)于代碼復(fù)用
    我們?yōu)榱诉@個模塊的自完備性,就可以重新實現(xiàn)下這幾個方法,而不是依賴Utils模塊
  • 每個模塊只做好一件事情,不要讓Common出現(xiàn)
  • 按照你架構(gòu)的層數(shù)從上到下依賴,不要出現(xiàn)下層模塊依賴上層模塊的現(xiàn)象,業(yè)務(wù)模塊之間也盡量不要耦合

如何做組件化設(shè)計

做模塊化還是要結(jié)合實際業(yè)務(wù),對目前APP的功能做一個模塊劃分,在劃分模塊的時候還需要關(guān)注模塊之間的層級。

比如說,在我們項目中,模塊被分成了3個層級:基礎(chǔ)層、中間層、業(yè)務(wù)層。

基礎(chǔ)層模塊

比如像網(wǎng)絡(luò)框架、工具類、各種系統(tǒng)類的擴展、持久化、Log、社交化分享這樣的模塊,這一層的模塊我們可以稱之為組件,具有很強的可重用性。這些代碼不會頻繁改動,可以作為基礎(chǔ)依賴。

中間層模塊

可以有登錄模塊、網(wǎng)絡(luò)層、資源模塊等,這一層模塊有一個特點是它們依賴著基礎(chǔ)組件但又沒有很強的業(yè)務(wù)屬性,同時業(yè)務(wù)層對這層模塊的依賴是很強的。做到公共模塊下沉。

業(yè)務(wù)層模塊

就是直接和產(chǎn)品需求對應(yīng)的模塊了,比如類似朋友圈、直播、Feeds流這樣的業(yè)務(wù)功能了。

組件化第一步-剝離公共庫和產(chǎn)品基礎(chǔ)庫
在具體的項目開發(fā)過程中,我們使用cocoapod的組件依賴管理利器已經(jīng)開始從Github上引入了一些第三方開源的基礎(chǔ)庫,
比如說AFNetworking、SDWebImage、SVProgressHUD、ZipArchive等。除開這些第三方開源基礎(chǔ)庫之外,
我們還需要做的事情就是將一些基礎(chǔ)組件從主工程剝離出來,形成產(chǎn)品自己的私有基礎(chǔ)庫倉庫,為我們進行業(yè)務(wù)獨立組件的分離做準(zhǔn)備。

這部分我將其分為兩類:
一類是公共基礎(chǔ)庫,用于跨產(chǎn)品使用;
一類是產(chǎn)品基礎(chǔ)庫,在某個產(chǎn)品中強相關(guān)依賴使用。

這里以我們自己產(chǎn)品劃分為例,概述一下這兩類庫都包括哪些基礎(chǔ)組件:

公共庫包括:組件化中間件、網(wǎng)絡(luò)診斷、第三方SDK管理封裝、長連接相關(guān)、Patch相關(guān)、網(wǎng)絡(luò)和頁面監(jiān)控相關(guān)、用戶行為統(tǒng)計庫、
          第三方分享庫、JSBridge相關(guān)、關(guān)于Device+file+crypt+http的基礎(chǔ)方法等。

產(chǎn)品基礎(chǔ)庫包括:通用的WebViewContainer組件(封裝了JSBridge)、自定義數(shù)字鍵盤、表情鍵盤、自定義下拉列表、
             循環(huán)滾動頁面、AFNeworking封裝庫(對上層業(yè)務(wù)隱藏AF的直接引用)、以及其他自定義的UI基礎(chǔ)組件庫。
組件化第二步-獨立業(yè)務(wù)模塊單獨成庫
在基礎(chǔ)庫成體系的基礎(chǔ)上(基礎(chǔ)依賴),下面需要對業(yè)務(wù)模塊之間(橫向的依賴)進行拆解。這部分是比較難也是容易碰到問題的。
我們可以按照需求定性將一些相對獨立的業(yè)務(wù)模塊獨立成庫,單獨在一個工程上進行開發(fā)、測試。

往往在這個階段有一個誤區(qū),千萬不能為了組件化而強行將一些耦合嚴(yán)重的業(yè)務(wù)模塊分出。如果在拆分過程中,
拆分模塊跟其他模塊耦合太嚴(yán)重,那就先放棄這部分模塊的獨立,畢竟產(chǎn)品是不會單獨拿出時間給你做組件化的。

另外拆分的粒度需要大一點,需要在功能模塊的基礎(chǔ)上,將業(yè)務(wù)獨立性考慮進去,如果沒有就不拆,等以后有了相對獨立的模塊之后再拆。
組件化第三步-對外服務(wù)接口最小化
組件化不是一蹴而就的,我們在完成第二步的時候并不要強行要求去掉組件之間代碼的硬依賴,
只需要保證單獨拆分出來的工程可以獨立運行和測試,并且能夠通過引用保證其他業(yè)務(wù)組件和主工程的依賴使用即可。

當(dāng)?shù)诙酵瓿芍?,我們可以在此基礎(chǔ)上總結(jié)其他組件和主工程的需求調(diào)用,
根據(jù)需求總結(jié)和抽象出當(dāng)前業(yè)務(wù)組件對外服務(wù)的最小化接口以及頁面跳轉(zhuǎn)調(diào)用。

這樣,最后基主工程就相當(dāng)于剩下一個空殼需要做的就是通過中間件解耦合各業(yè)務(wù)模塊。

CTMediator 方式的組件化

Casa (文章) 對 iOS 組件化方案的討論

調(diào)用方式

先說本地應(yīng)用調(diào)用,本地組件A在某處調(diào)用[[CTMediator sharedInstance] performTarget:targetName
action:actionName params:@{...}]向CTMediator發(fā)起跨組件調(diào)用,CTMediator根據(jù)獲得的target和action信息,
通過objective-C的runtime轉(zhuǎn)化生成target實例以及對應(yīng)的action選擇子,然后最終調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯,完成需求。

在遠(yuǎn)程應(yīng)用調(diào)用中,遠(yuǎn)程應(yīng)用通過openURL的方式,由iOS系統(tǒng)根據(jù)info.plist里的scheme配置找到可以響應(yīng)URL的應(yīng)用
 (在當(dāng)前我們討論的上下文中,這就是你自己的應(yīng)用),應(yīng)用通過AppDelegate接收到URL之后,
調(diào)用CTMediator的openUrl:方法將接收到的URL信息傳入。當(dāng)然,CTMediator也可以用openUrl:options:
的方式順便把隨之而來的option也接收,這取決于你本地業(yè)務(wù)執(zhí)行邏輯時的充要條件是否包含option數(shù)據(jù)。傳入URL之后,
CTMediator通過解析URL,將請求路由到對應(yīng)的target和action,隨后的過程就變成了上面說過的本地應(yīng)用調(diào)用的過程了,
最終完成響應(yīng)。

當(dāng)決定要實施組件化方案時,對于組件化方案的架構(gòu)設(shè)計優(yōu)劣直接影響到架構(gòu)體系能否長遠(yuǎn)地支持未來業(yè)務(wù)的發(fā)展,
對App的組件化不只是僅僅的拆代碼和跨業(yè)務(wù)調(diào)頁面,還要考慮復(fù)雜和非常規(guī)業(yè)務(wù)參數(shù)參與的調(diào)度,非頁面的跨組件功能調(diào)度,
組件調(diào)度安全保障,組件間解耦,新舊業(yè)務(wù)的調(diào)用接口修改等問題。

1. target-action做的事情都是跨業(yè)務(wù)調(diào)度的事情,它不是簡單地把方法換個位置。你先理解什么事跨業(yè)務(wù)調(diào)度。

2. category的目的是在調(diào)度的時候,調(diào)度的人不用去考慮應(yīng)該具體調(diào)哪個target-action,以及參數(shù)都有哪些,類型都是什么。
   category通過函數(shù)參數(shù)的方式收集參數(shù)并生成調(diào)用target-action時的param字典。
   category不會利用runtime去做事情,真正利用runtime的是CTMediator,真正做事情的是target-action

3. 同一。跨業(yè)務(wù)調(diào)度的時候,A業(yè)務(wù)有些功能需要B業(yè)務(wù)幫忙做點兒事。那么B業(yè)務(wù)要幫忙的事兒就都寫在target-action里,
   給A業(yè)務(wù)調(diào)度。

既然用runtime就可以解耦取消依賴,那還要Mediator做什么?組件間調(diào)用時直接用runtime接口調(diào)不就行了,這樣就可以沒有任何依賴就完成調(diào)用:

但這樣做的問題是:

  • 調(diào)用者寫起來很惡心,代碼提示都沒有,每次調(diào)用寫一坨。
  • runtime方法的參數(shù)個數(shù)和類型限制,導(dǎo)致只能每個接口都統(tǒng)一傳一個 NSDictionary。這個 NSDictionary里的key value是什么不明確,需要找個地方寫文檔說明和查看。
  • 編譯器層面不依賴其他組件,實際上還是依賴了,直接在這里調(diào)用,沒有引入調(diào)用的組件時就掛了。

把它移到Mediator后:

  • 調(diào)用者寫起來不惡心,代碼提示也有了。
  • 參數(shù)類型和個數(shù)無限制,由 Mediator 去轉(zhuǎn)就行了,組件提供的還是一個 NSDictionary 參數(shù)的接口,但在Mediator 里可以提供任意類型和個數(shù)的參數(shù),像上面的例子顯式要求參數(shù) NSString
    bookId 和 NSInteger type。
  • Mediator可以做統(tǒng)一處理,調(diào)用某個組件方法時如果某個組件不存在,可以做相應(yīng)操作,讓調(diào)用者與組件間沒有耦合。

到這里,基本上能解決我們的問題:各組件互不依賴,組件間調(diào)用只依賴中間件Mediator,Mediator不依賴其他組件。接下來就是優(yōu)化這套寫法,有兩個優(yōu)化點:

1. Mediator 每一個方法里都要寫 runtime 方法,格式是確定的,這是可以抽取出來的。
2. 每個組件對外方法都要在 Mediator 寫一遍,組件一多 Mediator 類的長度是恐怖的。

優(yōu)化后就成了 casa 的方案,target-action 對應(yīng)第一點,target就是class,action就是selector,通過一些規(guī)則簡化動態(tài)調(diào)用。Category 對應(yīng)第二點,每個組件寫一個 Mediator 的 Category,讓 Mediator 不至于太長。

總結(jié)起來就是:

1.各組件可以只專注于自身的業(yè)務(wù)設(shè)計,最后通過無侵入的 target-action 方式為外界提供接口調(diào)用,這個 target-action 設(shè)計的很精妙。
2.組件間通過中間件通信,中間件通過 runtime 和 組件的 target-action 解耦合。不依賴于任何組件。
3. 組件通過中間件的 category 實現(xiàn)對外的接口調(diào)用,這部分由提供服務(wù)的組件開發(fā)者維護,使得外界的調(diào)用者不用參與調(diào)用的內(nèi)部邏輯設(shè)計,而且具有多處復(fù)用的效果,調(diào)用者引入中間件即可,這是一種輕依賴,是權(quán)衡后的設(shè)計。而且通過 category 感官上分離組件接口代碼。

組件化解決的痛點和帶來的優(yōu)勢

在 iOS Native app 前期開發(fā)的時候,如果參與的開發(fā)人員也不多,那么代碼大多數(shù)都是寫在一個工程里面的,
這個時候業(yè)務(wù)發(fā)展也不是太快,所以很多時候也能保證開發(fā)效率。

但是一旦項目工程龐大以后,開發(fā)人員也會逐漸多起來,業(yè)務(wù)發(fā)展突飛猛進,這個時候單一的工程開發(fā)模式就會暴露出弊端了。

  - 項目內(nèi)代碼文件耦合比較嚴(yán)重
  - 容易出現(xiàn)沖突,大公司同時開發(fā)一個項目的人多,每次 pull 一下最新代碼就會有很多沖突,有時候合并代碼需要半個小時左右,
    這會耽誤開發(fā)效率。
  - 業(yè)務(wù)方的開發(fā)效率不夠高,開發(fā)人員一多,每個人都只想關(guān)心自己的組件,但是卻要編譯整個項目,與其他不相干的代碼糅合在一起。
    調(diào)試起來也不方便,即使開發(fā)一個很小的功能,都要去把整個項目都編譯一遍,調(diào)試效率低。

  為了解決這些問題,iOS 項目就出現(xiàn)了組件化的概念。所以 iOS 的組件化是為了解決上述這些問題的,
  這里與前端組件化解決的痛點不同。

iOS 組件化以后能帶來如下的好處:

- 不只提高了代碼的復(fù)用度,還可以實現(xiàn)真正的功能復(fù)用,比如同樣的功能模塊如果實現(xiàn)了自完備性,可以在多個app中復(fù)用
- 加快編譯速度,各組件做成 Framwork 這樣可以加快編譯速度,對源碼也可以隱藏起來!
 (不用編譯主客那一大坨代碼了,各個組件都是靜態(tài)庫)
- 自由選擇開發(fā)姿勢(MVC / MVVM / FRP)
- 方便 QA 有針對性地測試
- 提高業(yè)務(wù)開發(fā)效率
- 業(yè)務(wù)隔離,跨團隊開發(fā)代碼控制和版本風(fēng)險控制的實現(xiàn) 

缺點,模塊化當(dāng)然也有它的缺點:

- 入門門檻較高,新手入門需要的成本也更高
- 工具的使用成本,團隊間和模塊間的配合成本升高,開發(fā)效率短期會降低。

但是從長期的影響來說,帶來的好處遠(yuǎn)大于壞處的,因此模塊化仍然是最佳的架構(gòu)選擇。

小結(jié)

最終想要達到的理想目標(biāo)就是主工程就是一個殼工程,其他所有代碼都在組件 Pods 里面,主工程的工作就是初始化,加載這些組件的,沒有其他任何代碼了。

注意:組件化方案不適合在業(yè)務(wù)不穩(wěn)定的情況下過早實施,至少要等產(chǎn)品已經(jīng)經(jīng)過MVP階段時才適合實施組件化。因為業(yè)務(wù)不穩(wěn)定意味著鏈路不穩(wěn)定,在不穩(wěn)定的鏈路上實施組件化會導(dǎo)致將來主業(yè)務(wù)產(chǎn)生變化時,全局性模塊調(diào)度和重構(gòu)會變得相對復(fù)雜。


*參考文章:
iOS應(yīng)用架構(gòu)談 組件化方案
iOS組件化實踐方案-LDBusMediator煉就
淺析 iOS 應(yīng)用組件化設(shè)計
模塊化與解耦
iOS 組件化方案探索
組件化架構(gòu)漫談
組件化方案調(diào)研
一個iOS模塊化開發(fā)解決方案
iOS組件化文章集合

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容