組件化架構漫談
前段時間公司項目打算重構, 準確來說應該是按之前的產品邏輯重寫一個項目。在重構項目之前涉及到架構選型的問題,我和組里小伙伴一起研究了一下組件化架構, 打算將項目重構為組件化架構 。當然不是直接拿來照搬,還是要根據公司具體的業務需求設計架構。
在學習組件化架構的過程中,從很多高質量的博客中學到不少東西,例如 蘑菇街李忠 、 casatwy 、 bang 的博客。在學習過程中也遇到一些問題, 在微博和QQ上和一些做 iOS 的朋友進行了交流 ,非常感謝這些朋友的幫助。
本篇文章主要針對于之前蘑菇街提出的組件化方案,以及 casatwy 提出的組件化方案進行分析,后面還會簡單提到滴滴、淘寶、微信的組件化架構,最后會簡單說一下我公司設計的組件化架構。
組件化架構的由來
隨著移動互聯網的不斷發展, 很多程序代碼量和業務越來越多 , 現有架構已經不適合公司業務的發展速度了 ,很多都面臨著重構的問題。
在公司項目開發中,如果項目比較小,普通的 單工程+MVC架構 就可以滿足大多數需求了。但是像淘寶、蘑菇街、微信這樣的大型項目,原有的 單工程架構 就不足以滿足架構需求了。
就拿淘寶來說,淘寶在 13 年開啟的 “All in 無線” 戰略中,就將阿里系大多數業務都加入到手機淘寶中,使客戶端出現了業務的爆發。在這種情況下, 單工程架構則已經遠遠不能滿足現有業務需求了 。所以在這種情況下,淘寶在 13 年開啟了 插件化架構 的重構,后來在 14 年迎來了手機淘寶有史以來最大規模的重構,將其徹底 重構為組件化架構 。
蘑菇街的組件化架構
原因
在一個項目越來越大,開發人員越來越多的情況下,項目會遇到很多問題。
-
業務模塊間劃分不清晰,模塊之間耦合度很大,非常難維護。
-
所有模塊代碼都編寫在一個項目中, 測試某個模塊或功能 , 需要編譯運行整個項目 。
耦合嚴重的工程
為了解決上面的問題,可以考慮加一個 中間層 來協調模塊間的調用, 所有的模塊間的調用都會經過中間層中轉 。 (注意看兩張圖的箭頭方向)
添加中間層
但是發現增加這個中間層后,耦合還是存在的。中間層對被調用模塊存在耦合,其他模塊也需要耦合中間層才能發起調用。 這樣還是存在之前的相互耦合的問題 ,而且本質上比之前更麻煩了。
大體結構
所以應該做的是,只讓其他模塊對中間層產生耦合關系, 中間層不對其他模塊發生耦合 。
對于這個問題, 可以采用組件化的架構 ,將每個模塊作為一個組件。并且建立一個主項目,這個主項目負責集成所有組件。這樣帶來的好處是很多的:
-
業務劃分更佳清晰,新人接手更佳容易,可以按組件分配開發任務。
-
項目可維護性更強,提高開發效率。
-
更好排查問題,某個組件出現問題,直接對組件進行處理。
-
開發測試過程中,可以只編譯自己那部分代碼,不需要編譯整個項目代碼。
組件化結構
進行組件化開發后, 可以把每個組件當做一個獨立的app , 每個組件甚至可以采取不同的架構 ,例如分別使用 MVVM 、 MVC 、 MVCS 等架構。
MGJRouter方案
蘑菇街通過 MGJRouter 實現中間層,通過 MGJRouter 進行組件間的消息轉發,從名字上來說更像是路由器。實現方式大致是, 在提供服務的組件中提前注冊 block ,然后在調用方組件中通過 URL 調用 block ,下面是調用方式。
架構設計
MGJRouter組件化架構
MGJRouter 是一個單例對象,在其內部維護著一個“URL ->block”格式的注冊表,通過這個注冊表來 保存服務方注冊的block,以及 使調用方可以通過URL映射出block ,并通過MGJRouter對服務方發起調用。
在服務方組件中都對外提供一個接口類 ,在接口類 內部實現block的注冊工作,以及block對外提供服務的代碼實現。每一個block都對應著一個URL,調用方可以通過URL對block發起調用。
在程序開始運行時,需要將所有服務方的接口類實例化,以完成這個注冊工作,使 MGJRouter 中所有服務方的 block 可以正常提供服務。在這個服務注冊完成后,就可以被調用方調起并提供服務。
蘑菇街項目使用git作為 版本控制工具,將每個組件都當做一個獨立工程 ,并建立主項目來集成所有組件。集成方式是在主項目中通過 CocoaPods 來集成,將所有組件當做二方庫 集成到項目中。詳細的集成技術點在下面“標準組件化架構設計”章節中會講到。
MGJRouter調用
代碼模擬對詳情頁的注冊、調用,在調用過程中傳遞 id 參數。下面是注冊的示例代碼:
[MGJRouter registerURLPattern:@"mgj://detail?id=id" toHandler:^(NSDictionary *routerParameters) { // 下面可以在拿到參數后,為其他組件提供對應的服務 NSString uid = routerParameters[@"id"]; }];
通過 openURL: 方法傳入的 URL 參數,對詳情頁已經注冊的 block 方法發起調用。 調用方式類似于 GET 請求 , URL 地址后面拼接參數。
[MGJRouter openURL:@"mgj://detail?id=404"];
也可以通過字典方式傳參, MGJRouter 提供了帶有字典參數的方法,這樣就 可以傳遞非字符串之外的其他類型參數 。
[MGJRouter openURL:@"mgj://detail?" withParam:@{@"id" : @"404"}];
組件間傳值
有的時候組件間調用過程中,需要服務方在完成調用后返回相應的參數。蘑菇街提供了另外的方法,專門來完成這個操作。
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){ return @42; }];
通過下面的方式發起調用,并獲取服務方返回的返回值,要做的就是傳遞正確的 URL 和參數即可。
NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];
短鏈管理
這時候會發現一個問題,在蘑菇街組件化架構中, 存在了很多硬編碼的URL和參數 。在代碼實現過程中 URL 編寫出錯會導致調用失敗,而且參數是一個字典類型,調用方不知道服務方需要哪些參數,這些都是個問題。
對于這些數據的管理,蘑菇街開發了一個 web 頁面,這個 web 頁面統一來管理所有的 URL 和參數, Android 和 iOS 都使用這一套 URL ,可以保持統一性。
基礎組件
在項目中存在很多公共部分的東西,例如封裝的網絡請求、緩存、數據處理等功能,以及項目中所用到的資源文件。
蘑菇街將這些部分也當做組件,劃分為基礎組件,位于業務組件下層。所有業務組件都使用同一個基礎組件,也可以保證公共部分的統一性。
Protocol方案
整體架構
Protocol方案的中間件
為了解決 MGJRouter 方案中 URL 硬編碼 ,以及 字典參數類型不明確 等問題,蘑菇街在原有組件化方案的基礎上推出了 Protocol 方案。 Protocol 方案由兩部分組成,進行組件間通信的 ModuleManager 類以及 MGJComponentProtocol 協議類。
通過中間件 ModuleManager 進行消息的調用轉發,在 ModuleManager 內部維護一張映射表,映射表由之前的 "URL -> block" 變成 "Protocol -> Class" 。
在中間件中創建 MGJComponentProtocol 文件,服務方組件將可以用來調用的方法都定義在 Protocol 中,將所有服務方的 Protocol 都分別定義到 MGJComponentProtocol 文件中,如果協議比較多也可以分開幾個文件定義。這樣所有調用方依然是只依賴中間件,不需要依賴除中間件之外的其他組件。
Protocol 方案中每個組件也需要一個“接口類”,此類負責實現當前組件對應的協議方法,也就是對外提供服務的實現。在 程序開始運行時將自身的Class 注冊到ModuleManager中 ,并將 Protocol 反射出字符串當做 key 。這個注冊過程和 MGJRouter 是類似的,都 需要提前注冊服務 。
示例代碼
創建 MGJUserImpl 類當做 User 模塊的服務類,并在 MGJComponentProtocol.h 中定義 MGJUserProtocol 協議,由 MGJUserImpl 類實現協議中定義的方法,完成對外提供服務的過程。下面是協議定義:
@protocol MGJUserProtocol <NSObject> - (NSString *)getUserName; @end
Class 遵守協議并實現定義的方法,外界通過 Protocol 獲取的 Class 實例化為對象,調用服務方實現的協議方法。
ModuleManager 的協議注冊方法,注冊時將 Protocol 反射為字符串當做存儲的 key ,將實現協議的 Class 當做值存儲。通過 Protocol 取 Class 的時候,就是通過 Protocol 從 ModuleManager 中將 Class 映射出來。
[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];
調用時通過 Protocol 從 ModuleManager 中映射出注冊的 Class ,將獲取到的 Class 實例化,并調用 Class 實現的協議方法完成服務調用。
Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)]; id userComponent = [[cls alloc] init]; NSString *userName = [userComponent getUserName];
整體調用流程
蘑菇街是 OpenURL 和 Protocol 混用的方式,兩種實現的調用方式不同,但大體調用邏輯和實現思路類似,所以下面的 調用流程二者差不多 。在 OpenURL 不能滿足需求或調用不方便時,就可以通過 Protocol 的方式調用。
-
在進入程序后,先使用 MGJRouter 對服務方組件進行注冊。每個 URL 對應一個 block 的實現, block 中的代碼就是服務方對外提供的服務 ,調用方可以通過 URL 調用這個服務。
-
調用方通過 MGJRouter 調用 openURL: 方法,并將被調用代碼對應的 URL 傳入, MGJRouter 會根據 URL 查找對應的 block 實現,從而調用服務方組件的代碼進行通信。
-
調用和注冊 block 時, block 有一個字典用來傳遞參數。這樣的優勢就是參數類型和數量理論上是不受限制的,但是需要很多硬編碼的 key 名在項目中。
內存管理
蘑菇街組件化方案有兩種, Protocol 和 MGJRouter 的方式,但都需要進行 register 操作。 Protocol 注冊的是 Class , MGJRouter 注冊的是 Block ,注冊表是一個 NSMutableDictionary 類型的字典,而字典的擁有者又是一個 單例對象 ,這樣會造成 內存的常駐 。
下面是對兩種實現方式內存消耗的分析:
-
首先說一下 block 實現方式可能導致的內存問題, block 如果使用不當,很容易造成循環引用的問題。
經過暴力測試,證明并不會導致內存問題。被保存在字典中是一個 block 對象,而 block 對象本身并不會占用多少內存。在調用 block 后會對 block 體中的方法進行執行,執行完成后 block 體中的對象釋放。
而 block 自身的實現只是一個結構體,也就相當于字典中存放的是很多結構體,所以內存的占用并不是很大。
-
對于協議這種實現方式,和 block 內存常駐方式差不多。只是將存儲的 block 對象換成 Class 對象,如果不是已經實例化的對象,內存占用還是比較小的。
casatwy組件化方案
整體架構
casatwy組件化方案分為兩種調用方式, 遠程調用和本地調用 ,對于兩個不同的調用方式分別對應兩個接口。
-
遠程調用通過 AppDelegate 代理方法傳遞到當前應用后,調用遠程接口并在內部做一些處理,處理完成后會在遠程接口內部調用本地接口, 以實現本地調用為遠程調用服務 。
-
本地調用由 performTarget:action:params: 方法負責,但調用方一般 不直接調用 performTarget: 方法 。 CTMediator 會對外提供明確參數和方法名的方法,在方法內部調用 performTarget: 方法和參數的轉換。
casatwy提出的組件化架構
架構設計思路
casatwy是通過 CTMediator 類實現組件化的,在此類中對外提供明確參數類型的接口,接口內部通過 performTarget 方法調用服務方組件的 Target 、 Action 。由于 CTMediator 類的調用是 通過 runtime 主動發現服務 的,所以服務方對此類是完全解耦的。
但如果 CTMediator 類對外提供的方法都放在此類中,將會對 CTMediator 造成極大的負擔和代碼量。解決方法就是對每個服務方組件創建一個 CTMediator 的 Category ,并將對服務方的 performTarget 調用放在對應的 Category 中,這些 Category 都屬于 CTMediator 中間件,從而實現了感官上的接口分離。
casatwy組件化實現細節
對于服務方的組件來說,每個組件都提供一個或多個 Target 類,在 Target 類中聲明 Action 方法。 Target 類是當前組件對外提供的一個 “服務類” , Target 將當前組件中所有的服務都定義在里面, CTMediator 通過 runtime 主動發現服務 。
在 Target 中的所有 Action 方法,都只有一個字典參數,所以可以傳遞的參數很靈活,這也是 casatwy 提出的 去 Model 化的概念 。在 Action 的方法實現中,對傳進來的字典參數進行解析,再調用組件內部的類和方法。
架構分析
casatwy為我們提供了一個 Demo ,通過這個 Demo 可以很好的理解 casatwy 的設計思路,下面按照我的理解講解一下這個 Demo 。
文件目錄
打開 Demo 后可以看到文件目錄非常清楚,在上圖中用藍框框出來的就是中間件部分,紅框框出來的就是業務組件部分。我對每個文件夾做了一個簡單的注釋,包含了其在架構中的職責。
在 CTMediator 中定義遠程調用和本地調用的兩個方法 ,其他業務相關的調用由 Category 完成。
// 遠程App調用入口 - (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion; // 本地組件調用入口 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
在 CTMediator 中定義的 ModuleA 的 Category ,對外提供了一個獲取控制器并跳轉的功能,下面是代碼實現。由于 casatwy 的方案中使用 performTarget 的方式進行調用,所以 涉及到很多硬編碼字符串的問題 , casatwy 采取定義常量字符串來解決這個問題,這樣管理也更方便。
#import "CTMediator+CTMediatorModuleAActions.h" NSString * const kCTMediatorTargetA = @"A"; NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController"; @implementation CTMediator (CTMediatorModuleAActions) - (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 { // 這里處理異常場景,具體如何處理取決于產品 return [[UIViewController alloc] init]; } }
下面是 ModuleA 組件中提供的服務,被定義在 Target_A 類中,這些服務可以被 CTMediator 通過 runtime 的方式調用, 這個過程就叫做發現服務 。
我們發現,在這個方法中其實做了參數處理和內部調用的功能,這樣就可以保證組件內部的業務不受外部影響, 對內部業務沒有侵入性 。
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params { // 對傳過來的字典參數進行解析,并調用ModuleA內部的代碼 DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init]; viewController.valueLabel.text = params[@"key"]; return viewController; }
命名規范
在大型項目中代碼量比較大,需要避免命名沖突的問題。對于這個問題 casatwy 采取的是加前綴的方式,從 casatwy 的 Demo 中也可以看出,其組件 ModuleA 的 Target 命名為 Target_A ,被調用的 Action 命名為 Action_nativeFetchDetailViewController: 。
casatwy將類和方法的命名, 都統一按照其功能做區分當做前綴 ,這樣很好的將組件相關和組件內部代碼進行了劃分。
標準組件化架構設計
這個章節叫做 “標準組件化架構設計” ,對于項目架構來說 并沒有絕對意義的標準之說 。這里說到的 “標準組件化架構設計” 只是因為采取這樣的方式的人比較多,且這種方式相比而言較合理。
在上面文章中提到了 casatwy 方案的 CTMediator ,蘑菇街方案的 MGJRouter 和 ModuleManager ,下面統稱為中間件。
整體架構
組件化架構中,首先有一個主工程,主工程負責集成所有組件。 每個組件都是一個單獨的工程 ,創建不同的 git 私有倉庫來管理,每個組件都有對應的開發人員負責開發。開發人員只需要關注與其相關組件的代碼,其他業務代碼和其無關,來新人也好上手。
組件的劃分需要注意組件粒度,粒度根據業務可大可小。組件劃分后屬于業務組件,對于一些多個組件共同的東西,例如網絡、數據庫之類的,應該劃分到單獨的組件或基礎組件中。對于圖片或配置表這樣的資源文件,應該再單獨劃分一個資源組件,這樣避免資源的重復性。
服務方組件對外提供服務, 由中間件調用或發現服務 , 服務對當前組件無侵入性 ,只負責對傳遞過來的數據進行解析和組件內調用的功能。需要被其他組件調用的組件都是服務方,服務方也可以調用其他組件的服務。
通過這樣的組件劃分,組件的開發進度不會受其他業務的影響, 可以多個組件單獨的并行開發 。組件間的通信都交給中間件來進行,需要通信的類只需要接觸中間件,而中間件不需要耦合其他組件,這就實現了組件間的解耦。 中間件負責處理所有組件之間的調度 ,在所有組件之間起到控制核心的作用。
這套框架清晰的劃分了不同組件,從整體架構上來約束開發人員進行組件化開發,避免某個開發人員偷懶直接引用頭文件,產生組件間的耦合,破壞整體架構。假設以后某個業務發生大的改變,需要對相關代碼進行重構,可以在單個組件進行重構。組件化架構降低了重構的風險,保證了代碼的健壯性。
組件集成
組件化架構圖
每個組件都是一個單獨的工程,在組件開發完成后上傳到 git 倉庫。主工程通過 Cocoapods 集成各個組件,集成和更新組件時只需要 pod update 即可。這樣就是把每個組件當做第三方來管理,管理起來非常方便。
Cocoapods 可以控制每個組件的版本,例如 在主項目中回滾某個組件到特定版本 ,就可以通過修改 podfile 文件實現。選擇 Cocoapods 主要因為其本身功能很強大,可以很方便的集成整個項目, 也有利于代碼的復用 。通過這種集成方式,可以很好的避免在傳統項目中代碼沖突的問題。
集成方式
對于組件化架構的集成方式,我在看完 bang 的博客后專門請教了一下 bang 。根據在微博上和 bang 的聊天以及其他博客中的學習,在主項目中集成組件主要分為兩種方式—— 源碼和 framework ,但都是通過 CocoaPods 來集成。
無論是用 CocoaPods 管理源碼,還是直接管理 framework ,效果都是一樣的,都是可以直接進行 pod update 之類的操作的。
這兩種組件集成方案,實踐中也是各有利弊。直接在主工程中集成代碼文件, 可以在主工程中進行調試 。集成 framework 的方式, 可以加快編譯速度 ,而且 對每個組件的代碼有很好的保密性 。如果公司對代碼安全比較看重,可以考慮 framework 的形式,但 framework 不利于主工程中的調試。
例如 手機QQ 或者 支付寶 這樣的大型程序,一般都會采取 framework 的形式。而且一般這樣的大公司, 都會有自己的組件庫 ,這個組件庫往往可以代表一個大的功能或業務組件,直接添加項目中就可以使用。關于組件化庫在后面講淘寶組件化架構的時候會提到。
不推薦的集成方式
之前有些項目是直接用 workspace 的方式集成的,或者直接在原有項目中建立子項目,直接做文件引用。但這兩點都是不建議做的,因為 沒有真正意義上實現業務組件的剝離 ,只是像之前的項目一樣從文件目錄結構上進行了劃分。
組件化開發總結
對于項目架構來說, 一定要建立于業務之上來設計架構 。不同的項目業務不同,組件化方案的設計也會不同,應該設計最適合公司業務的架構。
架構對比
在除蘑菇街 Protocol 方案外,其他兩種方案都或多或少的 存在硬編碼問題 ,硬編碼如果量比較大的話挺麻煩的。
在 casatwy 的 CTMediator 方案中需要硬編碼 Target 、 Action 字符串,只不過 這個缺陷被封閉在中間件里面了 ,將這些字符串都統一定義為常量,外界使用不需要接觸到硬編碼。蘑菇街的 MGJRouter 的方案也是一樣的,也有硬編碼 URL 的問題,蘑菇街可能也做了類似的處理。
casatwy和蘑菇街提出的兩套組件化方案,大體結構是類似的,三套方案都分為 調用方 、 中間件 、 服務方 ,只是在具體實現過程中有些不同。例如 Protocol 方案在中間件中加入了 Protocol 文件, casatwy 的方案在中間件中加入了 Category 。
三種方案內部都有容錯處理,所以三種方案的穩定性都是比較好的,而且都可以拿出來單獨運行,在服務方不存在的情況下也不會有問題。
在三套方案中,服務方都對外提供一個供外界調用的接口類,這個類中實現組件對外提供的服務,中間件通過接口類來實現組件間的通信。 在此類中統一定義對外提供的服務 ,外界調用時就知道服務方可以做什么。
調用流程也不大一樣, 蘑菇街的兩套方案都需要注冊操作 ,無論是 Block 還是 Protocol 都需要注冊后才可以提供服務。而 casatwy 的方案則不需要,直接通過 runtime 調用。 casatwy 的方案實現了 真正的對服務方解耦 ,而蘑菇街的兩套方案則沒有,對服務方和調用方都造成了耦合。
我認為三套方案中, Protocol 方案是調用和維護最麻煩的一套方案。維護時需要同時維護 Protocol 、接口類兩部分。而且調用時需要將服務方的接口類返回給調用方,并由調用方執行一系列調用邏輯,調用一個服務的邏輯非常復雜,這在開發中是非常影響開發效率的。
總結
下面是組件化開發中的一個小總結,也是開發過程中的一些注意點。
-
在 MGJRouter 方案中,是通過調用 OpenURL: 方法并傳入 URL 來發起調用。鑒于 URL 協議名等固定格式,可以通過判斷協議名的方式, 使用配置表控制 H5 和 native 的切換 , 配置表可以從后臺更新 ,只需要將協議名更改一下即可。
mgj://detail?id=123456
http://www.mogujie.com/detail?id=123456
假設現在線上的 native 組件出現嚴重 bug , 在后臺將配置文件中原有的本地 URL 換成 H5 的 URL , 并更新客戶端配置文件 。在調用 MGJRouter 時傳入這個 H5 的 URL 即可完成切換, MGJRouter 判斷如果傳進來的是一個 H5 的 URL 就直接跳轉 webView 。而且 URL 可以傳遞參數給 MGJRouter ,只需要 MGJRouter 內部做參數截取即可。
-
casatwy方案和蘑菇街 Protocol 方案,都提供了傳遞明確類型參數的方法。在 MGJRouter 方案中,傳遞參數主要是通過類似 GET 請求一樣在 URL 后面拼接參數,和在字典中傳遞參數兩種方式組成。這兩種方式 會造成傳遞參數類型不明確 ,傳遞參數類型受限( GET 請求不能傳遞對象)等問題,后來使用 Protocol 方案彌補這個問題。
-
組件化開發可以很好的提升代碼復用性,組件可以直接拿到其他項目中使用,這個優點在下面淘寶架構中會著重講一下。
-
對于調試工作,應該放在每個組件中完成。 單獨的業務組件可以直接提交給測試提測 ,這樣測試起來也比較方便。最后組件開發完成并測試通過后,再將所有組件更新到主項目,提交給測試進行集成測試即可。
-
使用組件化架構開發,組件間的通信都是有成本的。所以盡量將業務封裝在組件內部,對外只提供簡單的接口。 即“高內聚、低耦合”原則 。
-
把握好劃分粒度的細化程度,太細則項目過于分散,太大則項目組件臃腫。但是項目都是從小到大的一個發展過程,所以不斷進行重構是掌握這個組件的細化程度最好的方式。
我公司架構
下面就簡單說說我公司項目架構,公司項目是一個地圖導航應用,業務層之下的基礎組件占比較大。且基礎組件相對比較獨立,對外提供了很多調用接口。剛開始想的是采用 MGJRouter 的方案,但如果這些調用都通過 Router 進行,開發起來比較復雜,反而會適得其反。最主要我們項目也并不是非常大,沒必要都用 Router 轉發。
對于這個問題,公司項目的架構設計是: 層級架構+組件化架構 ,組件化架構處于層級架構的最上層,也就是業務層。采取這種結構混合的方式進行整體架構,這個對于公共組件的管理和層級劃分比較有利,符合公司業務需求。
公司組件化架構
對于業務層級依然采用組件化架構的設計,這樣可以充分利用組件化架構的優勢,對項目組件間進行解耦。在上層和下層的調用中,下層的功能組件應該對外開放一個接口類,在接口類中聲明所有的服務,實現上層調用當前組件的一個中轉,上層直接調用接口類。這樣做的好處在于,如果 下層發生改變不會對上層造成影響 ,而且也省去了部分 Router 轉發的工作。
在設計層級架構時, 需要注意只能上層對下層依賴 , 下層對上層不能有依賴 , 下層中不要包含上層業務邏輯 。對于項目中存在的公共資源和代碼,應該將其下沉到下層中。
為什么這么做?
首先就像我剛才說的,我公司項目并不是很大,根本沒必要拆分的那么徹底。
因為組件化開發有一個很重要的原因就是解耦合,如果我做到了底層不對上層依賴, 這樣就已經解除了上下層的相互耦合 。而且上層對下層進行調用的時候,也不是直接調用下層,通過一個接口類進行中轉,實現了 下層的改變對上層無影響 ,這也是上層對下層解耦的表現。
所以對于第三方就不用說了,上層直接調用下層的第三方也是沒問題的,這都是解耦的。
模型類怎么辦,放在哪合適?
casatwy對模型類的觀點是 去Model化 ,簡單來說就是用字典代替 Model 存儲數據。這對于組件化架構來說,是解決組件之間數據傳遞的一個很好的方法。
因為模型類是關乎業務的,理論上必須放在業務層也就是業務組件這一層。但是要把模型對象從一個組件中當做參數傳遞到另一個組件中, 模型類放在調用方和服務方的哪個組件都不太合適 ,而且有可能不只兩個組件使用到這個模型對象。這樣的話在其他組件使用模型對象, 必然會造成引用和耦合 。
那么如果把模型類放在 Router 中,這樣會造成 Router 耦合了業務, 造成業務的侵入性 。如果在用到這個模型對象的所有組件中,都分別維護一份相同的模型類,這樣之后業務發生改變模型類就會很麻煩。
那應該怎么辦呢?
如果將模型類單獨拉出來,定義一個模型組件呢?這個看起來比較可行,將這個定義模型的組件下沉到下層,模型組件不包含業務,只聲明模型對象的類。但是一般組件的模型對象都是當前組件內使用的,將模型對象傳遞給其他組件的需求非常少, 那所有的模型類都定義到模型組件嗎 ?
對于這個問題,我建議在項目開發中將模型類還定義在當前業務組件中, 在組件間傳遞模型對象時進行去Model化 ,傳遞字典類型的參數。
上面只是思考,恰巧我公司持久化方案用的是 CoreData ,所有模型的定義都在 CoreData 組件中,這樣就避免了業務層組件之間因為模型類的耦合。
滴滴組件化架構
之前看過滴滴 iOS 負責人李賢輝的 技術分享 ,分享的是滴滴 iOS 客戶端的架構發展歷程,下面簡單總結一下。
發展歷程
滴滴在最開始的時候架構較混亂。然后在 2.0 時期重構為 MVC 架構,使項目劃分更加清晰。在 3.0 時期上線了新的業務線, 這時采用的游戲開發中的狀態機機制 ,暫時可以滿足現有業務。
然而在后期不斷上線順風車、代駕、巴士等多條業務線的情況下, 現有架構變得非常臃腫 , 代碼耦合嚴重 。從而在2015年開始了代號為 “The One” 的方案,這套方案就是滴滴的組件化方案。
架構設計
滴滴的組件化方案,和蘑菇街方案類似,也是通過私有 CocoaPods 來管理各個組件。 將整個項目拆分為業務部分和技術部分 ,業務部分包括專車、拼車、巴士等業務模塊,每個業務模塊就是一個單獨的組件,使用一個 pods 管理。技術部分則分為登錄分享、網絡、緩存這樣的一些基礎組件,分別使用不同的 pods 管理。
組件間通信通過 ONERouter 中間件進行通信, ONERouter 類似于 MGJRouter , 擔負起協調和調用各個組件的作用 。組件間通信通過 OpenURL 方法,來進行對應的調用。 ONERouter 內部保存一份 Class-URL 的映射表,通過 URL 找到 Class 并發起調用, Class 的注冊放在 +load 方法中進行。
滴滴在組件內部的業務模塊中, 模塊內部使用 MVVM+MVCS 混合架構 , 兩種架構都是 MVC 的衍生版本 。其中 MVCS 中的 Store 負責數據相關邏輯,例如訂單狀態、地址管理等數據處理。通過 MVVM 中的 VM 給控制器瘦身,最后 Controller 的代碼量就很少了。
滴滴首頁分析
滴滴文章中說道 首頁只能有一個地圖實例 ,這在很多地圖導航相關應用中都是這樣做的。滴滴首頁主控制器持有導航欄和地圖,每個業務線首頁控制器都添加在主控制器上,并且業務線控制器背景都設置為透明, 將透明部分響應事件傳遞到下面的地圖中 ,只響應屬于自己的響應事件。
由主控制器來切換各個業務線首頁, 切換頁面后根據不同的業務線來更新地圖數據 。
淘寶組件化架構
架構發展
淘寶 iOS 客戶端初期是單工程的普通項目,但隨著業務的飛速發展,現有架構并不能承載越來越多的業務需求,導致代碼間耦合很嚴重。后期開發團隊對其不斷進行重構,淘寶 iOS 和 Android 兩個平臺,除了某個平臺特有的一些特性或某些方案不便實施之外,大體架構都是差不多的。
發展歷程:
-
剛開始是普通的單工程項目,以傳統的 MVC 架構進行開發。隨著業務不斷的增加,導致項目非常臃腫、耦合嚴重。
-
2013年淘寶開啟 "all in 無線"計劃 ,計劃將淘寶變為一個大的平臺,將阿里系大多數業務都集成到這個平臺上, 造成了業務的大爆發 。
淘寶開始實行插件化架構,將每個業務模塊劃分為一個組件, 將組件以 framework 二方庫的形式集成到主工程 。但這種方式并沒有做到真正的拆分,還是在一個工程中使用 git 進行 merge ,這樣還會造成合并沖突、不好回退等問題。
-
迎來淘寶移動端有史以來最大的重構,將其重構為組件化架構。將每個模塊當做一個組件,每個組件都是一個單獨的項目,并且將組件打包成 framework 。主工程通過 podfile 集成所有組件 framework ,實現業務之間真正的隔離,通過 CocoaPods 實現組件化架構。
架構優勢
淘寶是使用 git 來做源碼管理的, 在插件化架構時需要盡可能避免 merge 操作 ,否則在大團隊中協作成本是很大的。而使用 CocoaPods 進行組件化開發,則避免了這個問題。
在 CocoaPods 中可以通過 podfile 很好的配置各個組件,包括組件的增加和刪除, 以及控制某個組件的版本 。使用 CocoaPods 的原因,很大程度是為了解決大型項目中,代碼管理工具 merge 代碼導致的沖突。并且可以通過配置 podfile 文件,輕松配置項目。
每個組件工程有兩個 target , 一個負責編譯當前組件和運行調試 , 另一個負責打包 framework 。先在組件工程做測試,測試完成后再集成到主工程中集成測試。
每個組件都是一個獨立 app ,可以獨立開發、測試,使得業務組件更加獨立, 所有組件可以并行開發 。下層為上層提供能滿足需求的底層庫,保證上層業務層可以正常開發,并將底層庫封裝成 framework 集成到項目中。
使用 CocoaPods 進行組件集成的好處在于,在集成測試自己組件時, 可以直接將本地主工程 podfile 文件中的當前組件指向本地 ,就可以直接進行集成測試,不需要提交到服務器倉庫。
淘寶四層架構
淘寶四層架構(圖片來自淘寶技術分享)
淘寶架構的核心思想是一切皆組件,將工程中所有代碼都抽象為組件。
淘寶架構主要分為四層,最上層是 組件 Bundle (業務組件),依次往下是 容器 (核心層), 中間件 Bundle (功能封裝), 基礎庫 Bundle (底層庫)。容器層為整個架構的核心,負責組件間的調度和消息派發。
總線設計
總線設計: URL 路由+服務+消息 。統一所有組件的通信標準,各個業務間通過總線進行通信。
總線設計(圖片來自淘寶技術分享)
URL 可以請求也可以接受返回值,和 MGJRouter 差不多。 URL 路由請求可以被解析就直接拿來使用, 如果不能被解析就跳轉 H5 頁面 。這樣就完成了一個 對不存在組件調用的兼容 ,使用戶手中比較老的版本依然可以顯示新的組件。
服務提供一些公共服務,由服務方組件負責實現,通過 Protocol 實現。消息負責統一發送消息,類似于通知也需要注冊。
Bundle App
Bundle App(圖片來自淘寶技術分享)
淘寶提出 Bundle App 的概念,可以通過已有組件, 進行簡單配置后就可以組成一個新的 app 出來 。解決了多個應用業務復用的問題,防止重復開發同一業務或功能。
Bundle 即 App , 容器即 OS ,所有 Bundle App 被集成到 OS 上,使每個組件的開發就像 app 開發一樣簡單。這樣就做到了從巨型 app 回歸普通 app 的輕盈,使大型項目的開發問題徹底得到了解決。
來自:http://www.jianshu.com/p/67a6004f6930