淺談 iOS 組件化開發

背景

組件化做爲目前移動應用架構的主流方式之一,近年來一直是業界積極探索和實踐的方向。html

起初的這個項目,App只有一條產品線,代碼邏輯相對比較清晰,後期隨着公司業務的迅速發展,如今App裏面承載了大概五六條產品線,每一個產品線的流程有部分是同樣的,也有部分是不同的,這就須要作各類各樣的判斷及定製化需求。大概作了一年多後,出現了不一樣產品線提過來的需求,開發人員都須要在主工程中開發,可是開發人員開發的是不一樣的產品線,也得將整個工程跑起來,代碼管理、並行開發效率、分支管理、上線時間明顯有所限制。大概就在去年末,咱們的領導提出了這個問題,但願做成組件化,將代碼重構拆分紅模塊,在主工程中組裝拆分的模塊,造成一個完整的App。 前端

注:

  1. 區別於功能模塊/組件(好比圖片庫,網絡庫),本文討論的是業務模塊/組件(好比訂單模塊,商品模塊)相關的架構設計。
  2. 相比組件(Component),我的感受稱之爲模塊(Module)更爲合適。組件強調物理拆分,以便複用;模塊強調邏輯拆分,以便解耦。並且若是用過 Android Studio, 會發現它建立的子系統都叫 Module. 但介於業界習慣稱之爲組件化,因此咱們繼續使用這個術語。本文下面所用名詞,「模塊」等同於「組件」。

正文

1、組件化初識

傳統的 App 架構設計更多強調的是分層,基於設計模式六大原則之一的單一職責原則,將系統劃分爲基礎層,網絡層,UI層等等,以便於維護和擴展。但隨着業務的發展,系統變得愈來愈複雜,只作分層就不夠了。App 內各子系統之間耦合嚴重, 邊界愈來愈模糊,常常發生你中有我我中有你的狀況(如圖一)。ios

圖一
這對代碼質量,功能擴展,以及開發效率都會形成很大的影響。此時,通常會將各個子系統劃分爲相對獨立的模塊,經過中介者模式收斂交互代碼,把模塊間交互部分進行集中封裝, 全部模塊間調用均經過中介者來作(如圖二)。

圖二

這時架構邏輯會清晰不少,但由於中介者仍然須要反向依賴業務模塊,這並無從根本上解除循壞依賴等問題。時不時發生一個模塊進行改動,多個模塊受影響編譯不過的狀況。進一步的,經過技術手段,消除中介者對業務模塊依賴,即造成了業務模塊化架構設計(圖三)。git

圖三

經過業務模塊化架構,通常能夠達到明確模塊職責及邊界,提高代碼質量,減小複雜依賴,優化編譯速度,提高開發效率等效果。不少文章都有相關分析,在此再也不累述。github

組件化開發的缺點:編程

  • 代碼耦合嚴重
  • 依賴嚴重
  • 其它app接入某條產品線難以集成
  • 項目複雜、臃腫、龐大,編譯時間過長
  • 難以作集成測試
  • 對開發人員,只能使用相同的開發模式

組件化開發的優勢:swift

  • 項目結構清晰
  • 代碼邏輯清晰
  • 拆分粒度小
  • 快速集成
  • 能作單元測試
  • 代碼利用率高
  • 迭代效率高

2、常見組件化方案

業務模塊化設計經過對各業務模塊的解耦改造,避免循環雙向依賴,達到提高開發效率和質量的目的。但業務需求的依賴是沒法消除的,因此模塊化方案首先要解決的是如何在無代碼依賴的狀況下實現跨模塊通訊的問題。iOS 由於其強大的運行時特性,不管是基於 NSInvocation 仍是基於 peformSelector 方法, 均可以很很容易作到這一點。但不能爲了解耦而解耦,提高質量與效率纔是咱們的目的。直接基於 hardcode 字符串 + 反射的代碼明顯會極大損害開發質量與效率,與目標背道而馳。因此,模塊化解耦需求的更準確的描述應該是「如何在保證開發質量和效率的前提下作到無代碼依賴的跨模塊通訊」。 目前業界常見的模塊間通信方案大體以下幾種:vim

