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

前言

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

為什么需要進(jìn)行組件化

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

組件化的期望:

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

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

模塊設(shè)計(jì)原則

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

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

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

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

基礎(chǔ)層模塊

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

中間層模塊

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

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

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

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

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

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

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

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

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

另外拆分的粒度需要大一點(diǎn),需要在功能模塊的基礎(chǔ)上,將業(yè)務(wù)獨(dú)立性考慮進(jìn)去,如果沒(méi)有就不拆,等以后有了相對(duì)獨(dú)立的模塊之后再拆。
組件化第三步-對(duì)外服務(wù)接口最小化
組件化不是一蹴而就的,我們?cè)谕瓿傻诙降臅r(shí)候并不要強(qiáng)行要求去掉組件之間代碼的硬依賴,
只需要保證單獨(dú)拆分出來(lái)的工程可以獨(dú)立運(yùn)行和測(cè)試,并且能夠通過(guò)引用保證其他業(yè)務(wù)組件和主工程的依賴使用即可。

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

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

CTMediator 方式的組件化

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

調(diào)用方式

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

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

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

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

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

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

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

但這樣做的問(wèn)題是:

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

把它移到Mediator后:

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

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

1. Mediator 每一個(gè)方法里都要寫(xiě) runtime 方法,格式是確定的,這是可以抽取出來(lái)的。
2. 每個(gè)組件對(duì)外方法都要在 Mediator 寫(xiě)一遍,組件一多 Mediator 類的長(zhǎng)度是恐怖的。

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

總結(jié)起來(lái)就是:

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

組件化解決的痛點(diǎn)和帶來(lái)的優(yōu)勢(shì)

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

但是一旦項(xiàng)目工程龐大以后,開(kāi)發(fā)人員也會(huì)逐漸多起來(lái),業(yè)務(wù)發(fā)展突飛猛進(jìn),這個(gè)時(shí)候單一的工程開(kāi)發(fā)模式就會(huì)暴露出弊端了。

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

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

iOS 組件化以后能帶來(lái)如下的好處:

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

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

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

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

小結(jié)

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

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


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

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

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

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