iOS 組件化方案

原文地址:iOS 組件化方案

摘要:最近在思考團隊擴張及項目數(shù)量增加的情況下,如何持續(xù)保障團隊高效產(chǎn)出的問題,很自然的想到了組件化這個話題。重翻了前段時間iOS開發(fā)圈關于組件化的討論,這里做下梳理和自己的思考。組件化的驅動力在開始討論組件化技術方案之前,可以先思考下驅動項目組件化背后的原動力。我們假設這樣一個場景,公司有A,B,C三個項目在appstore運作,三個項目分別由TeamA,TeamB,TeamC開發(fā)維護,每個Team由五名工程師組成,其中一名擔任小組長,三個Team之上再配備一位Leader,一位?

最近在思考團隊擴張及項目數(shù)量增加的情況下,如何持續(xù)保障團隊高效產(chǎn)出的問題,很自然的想到了組件化這個話題。重翻了前段時間iOS開發(fā)圈關于組件化的討論,這里做下梳理和自己的思考。

組件化的驅動力?

在開始討論組件化技術方案之前,可以先思考下驅動項目組件化背后的原動力。我們假設這樣一個場景,公司有 A,B,C三個項目在appstore運作,三個項目分別由Team A,Team B,Team C開發(fā)維護,每個Team由五名工程師組成,其中一名擔任小組長,三個Team之上再配備一位Leader,一位架構師。這時,公司決定開辟新的業(yè)務領域,成立項目D,并新招了5名工程師來開發(fā)。架構師和Leader此時首要工作是選定技術方案,讓項目D能又快又穩(wěn)的啟動,同時要規(guī)避新工程師磨合期可能引入的副作用。如果之前有過組件化的設計,項目D可以重用之前A,B,C的部分組件,比如【用戶登錄】,【內(nèi)存管理】,【日志打點系統(tǒng)】,【個人Profile模塊】等等,新成員也可以在已有的codebase基礎之上快速上手。如果沒有做過組件化的處理,那么要從A,B,C中抽離出諸如【用戶登錄】的獨立模塊,會相當?shù)耐纯?高度耦合的代碼盤根錯節(jié),重用起來費時費力,對團隊的人力是浪費,更影響整體的項目進度。我們的目標是重用高度抽象的代碼單元。

回到組件化的技術方案,最早是Limboy分享了一篇蘑菇街組件化的技術方案,接著Casa提出了不同意見,后來Limboy在Casa反饋之上對自己方案做了進一步優(yōu)化,最后Bang在前三篇文章基礎之上做了清晰的梳理總結。通讀之后,獲益頗多,組件化所面臨的問題,和可能的解決思路也變得更清晰。

組件的定義?

首先需要對組件進行定義,叫組件也好,模塊也罷,我們姑且認為我們討論的范疇是【獨立的業(yè)務或者功能單位】。至于這個單位的粒度大小,需要工程師自己把握。當我們寫一個類的時候,我們會謹記高內(nèi)聚,低耦合的原則去設計這個類,當涉及多個類之間交互的時候,我們也會運用SOLID原則,或者已有的設計模式去優(yōu)化設計,但在實現(xiàn)完整的業(yè)務模塊的時候,我們很容易忘記對這個模塊去做設計上的思考,粒度越大,越難做出精細穩(wěn)定的設計,我暫且把這個粒度認為是組件的粒度。組件是由一個或多個類構成,能完整描述一個業(yè)務場景,并能被其他業(yè)務場景復用的功能單位。組件就像是PC時代個人組裝電腦時購買的一個個部件,比如內(nèi)存,硬盤,CPU,顯示器等,拿出其中任何一個部件都能被其他的PC所使用。

所以組件可以是個廣義上的概念,并不一定是頁面跳轉,還可以是其他不具備UI屬性的服務提供者,比如日志服務,VOIP服務,內(nèi)存管理服務等等。說白了我們目標是站在更高的維度去封裝功能單元。對這些功能單元進行進一步的分類,才能在具體的業(yè)務場景下做更合理的設計。按我個人經(jīng)驗可以將組件分為以下幾類:

帶UI屬性的獨立業(yè)務模塊。?

不具備UI屬性的獨立業(yè)務模塊。?

不具備業(yè)務場景的功能模塊。?

