一篇開源代碼的組件化方案
關于組件化
網(wǎng)上組件化的文章很多胶征。很多文章一提到組件化塞椎,就會說解耦,一說到解耦就會說路由或者runtime睛低。好像組件化 == 解耦 == 路由/Runtime案狠,然而這是一個非常錯誤的觀念。持有這一觀點的人钱雷,沒有搞清楚在組件化中什么是想要結(jié)果骂铁,什么是過程。
組件化和解耦
大家不妨先思考兩個問題:
1罩抗、為何要進行組件化開發(fā)拉庵?
2、各個組件之間是否一定需要解耦套蒂?
采用組件化钞支,是為了組件能單獨開發(fā)茫蛹,單獨開發(fā)是結(jié)果。要讓組件能單獨開發(fā)烁挟,組件必須職責單一婴洼,職責單一需要用到重構和解耦的技術,所以重構和解耦是過程撼嗓。那解耦是否是必須的過程柬采?不一定。比如UIKit且警,我們用這個系統(tǒng)組件并沒有使用任何解耦手段警没。問題來了,UIKit蘋果可以獨立開發(fā)振湾,我們使用它為什么沒用解耦手段?答案很簡單亡脸,UIKit沒有依賴我們的代碼所以不用解耦押搪。
PS:我這里不糾結(jié)組件、服務浅碾、模塊大州、框架的概念,網(wǎng)上對這些概念的定義五花八門垂谢,實際上把簡單的事說復雜了厦画。我這里只關心一件事,這一部分代碼能否獨立開發(fā)滥朱,能就叫組件根暑,不能我管你叫什么
我們之所以要解耦才能獨立開發(fā),通常是出現(xiàn)了循環(huán)依賴徙邻。這時候當然可以無腦的用路由把兩個組件的耦合解開排嫌,也可以獨立開發(fā)。然而缰犁,這樣做只是把強引用改成了弱引用淳地,代碼還是爛代碼。站在重構的角度來說帅容,A颇象、B組件循環(huán)依賴就是設計有問題,要么應該重構A并徘、B讓依賴單向遣钳;要么應該抽離一個共用組件C,讓A饮亏、B組件都只依賴于C耍贾。
如果我們每個組件都只是單向依賴其他組件阅爽,各個組件之間也就沒有必要解耦。再換個角度說荐开,如果一個組件職責不單一付翁,即使跟其他組件解耦了,組件依然不能很好的工作晃听。如何解耦只是重構過程中可選手段百侧,代碼設計的原則如依賴倒置、接口隔離能扒、里氏替換佣渴,都可以指導我們寫出好的組件。
所以在組件化中重要的是讓組件職責單一初斑,職責單一的重要標志之一就是沒有組件間的循環(huán)依賴辛润。
架構圖
一般來講,App的組件可以分為三層见秤,上層業(yè)務組件砂竖、中層UI組件、底層SDK組件
同一層之間的組件互相獨立鹃答,上層的組件耦合下層的組件乎澄。一般來講,底層SDK組件和中層UI組件都是獨立的功能测摔,不會出現(xiàn)同層耦合置济。
業(yè)務組件解耦
上層業(yè)務組件之間的解耦,采用依賴注入的方式實現(xiàn)锋八。每個模塊都聲明一個自己依賴的協(xié)議浙于,在App集成方里去實現(xiàn)這些協(xié)議。
我之前的做法是每個模塊用協(xié)議提供自己對外的能力查库,其他模塊通過協(xié)議來訪問它路媚。這樣做雖然也可以解耦,但是維護成本很高樊销,每個模塊都要去理解其他模塊整慎。同時也引入了其他模塊自己用不到的功能,不符合最小依賴的原則围苫。
使用依賴注入裤园,APP集成方統(tǒng)一去管理各個模塊的依賴,每個模塊也能單獨編譯剂府,是業(yè)務層解耦的最佳實踐拧揽。
包管理
要解除循環(huán)依賴,引入包管理技術cocoapods會讓我們更有效率。pod不允許組件間有循環(huán)依賴淤袜,若有pod install時就會報錯痒谴。
cocoapods,提供私有pod repo铡羡,使用時把自己的組件放在私有pod repo里积蔚,然后在Podfile里直接通過pod命令集成。一個組件對應一個私有pod烦周,每個組件依賴自己所需要的三方庫尽爆。多個組件聯(lián)合開發(fā)的時候,可以再一個podspec里配置子模塊读慎,這樣在每個組件自己的podspec里漱贱,只需要把子模塊里的pod依賴關系拷貝過去就行了。
在多個組件集成時會有版本沖突的問題夭委。比如登錄組件(L)幅狮、廣告組件(A)都依賴了埋點組件(O),L依賴O的1.1版本株灸,A依賴O的1.2版本彪笼,這時候集成就會報錯。為了解決這個錯誤蚂且,在組件間依賴時,不寫版本號幅恋,版本號只在APP集成方寫杏死。即podfile里引用所有組件,并寫上版本號捆交,.podspec里不寫版本號淑翼。
這樣做既可以保證APP集成方的穩(wěn)定性,也可以解決組件依賴的版本沖突問題品追。這樣做的壞處是玄括,所有組件包括App集成方,在使用其他組件時肉瓦,都必須使用其他組件最新的API遭京,這會造成額外的升級工作量。如果不想接受組件升級最新api的成本泞莉,可以私有化一個三方庫自己維護哪雕。
組件開發(fā)完畢后告訴集成方,目前的組件穩(wěn)定版本是多少鲫趁,引用的三方庫穩(wěn)定版本集成方自己去決定
另一種版本管理的方式斯嚎,是在podspec里寫依賴組件的版本號,podfile里不寫組件依賴的版本,然后通過內(nèi)部溝通來解決版本沖突的問題堡僻。我認為雖然也能做糠惫,但有很多弊端。
1.作為App集成方钉疫,沒辦法單獨控制依賴的三方庫版本苍凛。三方庫升級會更復雜
2.每個依賴的三方庫,都應該做了完整的單元測試访圃,才能被集成到App中肴甸。所以正確的邏輯不是組件內(nèi)測試過三方庫沒問題就在組件內(nèi)寫死版本號,而是這個三方庫經(jīng)過我們測試后咨油,可以在我們系統(tǒng)中使用XX版本您炉。
3.在工程中就沒有一個地方能完整知道所有的pod組件,而App集成方有權利知道這一點
4.溝通成本高
順便說一句役电,基礎組件庫可以通過pod子模塊單獨暴露獨立功能赚爵,較常用。
以上法瑟,就是組件化的所有東西冀膝。你可能會奇怪,解耦在組件化過程中有什么用霎挟。答案是解耦是為了更好的實現(xiàn)組件的單一職責窝剖,解耦的作用在架構設計中談。需要再次強調(diào)酥夭,組件化 ≠ 解耦赐纱。
如果非要給組件化下一個定義,我的理解是:
組件化意味著重構熬北,目的是讓每個組件職責單一疙描。在結(jié)構上,每個組件都最小依賴它所需要的東西讶隐。
關于架構設計
在我看來起胰,iOS客戶端架構主要為了解決兩個問題,一是解決大型項目分組件開發(fā)的效率的問題巫延,二是解決單進程App的穩(wěn)定性的問題效五。
設計到架構設計的都是大型App,小型App主要是業(yè)務的堆疊炉峰。很多公司在業(yè)務初期都不會考慮架構火俄,在業(yè)務發(fā)展到一定規(guī)模的時候,才會重新審視架構混亂帶來的開發(fā)效率和業(yè)務穩(wěn)定性瓶頸讲冠。這時候就會引入組件化的概念瓜客,我們常常面臨的是對已有項目的組件化,這一過程會異常困難。
組件拆分原則
對老工程的組件拆分谱仪,我的辦法是玻熙,從底層開始拆。SDK> 模塊 > 業(yè)務 疯攒。如果App沒有SDK可以抽離嗦随,就從模塊開始拆,不要為了抽離SDK而抽離敬尺。常見的誤區(qū)是枚尼,大家一拿到代碼就把公共函數(shù)提出來作為共用框架,起的名字還特別接地氣砂吞,如XXCommon署恍。
事實上,這種框架型SDK蜻直,是最雞肋的組件盯质,原因是它實用性很小,無非就是減少了點冗余代碼概而。而且在架構能力不強的情況下呼巷,它很容易變成“垃圾堆”,什么東西都想往里面放赎瑰,后面越來越龐大王悍。所以,開始拆分架構的時候餐曼,盡量以業(yè)務優(yōu)先配名,比如先拆分享模塊。
如果兩個組件中有共同的函數(shù)晋辆,前期不要想著提出來,改個名字讓它冗余是更好的辦法宇整。如果共同耦合的是一個靜態(tài)庫瓶佳,可以利用動態(tài)庫的隔離性封裝靜態(tài)庫,具體方法可以網(wǎng)上找鳞青。
響應式
基礎組件常常要在系統(tǒng)啟動時初始化霸饲,或者接受App生命周期時間。這就引出了個問題臂拓,如何給appDelegate瘦身厚脉?比如我們現(xiàn)在有兩個基礎組件A、B胶惰,他們都需要監(jiān)聽App生命周期事件傻工,傳統(tǒng)的做法是,A、B兩個組件都提供一些函數(shù)在appDelegate中調(diào)用中捆。但這樣做的壞處是鸯匹,如果某一天我不想引入B組件了,還得去改appDelegate代碼泄伪。理想的方式是殴蓬,基礎組件的使用不需要在appDelegate里寫代碼
為了實現(xiàn)基礎組件與appDelegate分離,得對appDelegate改造蟋滴。首先得提出一個觀點染厅,蘋果的appDelegate設計的有問題,它在用代理模式解決觀察者模式的問題津函。在《設計模式》中肖粮,代理模式的設計意圖定義是:為其他對象提供一種代理以控制對這個對象的訪問。反過來看appDelegate你會發(fā)現(xiàn)球散,它大部分代理函數(shù)都沒有辦法控制application尿赚,如applicationDidBecomeActive。applicationDidBecomeActive這種事件常常需要多個處理者蕉堰,這種場景用觀察者模式更適合凌净。而openURL需要返回BOOL值,才需要使用代理模式屋讶。App生命周期事件雖然可以用監(jiān)聽通知獲取冰寻,但用起來不如響應式監(jiān)聽信號方便。
基于響應式編程的思想皿渗,我寫了一個TLAppEventBus斩芭,提供屬性來監(jiān)聽生命周期事件。我并不喜歡龐大的ReactiveObjectC乐疆,所以我通過category實現(xiàn)了簡單的響應式划乖,用戶只需要監(jiān)聽需要的信號即可。在TLAppEventBus里挤土,我默認提供了8個系統(tǒng)事件用來監(jiān)聽琴庵,如果有其他的系統(tǒng)事件需要監(jiān)聽,可以使用擴展的方法仰美,給TLAppEventBus添加屬性(見文末Demo)迷殿。
路由
對于Appdelegate中的openURL的事件,蘋果使用代理模式并沒有問題咖杂,但我們常常需要在openURL里面寫if-else區(qū)分事件的處理者庆寺,這也會造成多個URL處理模塊耦合在Appdelegate中。我認為appdelegate中的openURL應該用路由轉(zhuǎn)發(fā)的方式來解耦诉字。
openURL代理需要同步返回處理結(jié)果懦尝,但網(wǎng)上開源的路由框架能同步返回結(jié)果的知纷。所以我這邊實現(xiàn)了一個能同步返回結(jié)果的路由TLRouter,同時支持了注冊scheme导披。注冊scheme這一特性屈扎,在第三方分享的場景下會比較有用(見文末Demo)。
另外撩匕,網(wǎng)上大部分方案都搞錯了場景鹰晨。以蘑菇街的路由方案為例(好像iOS的路由就是他們提出來的?)止毕,蘑菇街認為路由主要有兩個作用模蜡,一是發(fā)送數(shù)據(jù)讓路由接收者處理,二是返回對象讓路由發(fā)送者繼續(xù)處理扁凛。我不禁想問忍疾,這是路由嗎?不妨先回到URL的定義
URL: 統(tǒng)一資源標識符(Uniform Resource Locator,統(tǒng)一資源定位符)是一個用于標識某一互聯(lián)網(wǎng)資源名稱的字符串
openURL就是在訪問資源谨朝,在瀏覽器中卤妒,openURL意味著打開一個網(wǎng)頁,openURL的發(fā)起者并不關心打開的內(nèi)容是什么字币,只關心打開的結(jié)果则披。所以蘋果的openURL Api 就只返回了除了結(jié)果YES/NO,沒有返回一個對象洗出。所以士复,我對openURL這一行為定義如下
openURL:訪問資源,返回是否訪問成功
那把蘑菇街的路由翩活,返回的對象改成BOOL值就可以了么阱洪?我認為還不夠。對于客戶端的路由菠镇,使用的實際上是通知的形式在解耦冗荸,帶來的問題是路由的注冊代碼散落在各地,所以路由方案必須要配路由文檔利耍,要不然開發(fā)者會不知道路由在干嘛蚌本。
有沒有比文檔更好的方式呢?我的思路是:用schema區(qū)分路由職責
系統(tǒng)的openURL只干了兩件事:打開App和打開網(wǎng)頁
[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"weixin://"]]; // 打開App
[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"https://www.baidu.com"]];//打開網(wǎng)頁
兩者的共性是頁面切換堂竟。所以我這邊設計的路由openURL,只擴充了controller跳轉(zhuǎn)的功能玻佩,比如打開登錄頁
[TLRouter openURL:@"innerJump://account/login"];
只擴充了controller跳轉(zhuǎn)的功能好處是讓路由的職責更單一出嘹,同時也更符合蘋果對openURL的定義。工程師在看到url schema的時候就知道他的作用咬崔,避免反復查看文檔税稼。
對于數(shù)據(jù)的傳遞烦秩,我認為不應該用路由的方式。相比路由郎仆,通過依賴注入傳入信號是更好的選擇只祠。
App配置
有時候我們需要組件的跨App復用,在App集成組件時扰肌,能夠不改代碼只改配置是最理想的方式抛寝。使用組件+plist配置是一個方案,具體做法是把A組件的配置放在A.plist中曙旭,在A組件內(nèi)寫死要讀取A.plist盗舰。
以配置代替硬編碼,防止對代碼的侵入桂躏,是一個很好的思路钻趋。設想一下,如果我們可以通過配置在決定App是否使用組件剂习、也可通過配置來改變組件和app所需的參數(shù)蛮位,那運維可以代替app開發(fā)來出包,這對效率和穩(wěn)定性都會有提升鳞绕。為了實現(xiàn)這一效果失仁,我使用了OC的runtime來動態(tài)注冊組件。需要在didfinishLaunch初始化的組件猾昆,可以實現(xiàn)代理 - (void)initializeWhenLaunch; 這樣陶因,自動初始化函數(shù),就可以通過runtime+plist里配置的class name自動初始化垂蜗。組件需要初始化的代碼楷扬,可以在自己的initializeWhenLaunch里做。
由于路由只擴充了controller跳轉(zhuǎn)的功能贴见,所以路由注冊這一行為也可進行一次抽象烘苹,把不同的部分放在plist配置文件,相同的放到runtime里做片部。這樣做還有個好處是镣衡,程序內(nèi)的路由跳轉(zhuǎn)在一個plist里可以都可以看到
iOS解耦工具Tourelle
Tourelle,是根據(jù)上面的思路寫的一個開源項目 https://github.com/zhudaye12138/Tourelle档悠,可以通過pod集成 pod 'Tourelle'廊鸥。下面介紹一下他的使用方式
TLAppEventBus
TLAppEventBus通過接收系統(tǒng)通知來獲取app生命周期事件,收到生命周期事件后改變對應屬性的值辖所。默認提供了didEnterBackground等八個屬性惰说,可以使用響應式函數(shù)來監(jiān)聽
- (void)observeWithBlock:(TLObservingBlock)block;
[TLAppEventBus.shared.didBecomeActive observeWithBlock:^(idnewValue) {
//do some thing }];
需要注意,如果在其它地方使用observeWithBlock缘回,需要設置屬性的owner吆视,否則沒有辦法監(jiān)聽到典挑。這里不用單獨設置是因為在TLAppEventBus里已設置好
TLAppEventBus使用前需要調(diào)用 - (void)start; 如果需要監(jiān)聽更多的事件,可以調(diào)用
- (void)startWithNotificationMap:(NSDictionary *)map;
NSMutableDictionary *defaultMap = [NSMutableDictionary dictionaryWithDictionary:[TLAppEventBus defaultNotificationMap]]; //獲取默認map
[defaultMapsetObject:KDidChangeStatusBarOrientation forKey:UIApplicationWillChangeStatusBarOrientationNotification]; //添加新的事件 [TLAppEventBus.shared startWithNotificationMap:defaultMap];//開啟EventBus
添加新事件需要用分類添加TLAppEventBus的屬性啦吧,添加后就可正常使用了
-(void)setDidChangeStatusBarOrientation:(NSNotification*)didChangeStatusBarOrientation {
objc_setAssociatedObject(self, (__bridge const void *)KDidChangeStatusBarOrientation , didChangeStatusBarOrientation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSNotification*)didChangeStatusBarOrientation {
returnobjc_getAssociatedObject(self, (__bridge const void *)KDidBecomeActive);
}
TLRouter
路由支持兩種注冊方式您觉,一種只寫schema,一種寫url路徑
[TLRouter registerURL:@"wx1234567://" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {
//do something
}]//注冊schema
[TLRouter registerURL:@"InnerJump://account/login" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {
//do something
}]//注冊url路徑
支持同步 & 異步獲取返回值授滓,其中異步轉(zhuǎn)同步內(nèi)部通過semaphore實現(xiàn)
+(void)openURL:(NSString*)url callback:(void(^)(BOOLresult))callback;
+(BOOL)openURL:(NSString*)url;
另外openURL除了支持url中帶參數(shù)琳水,也支持參數(shù)放在字典中
+(BOOL)openURL:(NSString*)url param:(NSDictionary *)param;
TLAppLaunchHelper
TLAppLaunchHelper有兩個函數(shù),一個用來初始化組件褒墨。該函數(shù)會讀取AutoInitialize.plist中的classes炫刷,通過runtime + 自動初始化協(xié)議完成初始化
-(void)autoInitialize;
另一個函數(shù)用來自動注冊路由,該函數(shù)會讀取AutoRegistURL.plist完成路由注冊郁妈。其中controller代表類名浑玛,params代表默認參數(shù),如果openURL傳的參數(shù)與默認參數(shù)不符合噩咪,路由會報錯
-(void)autoRegistURL;
路由注冊時顾彰,并不決定controller跳轉(zhuǎn)的方式。注冊者只是調(diào)用presentingSelf方法胃碾,跳轉(zhuǎn)方式由controller中presentingSelf方法決定涨享。
-(BOOL)presentingSelf {
UINavigationController *rootVC = (UINavigationController *) APPWINDOW.rootViewController; if(rootVC) { [rootVCpushViewController:self animated:YES]; returnYES;
}
return NO;
}
耦合檢測工具
針對既有代碼的組件化重構,我這邊開發(fā)了一個耦合檢測工具仆百,目前只支持OC厕隧。
耦合檢測工具的原理是這樣:工具認為工程中一級文件夾由組件構成,比如A工程下面有aa俄周、bb吁讨、cc三個文件夾,aa峦朗、bb建丧、cc就是三個待檢測的組件。耦合檢測分三步波势,第一步通過正則找到組件內(nèi).h文件中所有關鍵字(包括函數(shù)翎朱、宏定義和類)。第二步通過找到的組件內(nèi)關鍵字尺铣,再通過正則去其它組件的.m中找是否使用了該組件的關鍵字拴曲,如果使用了,兩個組件就有耦合關系凛忿。第三步澈灼,輸出耦合檢測報告
代碼:開源中....
總結(jié)
本文給出了組件化的定義:組件化意味著重構,目的是讓每個組件職責單一以提升集成效率侄非。包管理技術Pod是組件化常用的工具蕉汪,iOS組件依賴及組件版本號確定,都可以用pod實現(xiàn)逞怨。整個iOS工程的組件通常分為3層者疤,業(yè)務組件、模塊組件和SDK組件叠赦。在老工程重構時驹马,優(yōu)先抽離SDK組件,切記不要寫XXCommon讓它變成垃圾堆除秀。
關于解耦的技術糯累,appldegate適合用觀察者模式替換代理模式,路由只用來做controller之間的跳轉(zhuǎn)册踩,上層業(yè)務組件的解耦靠依賴注入而不是全用路由泳姐。工程的組件和路由都可通過runtime + 配置的形式自動注冊,這樣做維護和集成都會很方便暂吉。
Demo地址:https://github.com/zhudaye12138/Tourelle
作者:朱大爺12138
鏈接:http://www.reibang.com/p/d88aef8e29a4
來源:簡書
簡書著作權歸作者所有胖秒,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權并注明出處。