前言:當(dāng)業(yè)務(wù)需求量和團(tuán)隊(duì)規(guī)模達(dá)到一定程度后,任何一款A(yù)pp都需要考慮架構(gòu)設(shè)計(jì)的合理性。
簡單架構(gòu)向大型項(xiàng)目架構(gòu)演進(jìn)中,需要解決三個(gè)問題:
1、模塊粒度應(yīng)該如何劃分?
2、如何分層?
3、多團(tuán)隊(duì)如何寫作?
而這三條之中,第一條模塊粒度的劃分是非常關(guān)鍵的一補(bǔ),也是一個(gè)細(xì)活,最好可以在不同階段采用不同的粒度劃分模塊。
大項(xiàng)目、多人、多團(tuán)隊(duì)架構(gòu)思考
接下來,先要了解模塊粒度應(yīng)該怎么劃分的問題。
首先,項(xiàng)目規(guī)模變大后,模塊劃分必須遵循一定的原則。如果模塊劃分規(guī)則不規(guī)范不清晰,就會(huì)導(dǎo)致代碼耦合嚴(yán)重的問題,并加大架構(gòu)重構(gòu)的難度。這些問題主要表現(xiàn)在:
- 業(yè)務(wù)需求不斷,業(yè)務(wù)開發(fā)不能停。重新劃分模塊的工作量越大,成本越高,重構(gòu)拍上日程的難度也就越大。
- 老業(yè)務(wù)代碼年久失修,沒有注釋,修改起來需要重新梳理邏輯和關(guān)系,耗時(shí)長。
其次,搞清楚模塊粒度采用什么標(biāo)準(zhǔn)劃分,就是要遵循的原則是什么。對于iOS這種面相對象編程的開發(fā)模式來說,應(yīng)該遵循以下五個(gè)原則,即SOLID原則。
- 單一功能原則: 對象功能要單一,不要在一個(gè)對象里添加很多功能。
- 開閉原則: 擴(kuò)展是開放的,修改是封閉的。
- 里氏替換原則: 子類對象是可以代替基類對象的。
- 接口隔離原則: 接口的用途要單一,不要在一個(gè)接口上根據(jù)不同入?yún)?shí)現(xiàn)多個(gè)功能。
- 依賴反轉(zhuǎn)原則: 方法應(yīng)該依賴抽象,不要依賴實(shí)例。iOS開發(fā)就是高層業(yè)務(wù)方法依賴于協(xié)議。
最后,需要選擇合適的粒度,切記,大型項(xiàng)目的模塊粒度過大或者過小都不合適。
其中,組件可以認(rèn)為是可組裝的、獨(dú)立的業(yè)務(wù)單元,具有高內(nèi)聚,低耦合的特性,是一種比較適中的粒度。就像用樂高拼房子一樣,每個(gè)對象就是一塊小積木。一個(gè)組件就是由一塊一塊的小積木組成的有單一功能的組合,比如門、柱子、煙囪。
在我看來,iOS 開發(fā)中的組件,不是 UI 的控件,也不是 ViewController 這種大 UI 和功能的集合。因?yàn)椋琔I 控件的粒度太小,而頁面的粒度又太大。iOS 組件,應(yīng)該是包含 UI 控件、相關(guān)多個(gè)小功能的合集,是一種粒度適中的模塊。
并且,采用組件的話,對于代碼邏輯和模塊間的通信方式的改動(dòng)都不大,完成老代碼切換也就相對容易些。我們可以先按照物理劃分,也就是將多個(gè)相同功能的類移動(dòng)到同一個(gè)文件夾下,然后做成 CocoaPods 的包進(jìn)行管理。
但是,僅做到這一步還不夠,因?yàn)楣δ苣K之間的耦合還是沒有被解除。如果沒有解除耦合關(guān)系的話,不同功能的開發(fā)還是沒法獨(dú)立開來,勉強(qiáng)開發(fā)完成后的影響范圍評估也難以確定。
所以接下來,就需要重新梳理組件之間的邏輯關(guān)系,進(jìn)行改造。
但是,組件解耦并不是說要求每個(gè)組件間都沒有耦合,組件間也需要有上下層依賴的關(guān)系。組件間的上下層關(guān)系劃分清楚了,就會(huì)容易維護(hù)和管理。而對于組件間如何分層這個(gè)問題,感覺層級最多不要超過三個(gè),可以這么設(shè)置:
- 底層可以是與業(yè)務(wù)無關(guān)的基礎(chǔ)組件,比如網(wǎng)絡(luò)和存儲(chǔ)等;
- 中間層一般是通用的業(yè)務(wù)組件,比如賬號、埋點(diǎn)、支付、購物車等;
- 最上層是迭代業(yè)務(wù)組件,更新頻率最高。
這樣的三層結(jié)構(gòu),尤其有利于多個(gè)團(tuán)隊(duì)分別開發(fā)維護(hù)。比如,一開始有兩個(gè)業(yè)務(wù)團(tuán)隊(duì) A 和 B,他們在開發(fā)時(shí)既有通用的功能、賬號、埋點(diǎn)、個(gè)人頁等,也有專有的業(yè)務(wù)功能模塊,每個(gè)功能都是一個(gè)組件。
這樣,新創(chuàng)建的業(yè)務(wù)團(tuán)隊(duì) C,就能非常輕松地使用團(tuán)隊(duì) A 和 B 開發(fā)出的通用組件。而且,如果兩個(gè)業(yè)務(wù)團(tuán)隊(duì)有相同功能時(shí),對相應(yīng)的功能組件進(jìn)行簡單改造后,也能同時(shí)適用于兩個(gè)業(yè)務(wù)團(tuán)隊(duì)。
但是,不用把所有的功能都做成組件,只有那些會(huì)被多個(gè)業(yè)務(wù)或者團(tuán)隊(duì)使用的功能模塊才需要做成組件。因?yàn)椋脑斐山M件也是需要時(shí)間成本的,很少有公司愿意完全停下業(yè)務(wù)去進(jìn)行重構(gòu),而一旦決定某業(yè)務(wù)功能模塊要改成組件,就要抓住機(jī)會(huì),嚴(yán)格按照 SOLID 原則去改造組件,因?yàn)榉倒ず驮賰?yōu)化的機(jī)會(huì)可能不會(huì)再有。
多團(tuán)隊(duì)之間如何分工?
在代碼層面,我們通過組件化解決了大項(xiàng)目、多人、多團(tuán)隊(duì)架構(gòu)的問題,但是架構(gòu)問題還涉及到團(tuán)隊(duì)人員結(jié)構(gòu)上的架構(gòu)。當(dāng)公司或者集團(tuán)的 App 多了后,相應(yīng)的團(tuán)隊(duì)也就多了,為了能夠讓產(chǎn)品快速迭代和穩(wěn)定發(fā)展,也需要一個(gè)合理的團(tuán)隊(duì)結(jié)構(gòu)。在我看來,這個(gè)合理的團(tuán)隊(duì)結(jié)構(gòu)應(yīng)該是這樣的:
- 首先,需要一個(gè)專門的基建團(tuán)隊(duì),負(fù)責(zé)業(yè)務(wù)無關(guān)的基礎(chǔ)功能組件和業(yè)務(wù)相關(guān)通用業(yè)務(wù)組件的開發(fā)。
- 然后,每個(gè)業(yè)務(wù)都由一個(gè)專門的團(tuán)隊(duì)來負(fù)責(zé)開發(fā)。業(yè)務(wù)可以按照功能耦合度來劃分,耦合度高的業(yè)務(wù)可以劃分成單獨(dú)的業(yè)務(wù)團(tuán)隊(duì)。
- 其次,基建團(tuán)隊(duì)人員應(yīng)該是流動(dòng)的,從業(yè)務(wù)團(tuán)隊(duì)里來,再回到業(yè)務(wù)團(tuán)隊(duì)中去。這么設(shè)計(jì)是因?yàn)闃I(yè)務(wù)團(tuán)隊(duì)和基建團(tuán)隊(duì)的邊界不應(yīng)該非常明顯,否則就會(huì)出現(xiàn)基建團(tuán)隊(duì)埋頭苦干,結(jié)果可能是做得過多、做得不夠,或著功能不好用的問題,造成嚴(yán)重的資源浪費(fèi)。
總結(jié)來講,團(tuán)隊(duì)分工要靈活,不要把人員隔離固化了,否則各干各的,做的東西相互都不用。核心上,團(tuán)隊(duì)分工還是要圍繞著具體業(yè)務(wù)進(jìn)行功能模塊提煉,去解決重復(fù)建設(shè)的問題,在這個(gè)基礎(chǔ)上把提煉出的模塊做精做扎實(shí)。
我心目中好的架構(gòu)是什么樣的?
現(xiàn)在,我們已經(jīng)可以從代碼內(nèi)外來分析 App 開發(fā)的架構(gòu)設(shè)計(jì)了,但也只是會(huì)分析了而已,腦海中并沒有明確好的架構(gòu)是什么樣的,也不知道具體應(yīng)該怎么設(shè)計(jì)。接下來,我們就帶著這兩個(gè)問題繼續(xù)看下面的內(nèi)容。
組件化是解決項(xiàng)目大、人員多的一種很好的手段,這在任何公司或團(tuán)隊(duì)都是沒有歧義的。組件間關(guān)系協(xié)調(diào)卻沒有固定的標(biāo)準(zhǔn),協(xié)調(diào)的優(yōu)劣,成為了衡量架構(gòu)優(yōu)劣的一個(gè)基本標(biāo)準(zhǔn)。所以在實(shí)踐中,一般分為了協(xié)議式和中間者兩種架構(gòu)設(shè)計(jì)方案。
協(xié)議式架構(gòu)設(shè)計(jì)主要采用的是協(xié)議式編程的思路:在編譯層面使用協(xié)議定義規(guī)范,實(shí)現(xiàn)可在不同地方,從而達(dá)到分布管理和維護(hù)組件的目的。這種方式也遵循了依賴反轉(zhuǎn)原則,是一種很好的面向?qū)ο缶幊痰膶?shí)踐。但是,這個(gè)方案的缺點(diǎn)也很明顯,主要體現(xiàn)在以下兩個(gè)方面:
1、由于協(xié)議式編程缺少統(tǒng)一調(diào)度層,導(dǎo)致難于集中管理,特別是項(xiàng)目規(guī)模變大、團(tuán)隊(duì)變多的情況下,架構(gòu)管控就會(huì)顯得越來越重要。
2、協(xié)議式編程接口定義模式過于規(guī)范,從而使得架構(gòu)的靈活性不夠高。當(dāng)需要引入一個(gè)新的設(shè)計(jì)模式來開發(fā)時(shí),我們就會(huì)發(fā)現(xiàn)很難融入到當(dāng)前架構(gòu)中,缺乏架構(gòu)的統(tǒng)一性。
雖然協(xié)議式架構(gòu)有這兩方面的局限性,但其簡單易用的特點(diǎn)依然被很多人推崇。
另一種常用的架構(gòu)形式是中間者架構(gòu)。它采用中間者統(tǒng)一管理的方式,來控制 App 的整個(gè)生命周期中組件間的調(diào)用關(guān)系。同時(shí),iOS 對于組件接口的設(shè)計(jì)也需要保持一致性,方便中間者統(tǒng)一調(diào)用。
拆分的組件都依賴于中間者,組間之間不存在相互依賴的關(guān)系。由于其他組件都會(huì)依賴于這個(gè)中間者,相互間的通信都會(huì)通過中間者統(tǒng)一調(diào)度,所以組件間的通信也就更容易管理了。在中間者上也能夠輕松添加新的設(shè)計(jì)模式,從而使得架構(gòu)更容易擴(kuò)展。
好的架構(gòu)一定是健壯的、靈活的。中間者架構(gòu)的易管控帶來的架構(gòu)更穩(wěn)固,易擴(kuò)展帶來的靈活性,所以中間者這種架構(gòu)設(shè)計(jì)模式是非常值得推薦的。casatwy 以前設(shè)計(jì)了一個(gè) CTMediator就是按照中間者架構(gòu)思路設(shè)計(jì)的。
CTMediator 使用的是運(yùn)行時(shí)解耦,可以通過開源的 CTMediator 代碼,一起查看下如何使用運(yùn)行時(shí)技術(shù)來解耦。解耦核心方法如下所示:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這里是處理無響應(yīng)請求的地方之一,這個(gè)demo做得比較簡單,如果沒有可以響應(yīng)的target,就直接return了。實(shí)際開發(fā)過程中是可以事先給一個(gè)固定的target專門用于在這個(gè)時(shí)候頂上,然后處理這種請求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里是處理無響應(yīng)請求的地方,如果無響應(yīng),則嘗試調(diào)用對應(yīng)target的notFound方法統(tǒng)一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這里也是處理無響應(yīng)請求的地方,在notFound都沒有的時(shí)候,這個(gè)demo是直接return了。實(shí)際開發(fā)過程中,可以用前面提到的固定的target頂上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
performTarget:action:params:shouldCacheTarget: 方法主要是對 targetName 和 actionName 進(jìn)行容錯(cuò)處理,也就是對調(diào)用方法無響應(yīng)的處理。這個(gè)方法封裝了 safePerformAction:target:params 方法,入?yún)?targetName 就是調(diào)用接口的對象,actionName 是調(diào)用的方法名,params 是參數(shù)。
從代碼中同時(shí)還能看出只有滿足 Target_ 前綴的對象和 Action 的方法才能被 CTMediator 使用。這時(shí),我們可以看出中間者架構(gòu)的優(yōu)勢,也就是利于統(tǒng)一管理,可以輕松管控制定的規(guī)則。
下面這段代碼,是使用 CTMediator 如何調(diào)用一個(gè)彈窗顯示方法的代碼示范:
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionShowAlert
params:paramsToSend
shouldCacheTarget:NO];
可以看出,指定了對象名和調(diào)用方法名,把參數(shù)封裝成字典傳進(jìn)去就能夠直接調(diào)用該方法了。
但是,這種運(yùn)行時(shí)直接硬編碼的調(diào)用方式也有些缺點(diǎn),主要表現(xiàn)在兩個(gè)方面:
1、直接硬編碼的調(diào)用方式,參數(shù)是以 string 的方法保存在內(nèi)存里,雖然和將參數(shù)保存在 Text 字段里占用的內(nèi)存差不多,同時(shí)還可以避免.h 文件的耦合,但是其對代碼編寫效率的降低也比較明顯。
2、由于是在運(yùn)行時(shí)才確定的調(diào)用方法,調(diào)用方式由 [obj method] 變成 [obj performSelector:@""]。這樣的話,在調(diào)用時(shí)就缺少類型檢查,是個(gè)很大的缺憾。因?yàn)椋绻椒ê蛥?shù)比較多的時(shí)候,代碼編寫效率就會(huì)比較低。
但是我們可以通過casatwy給的建議完美解決這兩個(gè)問題。下面是 casatwy 的原話。
CTMediator 本質(zhì)就是一個(gè)方法,用來接收 target、action、params。由于 target、action 都是字符串,params 是字典,對于調(diào)用者來說十分不友好,因?yàn)檎{(diào)用者要寫字符串,而且調(diào)用的時(shí)候若是不看文檔,他也不知道這個(gè)字典里該塞什么東西。
所以實(shí)際情況中,調(diào)用者是不會(huì)直接調(diào)用 CTMediator 的方法的。那調(diào)用者怎么發(fā)起調(diào)用呢?通過響應(yīng)者給 CTMediator 做的 category 或者 extension 發(fā)起調(diào)用。
category 或 extension 以函數(shù)聲明的方式,解決了參數(shù)的問題。調(diào)用者看這個(gè)函數(shù)長什么樣子,就知道給哪些參數(shù)。在 category 或 extension 的方法實(shí)現(xiàn)中,把參數(shù)字典化,順便把 target、action 這倆字符串寫死在調(diào)用里。
于是,對于調(diào)用者來說,他就不必查文檔去看參數(shù)怎么給,也不必?fù)?dān)心 target、action 字符串是什么了。這個(gè) category 是一個(gè)獨(dú)立的 Pod,由響應(yīng)者業(yè)務(wù)的開發(fā)給到。
所以,當(dāng)一個(gè)工程師開發(fā)一個(gè)業(yè)務(wù)的時(shí)候,他會(huì)開發(fā)兩個(gè) Pod,一個(gè)是 category Pod,一個(gè)是自己本身的業(yè)務(wù) Pod。這樣就完美解決了 CTMediator 它自身的缺點(diǎn)。
對于調(diào)用者來說,他不會(huì)直接依賴 CTMediator 去發(fā)起調(diào)用,而是直接依賴 category Pod 去發(fā)起調(diào)用的。這么一來,CTMediator 方案就完美了。
然后還有一點(diǎn)可能需要強(qiáng)調(diào):基于 CTMediator 方案的工程,每一個(gè)組件無所謂是 OC 還是 Swift,Pod 也無所謂是 category 還是 extension。也就是說,假設(shè)一個(gè)工程由 100 個(gè)組件組成,那可以是 50 個(gè) OC、50 個(gè) Swift。因?yàn)?CTMediator 抹去了不同語言的組件之間的隔閡,所以大家老的 OC 工程可以先應(yīng)用 CTMediator,把組件拆出來。然后新的業(yè)務(wù)來了,用 Swift 寫,等有空的時(shí)候再把老的 OC 改成 Swift,或者不改,都是沒問題的。
不過,解耦的精髓在于業(yè)務(wù)邏輯能夠獨(dú)立出來,并不是形式上的解除編譯上的耦合(編譯上解除耦合只能算是解耦的一種手段而已)。所以,在考慮架構(gòu)設(shè)計(jì)時(shí),我們更多的還是需要在功能邏輯和組件劃分上做到同層級解耦,上下層依賴清晰,這樣的結(jié)構(gòu)才能夠使得上層組件易插拔,下層組件更穩(wěn)固。而中間者架構(gòu)模式更容易維護(hù)這種結(jié)構(gòu),中間者的易管控和易擴(kuò)展性,也使得整體架構(gòu)能夠長期保持穩(wěn)健與活力。所以,中間者架構(gòu)就是我心目中好的架構(gòu)。
小結(jié)
架構(gòu)的設(shè)計(jì)絕對不是要等到工程到了燃眉之急之時(shí),再去環(huán)顧其他公司或團(tuán)隊(duì)在用什么架構(gòu),然后拍腦袋拿一個(gè)過來,來次大重構(gòu)。好的架構(gòu),需要在業(yè)務(wù)開發(fā)過程中及早發(fā)現(xiàn)開發(fā)的痛點(diǎn),進(jìn)行有針對性的改良,不然就會(huì)和實(shí)際開發(fā)越走越遠(yuǎn)。
比如,某個(gè)業(yè)務(wù)模塊的邏輯非常復(fù)雜、狀態(tài)有很多,這時(shí)我們就需要在架構(gòu)層面考慮如何處理會(huì)更方便,改動(dòng)最小的支持狀態(tài)機(jī)模式,又或者在開始架構(gòu)設(shè)計(jì)時(shí)就多考慮如何將架構(gòu)設(shè)計(jì)的具有更高的易用性和可擴(kuò)展性。
好的架構(gòu)是能夠在一定的規(guī)范內(nèi)同時(shí)支持高靈活度,這種度的把握是需要架構(gòu)師長期跟隨團(tuán)隊(duì)開發(fā),隨著實(shí)際業(yè)務(wù)需求的演進(jìn)進(jìn)行分析和把控的。
在項(xiàng)目大了,人員多了的情況下,好的架構(gòu)一定是不簡單的,不通用的,但一定是接地氣的,這樣才能更適合自己的團(tuán)隊(duì),才能夠用得上。那些大而全,炫技,脫離業(yè)務(wù)開發(fā)需求的架構(gòu)是沒法落地的。
作為一名普通的開發(fā)者,除了日常需求開發(fā)和技術(shù)方案調(diào)研、設(shè)計(jì)外,還需要了解自己所在項(xiàng)目的整體架構(gòu)是怎樣的,想想架構(gòu)上哪些地方是不夠好需要改進(jìn)的,業(yè)界有哪些好的架構(gòu)思想是可以落地到自己項(xiàng)目中的。有了從項(xiàng)目整體上去思考的意識,才能夠站在更高的視角上去思考問題。