第一類是Limboy,Casa討論較多的組件,這些組件有很具體的業(yè)務場景。比如一個App的主頁模塊,從Server獲取列表,并通過controller展示。這類模塊一般有個入口Controller,可以通過Push或Present的方式作為入口接入。電商類App的大部分場景都可以歸于這一類,Controller作為頁面的基本單位和Web Page有很高的相似度,我想這也是為什么蘑菇街會采取URL注冊的實現(xiàn)方式,用URL來標記本地的每一個Controller,不僅方便本地的跳轉,還能支持Server下發(fā)跳轉指令,對運營團隊來說再合適不過。從理論上來說,組件化和URL本身并沒有什么聯(lián)系,URL只是接入組件的方式之一,這種接入方式還存在一定局限性,比如無法傳遞像UIImage這類非primitive數(shù)據(jù)。這種局限性在電商app業(yè)務環(huán)境下,會帶來多少副作用值得商榷,按我的經(jīng)驗,在完整獨立的業(yè)務模塊間傳遞復雜對象的場景并不多,即使有也可以通過memory cache或者disk cache來做中轉。我沒記錯的話,之前天貓無線客戶端不同業(yè)務模塊間跳轉也是通過URL的方式來實現(xiàn)的,有個類似Router的中間類來出來URL的解析及跳轉,并沒有Mediator去對組件做進一步的封裝。以URL注冊方式來接入組件,在副作用小,業(yè)務運營方便的背景下,蘑菇街的選擇或許并不能算作‘’錯誤的方向“。

第二類業(yè)務模塊不具備UI場景,但卻和具體的業(yè)務相關。比如日志上報模塊,app可能需要統(tǒng)計用戶注冊模塊每個Controller進入的路徑,便于分析每一步用戶的流失率。這類業(yè)務模塊如果要用URL去表達和接入會顯得非常變扭。試想下通過如下的代碼調(diào)用啟用日志:

[[MGJRouter sharedInstance] openURL:@"mgj://log/start" withParams:@{}];?

[[MGJRouter sharedInstance] openURL:@"mgj://log/start" withParams:@{}];?

這也是蘑菇街以URL方案來實現(xiàn)組件化不合理的地方,按Casa的分法,組件被調(diào)用分為遠程和本地,這種日志服務的調(diào)用是本地類型的調(diào)用,用URL來標這類記本地服務頗有些繞遠路的感覺。

第三類模塊和具體的業(yè)務場景無關,比如Database模塊,提供數(shù)據(jù)的讀寫服務,包含多線程的處理。比如Network模塊,提供和Server數(shù)據(jù)交互的方式,包含并發(fā)數(shù)控制,網(wǎng)絡優(yōu)化等處理。比如圖片處理類,提供異步繪制圓角頭像。這些模塊可以被任意模塊使用,但不和任何業(yè)務相關。這種組件屬于我們app的基礎服務提供者,更像是一個個SDK,或是toolkit。我不知道蘑菇街是怎么處理這類組件接入的,很明顯URL的接入方式并不適合。我們通過Pods使用的很多著名第三方庫都屬于這一類,像FMDB,SDWebImage等。

接下來我們再看看各家方案對上面三種組件的接入能力及優(yōu)缺點。

蘑菇街的URL方案?

首先從上面的分析可以看出,這種方案在針對第一類組件是并沒有什么大問題,只是不太適合第二類和第三類組件。

URL方案在啟動的時候有個模塊初始化的過程,初始化的時候注冊模塊自己提供的各種服務:

[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {?

NSNumber *id = routerParameters[@"id"];?

// create view controller with id?

// push view controller?

}];?