基於路由 URL 的 UI 頁面統跳管理。 基於反射的遠程接口調用封裝。 基於面向協議思想的服務註冊方案。 基於通知的廣播方案。 根據具體業務和需求的不一樣,大部分公司會採用以上一種或者某幾種的組合。後端

2.1 URL 跳轉方案

統跳路由是頁面解耦的最多見方式,大量應用於前端頁面。經過把一個 URL 與一個頁面綁定,須要時經過 URL 能夠方便的打開相應頁面。設計模式

//經過路由URL跳轉到商品列表頁面
//kRouteGoodsList = @"//goods/goods_list"
UIViewController *vc = [Router handleURL:kRouteGoodsList];  
if(vc) {  
    [self.navigationController pushViewController:vc animated:YES];
}
複製代碼

固然有些場景會比這個複雜,好比有些頁面須要更多參數。 基本類型的參數,URL 協議自然支持:

//kRouteGoodsDetails = @「//goods/goods_detail?goods_id=%d」
NSString *urlStr = [NSString stringWithFormat:@"kRouteGoodsDetails", 123];  
UIViewController *vc = [Router handleURL:urlStr];  
if(vc) {  
   [self.navigationController pushViewController:vc animated:YES];
}

複製代碼

複雜類型的參數,能夠提供一個額外的字典參數 complexParams, 將複雜參數放到字典中便可:

+ (nullable id)handleURL:(nonnull NSString *)urlStr
           complexParams:(nullable NSDictionary*)complexParams
              completion:(nullable RouteCompletion)completion;

複製代碼

上面方法裏的 completion 參數,是一個回調 block, 處理打開某個頁面須要有回調功能的場景。好比打開會員選擇頁面,搜索會員,搜到以後點擊肯定,回傳會員數據:

//kRouteMemberSearch = @「//member/member_search」
UIViewController *vc = [Router handleURL:urlStr complexParams:nil completion:^(id  _Nullable result) {  
    //code to handle the result
    ...
}];
if(vc) {  
    [self.navigationController pushViewController:vc animated:YES];
}

複製代碼

考慮到實現的靈活性,提供路由服務的頁面,會將 URL 與一個 block 相綁定。block 中放入所需的初始化代碼。能夠在合適的地方將初始化 block 與路由 URL 綁定,好比在 +load 方法裏:

+ (void)load {
    [Router bindURL:kRouteGoodsList
           toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
        return [[GoodsListViewController alloc] init];
    }];

複製代碼

更多路由 URL 相關例子,能夠參考 Bifrost 項目中的 Demo.

URL 自己是一種跨多端的通用協議。使用路由URL統跳方案的優點是動態性及多端統一 (H5, iOS,Android,Weex/RN); 缺點是能處理的交互場景偏簡單。因此通常更適用於簡單 UI 頁面跳轉。一些複雜操做和數據傳輸,雖然也能夠經過此方式實現,但都不是很效率。

目前天貓和蘑菇街都有使用路由 URL 做爲本身的頁面統跳方案,達到解耦的目的。

2.2 Target-Action 方案

當沒法 import 某個類的頭文件但仍需調用其方法時,最常想到的就是基於反射來實現了。例:

Class manager = NSClassFromString(@"YZGoodsManager");  
NSArray *list = [manager performSelector:@selector(getGoodsList)];  
//code to handle the list

複製代碼

但這種方式存在大量的 hardcode 字符串。沒法觸發代碼自動補全,容易出現拼寫錯誤,並且這類錯誤只能在運行時觸發相關方法後才能發現。不管是開發效率仍是開發質量都有較大的影響。

如何進行優化呢?這實際上是各端遠程調用都須要解決的問題。移動端最多見的遠程調用就是向後端接口髮網絡請求。針對這類問題,咱們很容易想到建立一個網絡層,將這類「危險代碼」封裝到裏面。上層業務調用時網絡層接口時,不須要 hardcode 字符串,也不須要理解內部麻煩的邏輯。

相似的,我能夠將模塊間通信也封裝到一個「網絡層」中(或者叫消息轉發層)。這樣危險代碼只存在某幾個文件裏,能夠特別地進行 code review 和聯調測試。後期還能夠經過單元測試來保障質量。模塊化方案中,咱們能夠稱這類「轉發層」爲 Mediator (固然你也能夠起個別的名字)。同時由於 performSelector 方法附帶參數數量有限,也沒有返回值,因此更適合使用 NSInvocation 來實現。

//Mediator提供基於NSInvocation的遠程接口調用方法的統一封裝
- (id)performTarget:(NSString *)targetName
             action:(NSString *)actionName
             params:(NSDictionary *)params;

//Goods模塊全部對外提供的方法封裝在一個Category中
@interface Mediator(Goods)
- (NSArray*)goods_getGoodsList;
- (NSInteger)goods_getGoodsCount;
...
@end
@impletation Mediator(Goods)
- (NSArray*)goods_getGoodsList {
    return [self performTarget:@「GoodsModule」 action:@"getGoodsList" params:nil];
}
- (NSInteger)goods_getGoodsCount {
    return [self performTarget:@「GoodsModule」 action:@"getGoodsCount" params:nil];
}
...
@end
複製代碼

而後各個業務模塊依賴Mediator, 就能夠直接調用這些方法了。

//業務方依賴Mediator模塊,能夠直接調用相關方法
NSArray *list = [[Mediator sharedInstance] goods_getGoodsList];  

複製代碼

這種方案的優點是調用簡單方便,代碼自動補全和編譯時檢查都仍然有效。 劣勢是 category 存在重名覆蓋的風險,須要經過開發規範以及一些檢查機制來規避。同時 Mediator 只是收斂了 hardcode, 並未消除 hardcode, 仍然對開發效率有必定影響。

業界的 CTMediator 開源庫,以及美團都是採用相似方案。

2.3 服務註冊方案

有沒有辦法絕對的避免 hardcode 呢?若是接觸事後端的服務化改造,會發現和移動端的業務模塊化很類似。Dubbo 就是服務化的經典框架之一。它是經過服務註冊的方式來實現遠程接口調用的。即每一個模塊提供本身對外服務的協議聲明,而後將此聲明註冊到中間層。調用方能從中間層看到存在哪些服務接口,而後直接調用便可。例:

//Goods模塊提供的全部對外服務都放在GoodsModuleService中
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
//Goods模塊提供實現GoodsModuleService的對象, 
//並在+load方法中註冊
@interface GoodsModule : NSObject<GoodsModuleService>
@end
@implementation GoodsModule
+ (void)load {
    //註冊服務
    [ServiceManager registerService:@protocol(service_protocol) 
                  withModule:self.class]
}
//提供具體實現
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
@end

//將GoodsModuleService放在某個公共模塊中,對全部業務模塊可見
//業務模塊能夠直接調用相關接口
...
id<GoodsModuleService> module = [ServiceManager objByService:@protocol(GoodsModuleService)];  
NSArray *list = [module getGoodsList];  
...
複製代碼

這種方式的優點也包括調用簡單方便。代碼自動補全和編譯時檢查都有效。實現起來也簡單,協議的全部實現仍然在模塊內部,因此不須要寫反射代碼了。同時對外暴露的只有協議,符合團隊協做的「面向協議編程」的思想。劣勢是若是服務提供方和使用方依賴的是公共模塊中的同一份協議(protocol), 當協議內容改變時,會存在全部服務依賴模塊編譯失敗的風險。同時須要一個註冊過程,將 Protocol 協議與具體實現綁定起來。

業界裏,蘑菇街的 ServiceManager 和阿里的 BeeHive 都是採用的這個方案。

2.4 通知廣播方案

基於通知的模塊間通信方案,實現思路很是簡單, 直接基於系統的 NSNotificationCenter 便可。 優點是實現簡單,很是適合處理一對多的通信場景。 劣勢是僅適用於簡單通信場景。複雜數據傳輸,同步調用等方式都不太方便。 模塊化通信方案中,更多的是把通知方案做爲以上幾種方案的補充。

3、組件化開發必備工具

組件的存在方式是以每一個pod庫的形式存在的。那麼咱們組合組件的方法就是經過利用CocoaPods的方式添加安裝各個組件,咱們就須要製做CocoaPods遠程私有庫,將其發不到公司的gitlab或GitHub,使工程可以Pod下載下來。