[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {NSNumber *id = routerParameters[@"id"];// create view controller with id// push view controller}];?

組件的使用方使用的時候通過傳入具體的URL Pattern來完成調(diào)用:

[MGJRouter openURL:@"mgj://detail?id=404"]?

[MGJRouter openURL:@"mgj://detail?id=404"]?

Bang針對這種方式提出了三個問題:

需要有個地方列出各個組件里有什么 URL 接口可供調(diào)用。蘑菇街做了個后臺專門管理。?

每個組件都需要初始化,內(nèi)存里需要保存一份表,組件多了會有內(nèi)存問題。?

參數(shù)的格式不明確,是個靈活的 dictionary,也需要有個地方可以查參數(shù)格式。?

第一個問題是最明顯的問題,組件的使用方必須通過查閱web文檔之后,再手寫string來完成調(diào)用。這種組件調(diào)用方式確實會有一定的效率問題。

第二個問題所說的表和內(nèi)存問題我沒理解具體是指哪一塊。我算了下Router當中的額外內(nèi)存開銷,一個用來存儲Mapping的NSMutableDictionary,iOS App當中使用Dictionary的場景會很多,Dictionary帶來的內(nèi)存開銷主要看其所強引用的key和value。二是以URLPattern為Key的各種string,這個估計是大頭,但Casa的方案里將Action以String的方式hardcode,也會導致這些String常住內(nèi)存,其本質(zhì)是將原本處于Text區(qū)的函數(shù)符號換成了位于Data區(qū)的string,此消彼長,這部分內(nèi)存消耗也在正常范圍之內(nèi),最后是handler block,這部分開銷也屬于常規(guī)使用,和一次函數(shù)調(diào)用并沒有本質(zhì)區(qū)別,看上去內(nèi)存消耗總量并沒有特別增長,或許還有其他我沒考慮到的部分。

第三個問題其實和第一個問題是類似的,需要查閱文檔來hardcode參數(shù)名稱。

在我看來這種URL注冊的方式本質(zhì)是以string來替換原本的函數(shù)聲明,string可以避免頭文件引用,實現(xiàn)了編譯上的解耦,但付出的代價是沒有接口和參數(shù)聲明,給組件使用方的效率帶來了影響。

MGJRouter其實也是充當了Mediator的角色,只不過是大部分時候是在組件和組件使用方之間傳遞數(shù)據(jù)。Router如果自己解析URL,也可以加入中間邏輯來判斷組件是否存在等。

Casa的Mediator方案?

Casa在提出Mediator方案之前,首先指出了蘑菇街方案混淆本地調(diào)用和遠程調(diào)用的問題。這點很有意義,將組件化的使用場景描述的更明確。

Casa提出了Mediator方案,他的方案當中Mediator承接了大部分的組件接入代碼,可以用如下圖示:

圖中虛線箭頭表示Casa所提出的”通過runtime發(fā)現(xiàn)服務的過程“,Bang也認為虛線箭頭部分實現(xiàn)了解耦,不需要import頭文件,可以通過runtime來完成組件的接入。

這里我對”發(fā)現(xiàn)服務“這個概念存有疑惑,我所了解的wsdl可以用來發(fā)現(xiàn)web sevice所提供的具體服務,你需要發(fā)送一個web請求來獲取wsdl文件,這可以稱作是”發(fā)現(xiàn)服務“的過程。但是使用OC的runtime機制以String來完成函數(shù)調(diào)用是”使用服務“的一種方式,你還是需要組件方提供額外文檔來描述具體有哪些服務,不然從何處去”發(fā)現(xiàn)“這些String呢?所以私以為runtime并不能發(fā)現(xiàn)服務,只是換了一種方式去調(diào)用服務,把原來的[object sendMessage]換成了[object performSelector:@””]。當然runtime的方式看起來沒有耦合。

這里我們再來探討下耦合的概念,我們可以從多種維度去理解耦合,import頭文件算一種耦合,因為頭文件缺失會導致編譯出錯。業(yè)務耦合是另一種維度的耦合,我不認為業(yè)務的耦合可以被消除多少,你需要使用的組件服務因為業(yè)務需要一個都不能少,如果組件方修改了業(yè)務接口,即使你能編譯通過,你所調(diào)用的組件也無法正常工作了。你可以選擇不同的調(diào)用方式,但調(diào)用本身是一定存在的,我在上圖中用虛線箭頭表示了這種業(yè)務耦合,它無法被消除,可以從語法上,從代碼技巧上去”弱化“,但這種”弱化“也有其代價。

這種代價和蘑菇街URL注冊方式是同一種代價,以String來替換原先的函數(shù)和參數(shù)聲明,配合runtime來完成組件調(diào)用。這種方式同樣會加大接入的難度,我們來看下Casa Demo的工程結構:

Mediator對組件的使用方提供了Category來暴露所支持的服務,對使用方來說看上去很清晰。但Mediator其實也是由組件使用方來維護的,我們看看Mediator當中的代碼。CTMediator+CTMediatorModuleAActions.m當中完成一個服務接入的代碼如下:

//CTMediator+CTMediatorModuleAActions.m?

NSString * const kCTMediatorTargetA = @"A";?

NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";- (UIViewController *)CTMediator_viewControllerForDetail?

{?

UIViewController *viewController = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController?

params:@{@"key":@"value"}];?

if ([viewController isKindOfClass:[UIViewController class]]) {?

// view controller 交付出去之后,可以由外界選擇是push還是present?

return viewController;?

} else {?

// 這里處理異常場景,具體如何處理取決于產(chǎn)品?

return [[UIViewController alloc] init];?

}?

}?

//CTMediator+CTMediatorModuleAActions.mNSString * const kCTMediatorTargetA = @"A";NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";- (UIViewController *)CTMediator_viewControllerForDetail{UIViewController *viewController = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewControllerparams:@{@"key":@"value"}];if ([viewController isKindOfClass:[UIViewController class]]) {// view controller 交付出去之后,可以由外界選擇是push還是presentreturn viewController;} else {// 這里處理異常場景,具體如何處理取決于產(chǎn)品return [[UIViewController alloc] init];}}?

Target,Action,Params全是用String去描述的,這里會有個問題:

如果組件使用團隊在杭州,組件開發(fā)團隊位于北京,如何去獲取這些String?

如果是通過web文檔的方式,那么使用方需要依照文檔將Target,Action,每個Param全部手敲一遍,一個都不能出錯,傳入param value的時候要看清楚對方是需要long還是NSNumber,因為沒有類型檢查,只能靠肉眼。如果沒有文檔,使用方需要自己查看組件的頭文件,再把頭文件當中暴露的接口翻譯成String。這個方式看起來效率并不高且易出錯,尤其是在組件數(shù)量多的情況下。

DemoModule下有兩個問題。

第一是target在解析組件param的時候需要再次的hardcode:

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params?

{?

// 因為action是從屬于ModuleA的,所以action直接可以使用ModuleA里的所有聲明?

DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];?

viewController.valueLabel.text = params[@"key"];?

return viewController;?

}?

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params{// 因為action是從屬于ModuleA的,所以action直接可以使用ModuleA里的所有聲明DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];viewController.valueLabel.text = params[@"key"];return viewController;}?

同一個@”key“同時出現(xiàn)在了組件方和組件調(diào)用方,我不知道該如何去高效的協(xié)調(diào)這種hardcode,或許還是只能依賴web文檔,但查文檔對于程序員編寫代碼來說是個低效的過程。

第二個問題是參數(shù)是以Dictionary傳入的,我不知道有多少開發(fā)SDK或者組件的團隊會選擇以Dictionary的方式定義”函數(shù)入?yún)ⅰ?。使用Dictionary很符合Casa”去Model化“的風格,我對于Casa所提的”去Model化“始終存疑,我仔細讀過其博客關于“去model化”的解釋,也拜讀了Martin Fowler反對Anemic Domain Model的文章,Martin Fowler并沒有反對使用model,而是提倡讓model去承擔更多的domain logic。就我個人寫代碼體驗而言,使用model來描述數(shù)據(jù)比dictionary更清晰直觀,這里使用顯示的函數(shù)入?yún)⒙暶饕哺庇^。第三方庫在提供接口的時候也鮮少有以Dictionary作為入?yún)⒌摹?/p>