3.1 Git的基礎命令:

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master
複製代碼

3.2 CocoaPods遠程私有庫製做:

  • 一、Create Component Project
pod lib create ProjectName
複製代碼
  • 二、Use Git
echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master
複製代碼

三、Edit podspec file

vim CoreLib.podspec
Pod::Spec.new do |s|
  s.name             = '組件工程名'
  s.version          = '0.0.1'
  s.summary          = 'summary'

  s.description      = <<-DESC
  description
                       DESC

  s.homepage         = '遠程倉庫地址'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { '做者' => '做者' }
  s.source           = { :git => '遠程倉庫地址', :tag => s.version.to_s }

  s.ios.deployment_target = '8.0'

  s.source_files = 'Classes/**/*.{swift,h,m,c}'
  s.resources = 'Assets/*'
  
  s.dependency 'AFNetworking', '~> 2.3'
end
複製代碼
  • 四、Create tag
//create local tag
git tag '0.0.1'
或
git tag 0.0.1

//local tag push to remote
git push --tags
或
git push origin 0.0.1

//delete local tag
git tag -d 0.0.1

//delete remote tag
git tag origin :0.0.1
複製代碼
  • 五、Verify Component Project
pod lib lint --allow-warnings --no-clean
複製代碼
  • 六、Push To CocoaPods
pod repo add CoreLib git@git.test/CoreLib.git
pod repo push CoreLib CoreLib.podspec --allow-warnings
複製代碼

4、各個組件該如何拆分

關於組件該如何拆分,這個沒有一個完整的標準,由於每一個公司的業務場景不同,對應衍生出來的各個業務模塊也就不同,因此業務組件間的拆分,這個根據本身公司的業務模塊來進行合理的劃分便可。這裏咱們來講下整個工程的組件大體的劃分方向。

    1. 項目主工程:當咱們工程徹底使用組件化架構進行開發後,咱們會驚奇的發現咱們的主工程就成了一個空殼子工程。由於全部的主工程呈現出來的內容都被拆分紅了各個獨立的業務組件了,包括各個工具組件也是各自互相獨立的。這樣咱們發現開發一個完整的APP就像是搭建樂高積木同樣,各個部件都有,任咱們隨意的組合搭建,這樣是否是感受很爽。
    1. 業務組件:業務組件就是咱們上面示例圖所示的各個獨立的產品業務功能模塊,咱們將其封裝成獨立的組件。例如示例Demo中的電子發票業務組件,業務組件A,業務組件B。咱們經過組裝各個獨立的業務組件來搭建一個完整的APP項目。
    1. 基礎工具類組件:基礎工具類是各個互相獨立,沒有任何依賴的工具組件。它們和其它的工具組件、業務組件等沒有任何依賴關係。這類組件例若有:對數組,字典進行異常保護的Safe組件,對數組功能進行擴展Array組件,對字符串進行加密處理的加密組件等等。
    1. 中間件組件:這個組件比較特殊,這個是咱們爲了實現組件化開發而衍生出來的一個組件,上面示例圖中的中間調度者就是一個功能獨立的中間件組件。
    1. 基礎UI組件:視圖組件就比較常見了,例如咱們封裝的導航欄組件,Modal彈框組件,PickerView組件等。
    1. 業務工具組件:這類組件是爲各個業務組件提供基礎功能的組件。這類組件可能會依賴到其餘的組件。例如:網絡請求組件,圖片緩存組件,jspatch組件等等

至於組件的拆分顆粒度,這個着實很差去判定,因人而異,不一樣的需求功能複雜度拆分出來的組件大小也不盡相同

總結

以上咱們只是講解了簡單的理論知識,若是你們要實戰的話仍是要多查閱一些資料,不過目前咱們的app使用的是組件化開發的方式,目前各個模塊解耦,能夠快速開發新的app。優勢仍是有不少的,但願你們也能夠敢於嘗試。不過組件化模塊化拆分也要考慮到本身的項目的,最終找到適合本身項目的方案纔是你要作的事情,但願本片文檔能夠爲你提供一些思路。

參考

相關文章
相關標籤/搜索