從上面兩個問題可以看出,Mediator的方式并沒有減少組件使用方的接入工作,反而因為要降低耦合,使用runtime,在hardcode String上引入了額外的人力消耗。

Protocol+Version方案?

Bang在梳理各種方案的時候畫了兩張很有意思的圖:

第一張圖看上去雜亂無章,互相耦合。第二張圖通過Mediator將結構變得清晰很多。

這兩張圖其實表達了一個業(yè)界經(jīng)典的話題:Distributed Design Vs Centralized Design

第一張圖看上去是一坨,但它卻是典型的Distributed Design。第二種圖更符合人腦的”審美“,Centralized在結構上更容易被大腦梳理清楚。具體到工程場景,孰優(yōu)孰劣還真不一定。

不知道大家有沒有了解過IP協(xié)議的路由尋址算法,這也是Distributed Design Vs Centralized Design的一個經(jīng)典場景。如果采用Centralized Design,我們可以用一個cache空間無限大,packet處理能力沒有瓶頸的中央路由器來”瞬時“的算出兩個路由器之間的最短路徑,但顯然并不存在這樣的路由器?,F(xiàn)實是每個路由器所能緩存的周邊路由器信息相當有限,packet處理能力也十分有限,結果是每個路由器只能在自己所認知的范圍內(nèi)算最短路徑,但這就是今天的互聯(lián)網(wǎng)所使用的設計,Distributed Design。

Centralized設計在Node增加的情形下會增加中央節(jié)點的負擔。Mediator就是這個中央節(jié)點,工作量并沒有減少,未來的風險不可預知。

我個人在組件化上還是傾向于Distributed Design。各個組件”自掃門前雪“,用規(guī)范的protocol聲明,加上嚴格的版本控制來提供組件服務。姑且稱之為Protocol+Version方案。

這種方案可以分兩部分去講解。

Protocol?

選擇protocol作為接入方式會有一定程度的耦合,畢竟需要@import。protocol所帶來的耦合介于runtime和類的 .h文件之間,protocol相較于runtime雖然存在頭文件的編譯耦合,但在業(yè)務描述上更加清晰,函數(shù)名稱和參數(shù)類型都有明確定義,很多時候甚至不需要查閱文檔就能明白組件的使用方式。我個人更偏向于使用protocol作為組件的接入和使用方式。我們用兩種類型的protocol來規(guī)范組件。

組件通用protocol

不同的組件類型接入的方式也不同。

第三類組件屬于基礎組件,類似工具箱。我們所使用的大部分第三方庫都屬于這一類,平時一般使用CocoaPods直接接入,講究一點的話可以對這些第三方庫接口再做一層封裝,再升級或替換的時候會更省力。大廠一般都會編寫自己的基礎組件,放到私有的Pods源。這類組件往往比較穩(wěn)定,適合已Framework的方式集成,我們在接入的時候不需要做特別的處理。

第一類和第二類組件都具備業(yè)務場景和業(yè)務狀態(tài),他們的接入和業(yè)務聯(lián)系緊密,需要有專門的protocol來定義他們的行為。這個protocol用來規(guī)定每個組件通用的行為,以及組件完整生命周期的一些回調(diào)處理。類似:

@protocol IAppModule NSObject>?

//module life cycle?

- (void)initModule;?

- (void)destroyModule;?

//common behavior?

- (NSString*)getModuleVersion;?

- (BOOL)handleUrl:(NSString*)url;?

- (UIViewController*)getDefaultController;?

@end?

@protocol IAppModule NSObject>//module life cycle- (void)initModule;- (void)destroyModule;//common behavior- (NSString*)getModuleVersion;- (BOOL)handleUrl:(NSString*)url;- (UIViewController*)getDefaultController;@end?

每一個組件如果單獨編譯可以作為一個獨立的App,所以應該能經(jīng)歷一個iOS App的完整生命周期。

在didFinishLaunchingWithOptions的時候initModule。

在退出或需要銷毀組件的時候調(diào)用destroyModule。

至于applicationWillResignActive,applicationWillEnterForeground等可以在組件當中通過通知自行處理。

針對外部URL跳轉的場景用如下代碼處理:

for (int i = 0; imodule = _modules[i];?

if ([module respondsToSelector:@selector(handleUrl:)]) {?

BOOL ret = [module handleUrl:url];?

if (ret) {?

break;?

}?

}?

}?

for (int i = 0; imodule = _modules[i];if ([module respondsToSelector:@selector(handleUrl:)]) {BOOL ret = [module handleUrl:url];if (ret) {break;}}}?

Url Pattern需要有個統(tǒng)一的web后臺管理頁面,各組件需要注冊自己的Controller。

對于需要接入Controller的場景(第一類組件,有入口Controller),如下處理:

id homeModule = [HomeModule new];?

[homeModule initModule];?

if ([homeModule respondsToSelector:@selector(getDefaultController)]) {?

UIViewController* defaultCtrl = [homeModule getDefaultController];?

if (defaultCtrl) {?

[self.navigationController pushViewController:defaultCtrl animated:true];?

}?

}?

id homeModule = [HomeModule new];[homeModule initModule];if ([homeModule respondsToSelector:@selector(getDefaultController)]) {UIViewController* defaultCtrl = [homeModule getDefaultController];if (defaultCtrl) {[self.navigationController pushViewController:defaultCtrl animated:true];}}?

隨著接入的業(yè)務越來越多,業(yè)務組件的形態(tài)應更加多樣化,我們可能需要在IAppModule加入更多的通用接口來規(guī)范行為。

組件業(yè)務protocol

組件都需要自己的業(yè)務protocol,業(yè)務protocol能完整的描述該組件所提供的業(yè)務清單。不需要查閱額外文檔就能大致了解業(yè)務的類型和細節(jié),這得益于OC詳細到甚至啰嗦的方法簽名。也是protocol較之runtime的優(yōu)勢所在。比如我們需要導入購物車組件:

//IOrderCartModule.h?

@protocol IOrderCartModule NSObject>?

- (int)getOrderCount;?

- (Order*)getOrderByID:(NSString*)orderID;?

- (void)insertNewOrder:(Order*)order;?

- (void)removeOrderByID:(NSString*)orderID;?

- (void)clearCart;?

@end?

//?aliyunzixun@xxx.com?IOrderCartModule NSObject>- (int)getOrderCount;- (Order*)getOrderByID:(NSString*)orderID;- (void)insertNewOrder:(Order*)order;- (void)removeOrderByID:(NSString*)orderID;- (void)clearCart;@end?

//OrderCartModule?

@interface OrderCartModule : NSObject IAppModule, IOrderCartModule>?

@end?

//?aliyunzixun@xxx.com?OrderCartModule : NSObject IAppModule, IOrderCartModule>@end?

直接@import IOrderCartModule, @import OrderCartModule就可以開始使用購物車組件。

id orderCart = [OrderCartModule new];?

int orderCount = [orderCart getOrderCount];?

lbOrderCount.text = @(orderCount).stringValue;?

id orderCart = [OrderCartModule new];int orderCount = [orderCart getOrderCount];lbOrderCount.text = @(orderCount).stringValue;?

組件的生成代碼需要統(tǒng)一管理,所以我們需要一個ModuleManager來管理接入的業(yè)務組件(遵循IAppModule的組件),包含組件的初始化和生命周期管理等等。

//ModuleManager.h?

@interface ModuleManager : NSObject?

+ (instancetype)sharedInstance;?

- (idIOrderCartModule>)getOrderCartModule;?

- (void)handleModuleURL:(NSString*)url;?

@end?

//?aliyunzixun@xxx.com?ModuleManager : NSObject+ (instancetype)sharedInstance;- (idIOrderCartModule>)getOrderCartModule;- (void)handleModuleURL:(NSString*)url;@end?

ModuleManager只負責管理組件的聲明周期,及通用的組件行為。不會像MGJRouter做URL注冊,也不需要像Mediator做接口的再次封裝。

再看下這種組件接入方式帶來的耦合:

除了引入IOrderCartModule.h, OrderCartModule.h之外,還有一些model也被引用了,比如

- (void)insertNewOrder:(Order*)order;?

- (void)insertNewOrder:(Order*)order;?

這里涉及到復雜業(yè)務對象的描述,至于到底是引入Order.h還是使用NSDictionary來描述又是一次取舍。我個人還傾向于使用model來描述,和使用protocol而非runtime的理由一致,更清晰更直觀。不可否認這種方式耦合度會更高一些,我們看下實際工程當中對我們開發(fā)會帶來哪些影響。

假設購物車組件是由團隊D開發(fā)完成,第一版本的Order定義如下:

@interface Order : NSObject?

@property (nonatomic, strong) NSString* orderID;?

@property (nonatomic, strong) NSString* orderName;?

@end?

@interface Order :?aliyunzixun@xxx.com?(nonatomic, strong) NSString* orderID;@property (nonatomic, strong) NSString* orderName;@end?

第二版本的Order新增功能可以查詢訂單的生成時間:

@interface Order : NSObject?

@property (nonatomic, strong) NSString* orderID;?

@property (nonatomic, strong) NSString* orderName;?

@property (nonatomic, strong) NSNumber* createdDate;?

@end?

@interface Order :?aliyunzixun@xxx.com?(nonatomic, strong) NSString* orderID;@property (nonatomic, strong) NSString* orderName;@property (nonatomic, strong) NSNumber* createdDate;@end?

這種場景對組件接入方幾乎沒有影響,屬于新增功能,createdDate是否使用取決于接入方的業(yè)務進展。

但如果是改變orderID的管理方式:

@interface Order : NSObject?

@property (nonatomic, strong) NSNumber* orderID;?

@property (nonatomic, strong) NSString* orderName;?

@end?

@interface Order :?aliyunzixun@xxx.com?(nonatomic, strong) NSNumber* orderID;@property (nonatomic, strong) NSString* orderName;@end?

將原本的NSString換成了NSNumber,這種改變會產(chǎn)生較大的影響,組件接入方所有使用orderID的地方都需要將類型做一次修改。這是不是說明import model的方式實際效率較差呢?假設我們是使用NSDitionary來描述Order數(shù)據(jù),接入方?jīng)]法第一時間通過編譯來發(fā)現(xiàn)Order改變,需要調(diào)試在runtime的crash場景下發(fā)現(xiàn)type的改變,反而不如使用model效率高。因為這種場景下的業(yè)務改動是屬于必須去適配的,所以我們更需要的是一種快速定位組件變化的方式來更新組件。業(yè)務的接入本身就是“侵入式”的,即使在語言層面做了隔離,組件的改變還是會牽動接入方的改變,否則新的業(yè)務邏輯如何生效呢?

可見我們的重點不是如何在語言層面去降低業(yè)務耦合,而是通過合理的流程去規(guī)范組件的演進和變化,也就是我們組件方案的第二部分Version Control。

Version Control?

我們可以通過Semantic Versioning來規(guī)范我們組件的版本演進方式,再配合CocoaPods進行版本配置。Semantic Versioning定義如下:

Given a version number MAJOR.MINOR.PATCH, increment the:?MAJOR version when you make incompatible API changes,MINOR version when you add functionality in a backwards-compatible manner, and?PATCH version when you make backwards-compatible bug fixes.?Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

所以上述orderID類型的修改需要改變Major版本號,組件接入方看到Major的更新,可以在第一時間安排更新計劃。

最后我們可以得到如下的架構圖:

底部的三類組件就是我們總體的組件庫,任何新啟的項目都可以從這三類組件當中選取合適的組件作為codebase。

這類還值得一提的話題是組件的粒度,在什么時候我們需要重新抽象一個新的組件。我個人認為并不是所有的業(yè)務模塊都適合抽象成組件,現(xiàn)在移動互聯(lián)網(wǎng)公司業(yè)務變化都非常快,大部分的業(yè)務都不會被重用,不被重用的模塊去花精力做封裝設計并不劃算,另外還會造成組件庫的膨脹和維護問題。至于哪些業(yè)務需要被抽象成組件,需要各小組組長也移動端總架構師去溝通協(xié)商。一個5人小團隊內(nèi)部將不同的tab都做組件化的封裝是多此一舉,可能反而會延緩項目進度。比如Project A里的首頁模塊,用戶詳情頁被其他Project復用的可能性非常小,組件化有其代價存在。

Dependency Hell

組件過多的時候很容易出現(xiàn)Dependency Hell的問題,比如上圖中購物車組件和支付組件依賴于不同版本的log組件,解決這種依賴沖突會耗費額外的團隊溝通時間,反而會因為組件化降低開發(fā)效率。

總結?

說了這么多組件化方式,最后還是回到了最基礎的protocol方案,大巧不工,返璞歸真的方案可能是更好的方案,runtime雖然巧妙,又有多少語言自帶runtime屬性。當然我個人并沒有大量組件化的實戰(zhàn)經(jīng)驗,以上都是理論分析,一家之言,具體業(yè)務環(huán)境下是否需要組件化,在我看來是個值得權衡的問題。對于小型的創(chuàng)業(yè)團隊,去實施組件化到底能有多少“效率”收益呢?

原文地址:iOS 組件化方案

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

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

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