打造完備的 iOS 組件化方案:如何面向接口進行模塊解耦?
關於組件化的探討已經有不少了,在之前的文章iOS VIPER架構實踐(三):面向接口的路由設計中,綜合比較了各種方案後,我傾向於使用面向接口的方式進行組件化。
這是一篇從代碼層面講解模塊解耦的文章,會全方位地展示如何實踐面向接口的思想,儘量全面地探討在模塊管理和解耦的過程中,需要考慮到的各種問題,並且給出實際的解決方案,以及對應的模塊管理開源工具:ZIKRouter。你也可以根據本文的內容改造自己現有的方案。
文章主要內容:
- 如何衡量模塊解耦的程度
- 對比不同方案的優劣
- 在編譯時進行靜態路由檢查,避免使用不存在的模塊
- 如何進行模塊解耦,包括模塊重用、模塊適配、模塊間通信、子模塊交互
- 模塊的接口和依賴管理
- 管理界面跳轉邏輯
目錄
- 什麼是組件化
- 爲什麼要組件化
- 你的項目是否需要組件化
- 組件化方案的8條指標
- 方案對比
- URL 路由
- Target-Action 方案
- 基於 protocol 匹配的方案
- Protocol-Router 匹配方案
- 動態化的風險
- 靜態路由檢查
- 模塊解耦
- 模塊分類
- 什麼是解耦
- 模塊重用
- 依賴管理
- 依賴注入
- 分離模塊創建和配置
- 可選依賴:屬性注入和方法注入
- 必需依賴:工廠方法
- 避免接口污染
- 依賴查找
- 循環依賴
- 模塊適配器
- required protocol 和 provided protocol
- 模塊間通信
- 控制流 input 和 output
- 設置 input 和 output
- 子模塊
- Output 的適配
- 功能擴展
- 自動註冊
- 封裝界面跳轉
- 自定義跳轉
- 支持 storyboard
- URL 路由
- 用 router 對象代替 router 子類
- 簡化 router 實現
- 事件處理
- 單元測試
- 接口版本管理
- 最終形態
- 基於接口進行解耦的優勢
什麼是組件化
將模塊單獨抽離、分層,並制定模塊間通信的方式,從而實現解耦,以及適應團隊開發。
爲什麼需要組件化
主要有4個原因:
- 模塊間解耦
- 模塊重用
- 提高團隊協作開發效率
- 單元測試
當項目越來越大的時候,各個模塊之間如果是直接互相引用,就會產生許多耦合,導致接口濫用,當某天需要進行修改時,就會牽一髮而動全身,難以維護。
問題主要體現在:
- 修改某個模塊的功能時,需要修改許多其他模塊的代碼,因爲這個模塊被其他模塊引用
- 模塊對外的接口不明確,外部甚至會調用不應暴露的私有接口,修改時會耗費大量時間
- 修改的模塊涉及範圍較廣,很容易影響其他團隊成員的開發,產生代碼衝突
- 當需要抽離模塊到其他地方重用時,會發現耦合導致根本無法單獨抽離
- 模塊間的耦合導致接口和依賴混亂,難以編寫單元測試
所以需要減少模塊之間的耦合,用更規範的方式進行模塊間交互。這就是組件化,也可以叫做模塊化。
你的項目是否需要組件化
組件化也不是必須的,有些情況下並不需要組件化:
- 項目較小,模塊間交互簡單,耦合少
- 模塊沒有被多個外部模塊引用,只是一個單獨的小模塊
- 模塊不需要重用,代碼也很少被修改
- 團隊規模很小
- 不需要編寫單元測試
組件化也是有一定成本的,你需要花時間設計接口,分離代碼,所以並不是所有的模塊都需要組件化。
不過,當你發現這幾個跡象時,就需要考慮組件化了:
- 模塊邏輯複雜,多個模塊間頻繁互相引用
- 項目規模逐漸變大,修改代碼變得越來越困難
- 團隊人數變多,提交的代碼經常和其他成員衝突
- 項目編譯耗時較大
- 模塊的單元測試經常由於其他模塊的修改而失敗
組件化方案的8條指標
決定了要開始組件化之路後,就需要思考我們的目標了。一個組件化方案需要達到怎樣的效果呢?我在這裏給出8個理想情況下的指標:
- 模塊間沒有直接耦合,一個模塊內部的修改不會影響到另一個模塊
- 模塊可以被單獨編譯
- 模塊間能夠清晰地進行數據傳遞
- 模塊可以隨時被另一個提供了相同功能的模塊替換
- 模塊的對外接口容易查找和維護
- 當模塊的接口改變時,使用此模塊的外部代碼能夠被高效地重構
- 儘量用最少的修改和代碼,讓現有的項目實現模塊化
- 支持 Objective-C 和 Swift,以及混編
前4條用於衡量一個模塊是否真正解耦,後4條用於衡量在項目實踐中的易用程度。最後一條必須支持 Swift,是因爲 Swift 是一個必然的趨勢,如果你的方案不支持 Swift,說明這個方案在將來的某個時刻必定要改進改變,而到時候所有基於這個方案實現的模塊都會受到影響。
基於這8個指標,我們就能在一定程度上對我們的方案做出衡量了。
方案對比
現在主要有3種組件化方案:URL 路由、target-action、protocol 匹配。
接下來我們就比較一下這幾種組件化方案,看看它們各有什麼優缺點。這部分在之前的文章中已經探討過,這裏再重新比較一次,補充一些細節。必須要先說明的是,沒有一個完美的方案能滿足所有場景下的需求,需要根據每個項目的需求選擇最適合的方案。
URL 路由
目前 iOS 上絕大部分的路由工具都是基於 URL 匹配的,或者是根據命名約定,用 runtime 方法進行動態調用。
這些動態化的方案的優點是實現簡單,缺點是需要維護字符串表,或者依賴於命名約定,無法在編譯時暴露出所有問題,需要在運行時才能發現錯誤。
代碼示例:
// 註冊某個URL
[URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {
UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];
return editorViewController;
}];
// 調用路由
[URLRouter openURL:@"app://editor/?debug=true" completion:^(NSDictionary *info) {
}];
URL router 的優點:
- 極高的動態性,適合經常開展運營活動的 app,例如電商
- 方便地統一管理多平臺的路由規則
- 易於適配 URL Scheme
URL router 的缺點:
- 傳參方式有限,並且無法利用編譯器進行參數類型檢查,因此所有的參數都只能從字符串中轉換而來
- 只適用於界面模塊,不適用於通用模塊
- 不能使用 designated initializer 聲明必需參數
- 要讓 view controller 支持 url,需要爲其新增初始化方法,因此需要對模塊做出修改
- 不支持 storyboard
- 無法明確聲明模塊提供的接口,只能依賴於接口文檔,重構時無法確保修改正確
- 依賴於字符串硬編碼,難以管理
- 無法保證所使用的模塊一定存在
- 解耦能力有限,url 的"註冊"、"實現"、"使用"必須用相同的字符規則,一旦任何一方做出修改都會導致其他方的代碼失效,並且重構難度大
字符串解耦的問題
如果用上面的8個指標來衡量,URL 路由只能滿足"支持模塊單獨編譯"、"支持 OC 和 Swift"兩條。它的解耦程度非常一般。
所有基於字符串的解耦方案其實都可以說是僞解耦,它們只是放棄了編譯依賴,但是當代碼變化之後,即便能夠編譯運行,邏輯仍然是錯誤的。
例如修改了模塊定義時的 URL:
// 註冊某個URL
[URLRouter registerURL:@"app://editorView" handler:^(NSDictionary *userInfo) {
...
}];
那麼調用者的 URL 也必須修改,代碼仍然是有耦合的,只不過此時編譯器無法檢查而已。這會導致維護更加困難,一旦 URL 中的參數有了增減,或者決定替換爲另一個模塊,參數命名有了變化,幾乎沒有高效的方式來重構代碼。可以使用宏定義來管理字符串,不過這要求所有模塊都使用同一個頭文件,並且也無法解決參數類型和數量變化的問題。
URL 路由適合用來做遠程模塊的網絡協議交互,而在管理本地模塊時,最大的甚至是唯一的優勢,就是適合經常跨多端運營活動的 app,因爲可以由運營人員統一管理多平臺的路由規則。
代表框架
改進:避免字符串管理
改進 URL 路由的方式,就是避免使用字符串,通過接口管理模塊。
參數可以通過 protocol 直接傳遞,能夠利用編譯器檢查參數類型,並且在 ZIKRouter 中,能通過路由聲明和編譯檢查,保證所使用的模塊一定存在。在爲模塊創建路由時,也無需修改模塊的代碼。
但是必須要承認的是,儘管 URL 路由缺點多多,但它在跨平臺路由管理上的確是最適合的方案。因此 ZIKRouter 也對 URL 路由做出了支持,在用 protocol 管理的同時,可以通過字符串匹配 router,也能和其他 URL router 框架對接。
Target-Action 方案
有一些模塊管理工具基於 Objective-C 的 runtime、category 特性動態獲取模塊。例如通過NSClassFromString
獲取類並創建實例,通過performSelector:
NSInvocation
動態調用方法。
例如基於 target-action 模式的設計,大致是利用 category 爲路由工具添加新接口,在接口中通過字符串獲取對應的類,再用 runtime 創建實例,動態調用實例的方法。
示例代碼:
// 模塊管理者,提供了動態調用 target-action 的基本功能
@interface Mediator : NSObject
+ (instancetype)sharedInstance;
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
@end
// 在 category 中定義新接口
@interface Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController;
@end
@implementation Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController {
// 使用字符串硬編碼,通過 runtime 動態創建 Target_Editor,並調用 Action_viewController:
UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];
return viewController;
}
@end
// 調用者通過 Mediator 的接口調用模塊
UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
// 模塊提供者提供 target-action 的調用方式
@interface Target_Editor : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
@implementation Target_Editor
- (UIViewController *)Action_viewController:(NSDictionary *)params {
// 參數通過字典傳遞,無法保證類型安全
EditorViewController *viewController = [[EditorViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
優點:
- 利用 category 可以明確聲明接口,進行編譯檢查
- 實現方式輕量
缺點:
- 需要在 mediator 和 target 中重新添加每一個接口,模塊化時代碼較爲繁瑣
- 在 category 中仍然引入了字符串硬編碼,內部使用字典傳參,一定程度上也存在和 URL 路由相同的問題
- 無法保證所使用的模塊一定存在,target 模塊在修改後,使用者只有在運行時才能發現錯誤
- 過於依賴 runtime 特性,無法應用到純 Swift 上。在 Swift 中擴展 mediator 時,無法使用純 Swift 類型的參數
- 可能會創建過多的 target 類
- 使用 runtime 相關的接口調用任意類的任意方法,需要注意別被蘋果的審覈誤傷。參考:Are performSelector and respondsToSelector banned by App Store?
字典傳參的問題
字典傳參時無法保證參數的數量和類型,只能依賴調用約定,就和字符串傳參一樣,一旦某一方做出修改,另一方也必須修改。
相比於 URL 路由,target-action 通過 category 的接口把字符串管理的問題縮小到了 mediator 內部,不過並沒有完全消除,而且在其他方面仍然有很多改進空間。上面的8個指標中其實只能滿足第2個"支持模塊單獨編譯",另外在和接口相關的第3、5、6點上,比 URL 路由要有改善。
代表框架
改進:避免字典傳參
Target-Action 方案最大的優點就是整個方案實現輕量,並且也一定程度上明確了模塊的接口。只是這些接口都需要通過 Target-Action 封裝一次,並且每個模塊都要創建一個 target 類,既然如此,直接用 protocol 進行接口管理會更加簡單。
ZIKRouter 避免使用 runtime 獲取和調用模塊,因此可以適配 OC 和 swift。同時,基於 protocol 匹配的方式,避免引入字符串硬編碼,能夠更好地管理模塊,也避免了字典傳參。
基於 protocol 匹配的方案
有一些模塊管理工具或者依賴注入工具,也實現了基於接口的管理方式。實現思路是將 protocol 和對應的類進行字典匹配,之後就可以用 protocol 獲取 class,再動態創建實例。
BeeHive 示例代碼:
// 註冊模塊 (protocol-class 匹配)
[[BeeHive shareInstance] registerService:@protocol(EditorViewProtocol) service:[EditorViewController class]];
// 獲取模塊 (用 runtime 創建 EditorViewController 實例)
id<EditorViewProtocol> editor = [[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)];
優點:
- 利用接口調用,實現了參數傳遞時的類型安全
- 直接使用模塊的 protocol 接口,無需再重複封裝
缺點:
- 由框架來創建所有對象,創建方式有限,例如不支持外部傳入參數,再調用自定義初始化方法
- 用 OC runtime 創建對象,不支持 Swift
- 只做了 protocol 和 class 的匹配,不支持更復雜的創建方式和依賴注入
- 無法保證所使用的 protocol 一定存在對應的模塊,也無法直接判斷某個 protocol 是否能用於獲取模塊
相比直接 protocol-class 匹配的方式,protocol-block 的方式更加易用。例如 Swinject。
Swinject 示例代碼:
let container = Container()
// 註冊模塊
container.register(EditorViewProtocol.self) { _ in
return EditorViewController()
}
// 獲取模塊
let editor = container.resolve(EditorViewProtocol.self)!
代表框架
改進:離散式管理
BeeHive 這種方式和 ZIKRouter 的思路類似,但是所有的模塊在註冊後,都是由 BeeHive 單例來創建,使用場景十分有限,例如不支持純 Swift 類型,不支持使用自定義初始化方法以及額外的依賴注入。
ZIKRouter 進行了進一步的改進,並不是直接對 protocol 和 class 進行匹配,而是將 protocol 和 router 子類或者 router 對象進行匹配,在 router 子類中再提供創建模塊的實例的方式。這時,模塊的創建職責就從 BeeHive 單例上轉到了每個單獨的 router 上,從集約型變成了離散型,擴展性進一步提升。
Protocol-Router 匹配方案
變成 protocol-router 匹配後,代碼將會變成這樣:
一個 router 父類提供基礎的方法:
class ZIKViewRouter: NSObject {
...
// 獲取模塊
public class func makeDestination -> Any? {
let router = self.init(with: ViewRouteConfig())
return router.destination(with: router.configuration)
}
// 讓子類重寫
public func destination(with configuration: ViewRouteConfig) -> Any? {
return nil
}
}
<details><summary>Objective-C Sample</summary>
@interface ZIKViewRouter: NSObject
@end
@implementation ZIKViewRouter
...
// 獲取模塊
+ (id)makeDestination {
ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
return [router destinationWithConfiguration:router.configuration];
}
// 讓子類重寫
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return nil;
}
@end
</details>
每個模塊各自編寫自己的 router 子類:
// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter {
// 子類重寫,創建模塊
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}
}
<details><summary>Objective-C Sample</summary>
// editor 模塊的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
// 子類重寫,創建模塊
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
@end
</details>
把 protocol 和 router 類進行註冊綁定:
EditorViewRouter.register(RoutableView<EditorViewProtocol>())
<details><summary>Objective-C Sample</summary>
// 註冊 protocol 和 router
[EditorViewRouter registerViewProtocol:@protocol(EditorViewProtocol)];
</details>
然後就可以用 protocol 獲取 router 類,再進一步獲取模塊:
// 獲取模塊的 router 類
let routerClass = Router.to(RoutableView<EditorViewProtocol>())
// 獲取 EditorViewProtocol 模塊
let destination = routerClass?.makeDestination()
<details><summary>Objective-C Sample</summary>
// 獲取模塊的 router 類
Class routerClass = ZIKViewRouter.toView(@protocol(EditorViewProtocol));
// 獲取 EditorViewProtocol 模塊
id<EditorViewProtocol> destination = [routerClass makeDestination];
</details>
加了一層 router 中間層之後,解耦能力一下子就增強了:
- 可以在 router 上添加許多通用的擴展接口,例如創建模塊、依賴注入、界面跳轉、界面移除,甚至增加 URL 路由支持
- 在每個 router 子類中可以進行更詳細的依賴注入和自定義操作
- 可以自定義創建對象的方式,例如自定義初始化方法、工廠方法,在重構時可以直接搬運現有的創建代碼,無需在原來的類上增加或修改接口,減少模塊化過程中的工作量
- 可以讓多個 protocol 和同一個模塊進行匹配
- 可以讓模塊進行接口適配,允許外部做完適配後,爲 router 添加新的 protocol,解決編譯依賴的問題
- 返回的對象只需符合 protocol,不再和某個單一的類綁定。因此可以根據條件,返回不同的對象,例如適配不同系統版本時,返回不同的控件,讓外部只關注接口
動態化的風險
大部分組件化方案都會帶來一個問題,就是減弱甚至拋棄編譯檢查,因爲模塊已經變得高度動態化了。
當調用一個模塊時,怎麼能保證這個模塊一定存在?直接引用類時,如果類不存在,編譯器會給出引用錯誤,但是動態組件就無法在靜態時檢查了。
例如 URL 地址變化了,但是代碼中的某些 URL 沒有及時更新;使用 protocol 獲取模塊時,protocol 並沒有註冊對應的模塊。這些問題都只能在運行時才能發現。
那麼有沒有一種方式,可以讓模塊既高度解耦,又能在編譯時保證調用的模塊一定存在呢?
答案是 YES。
靜態路由檢查
ZIKRouter 最特別的功能,就是能夠保證所使用的 protocol 一定存在,在編譯階段就能防止使用不存在的模塊。這個功能可以讓你更安全、更簡單地管理所使用的路由接口,不必再用其他複雜的方式進行檢查和維護。
當使用了錯誤的 protocol 時,會產生編譯錯誤。
Swift 中使用未聲明的 protocol:
Objective-C 中使用未聲明的 protocol:
這個特性通過兩個機制來實現:
- 只有被聲明爲可路由的 protocol 才能用於路由,否則會產生編譯錯誤
- 可路由的 protocol 必定有一個對應的模塊存在
下面就一步步講解,怎麼在保持動態解耦特性的同時,實現一套完備的靜態類型檢查的機制。
路由聲明
怎麼才能聲明一個 protocol 是可以用於路由的呢?
要實現第一個機制,關鍵就是要爲 protocol 添加特殊的屬性或者類型,使用時,如果 protocol 不符合特定類型,就產生編譯錯誤。
原生 Xcode 並不支持這樣的靜態檢查,這時候就要考驗我們的創造力了。
Objective-C:protocol 繼承鏈
在 Objective-C 中,可以要求 protocol 必須繼承自某個特定的父 protocol,並且通過宏定義 + protocol 限定,對 protocol 的父 protocol 繼承鏈進行靜態檢查。
例如 ZIKRouter 中獲取 router 類的方法是這樣的:
@protocol ZIKViewRoutable
@end
@interface ZIKViewRouter()
@property (nonatomic, class, readonly) ZIKViewRouterType *(^toView)(Protocol<ZIKViewRoutable> *viewProtocol);
@end
toView
用類屬性的方式提供,以方便鏈式調用,這個 block 接收一個Protocol<ZIKViewRoutable> *
類型的 protocol,返回對應的 router 類。
Protocol<ZIKViewRoutable> *
表示這個 protocol 必須繼承自ZIKViewRoutable
。普通 protocol 的類型是Protocol *
,所以如果傳入@protocol(EditorViewProtocol)
就會產生編譯警告。
而如果用宏定義再給 protocol 變量加上一個 protocol 限定,進行一次類型轉換,就可以利用編譯器檢查 protocol 的繼承鏈:
// 聲明時繼承自 ZIKViewRoutable
@protocol EditorViewProtocol <ZIKViewRoutable>
@end
// 宏定義,爲 protocol 變量添加 protocol 限定
#define ZIKRoutable(RoutableProtocol) (Protocol<RoutableProtocol>*)@protocol(RoutableProtocol)
// 用 protocol 獲取 router
ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))
ZIKRoutable(EditorViewProtocol)
展開後是(Protocol<EditorViewProtocol> *)@protocol(EditorViewProtocol)
,類型爲Protocol<EditorViewProtocol> *
。在 Objective-C 中Protocol<EditorViewProtocol> *
是Protocol<ZIKViewRoutable> *
的子類型,編譯器將不會有警告。
但是當傳入的 protocol 沒有繼承自ZIKViewRoutable
時,例如ZIKRoutable(UndeclaredProtocol)
的類型是Protocol<UndeclaredProtocol> *
,編譯器在檢查 protocol 的繼承鏈時,由於UndeclaredProtocol
沒有繼承自ZIKViewRoutable
,因此Protocol<UndeclaredProtocol> *
不是Protocol<ZIKViewRoutable> *
的子類型,編譯器會給出類型錯誤的警告。在Build Settings
中可以把incompatible pointer types
警告變成編譯錯誤。
最後,把ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))
用宏定義簡化一下,變成ZIKViewRouterToView(EditorViewProtocol)
,就能在獲取 router 的時候方便地靜態檢查 protocol 的類型了。
Swift:條件擴展
Swift 中不支持宏定義,也不能隨意進行類型轉換,因此需要換一種方式來進行編譯檢查。
可以用 struct 的泛型傳遞 protocol,然後用條件擴展爲特定泛型的 struct 添加初始化方法,從而讓沒有聲明過的泛型類型不能直接創建 struct。
例如:
// 用 RoutableView 的泛型來傳遞 protocol
struct RoutableView<Protocol> {
// 禁止默認的初始化方法
@available(*, unavailable, message: "Protocol is not declared as routable")
public init() { }
}
// 泛型爲 EditorViewProtocol 的擴展
extension RoutableView where Protocol == EditorViewProtocol {
// 允許初始化
init() { }
}
// 泛型爲 EditorViewProtocol 時可以初始化
RoutableView<EditorViewProtocol>()
// 沒有聲明過的泛型無法初始化,會產生編譯錯誤
RoutableView<UndeclaredProtocol>()
此時 Xcode 還可以給出自動補全,列出所有聲明過的 protocol:
路由檢查
通過路由聲明,我們做到了在編譯時對所使用的 protocol 做出限制。下一步就是保證聲明過的 protocol 必定有對應的模塊,類似於程序在 link 階段,會檢查頭文件中聲明過的類必定有對應的實現。
這一步是無法直接在編譯階段實現的,不過可以參考 iOS 在啓動時檢查動態庫的方式,我們可以在啓動階段實現這個功能。
Objective-C: protocol 遍歷
在 app 以 DEBUG 模式啓動時,我們可以遍歷所有繼承自 ZIKViewRoutable 的 protocol,在註冊表中檢查是否有對應的 router,如果沒有,就給出斷言錯誤。
另外,還可以讓 router 同時註冊創建模塊時用到類:
EditorViewRouter.registerView(EditorViewController.self)
<details><summary>Objective-C Sample</summary>
// 註冊 protocol 和 router
[EditorViewRouter registerView:[EditorViewController class]];
</details>
從而進一步檢查 router 中的 class 是否遵守對應的 protocol。這時整個類型檢查過程就完整了。
Swift: 符號遍歷
但是 Swift 中的 protocol 是靜態類型,並不能通過 OC runtime 直接遍歷。是不是就無法動態檢查了呢?其實只要發揮創造力,一樣能做到。
Swift 的泛型名會在符號名中體現出來。例如上面聲明的 init 方法:
// MyApp 中,泛型爲 EditorViewProtocol 的擴展
extension RoutableView where Protocol == EditorViewProtocol {
// 允許初始化
init() { }
}
在還原符號後就是(extension in MyApp):ZRouter.RoutableView<A where A == MyApp.EditorViewProtocol>.init() -> ZRouter.RoutableView<MyApp.EditorViewProtocol>
。
此時我們可以遍歷 app 的符號表,來查找 RoutableView 的所有擴展,從而提取出所有聲明過的 protocol 類型,再去檢查是否有對應的 router。
Swift Runtime 和 ABI
但是如果要進一步檢查 router 中的 class 是否遵守 router 中的 protocol,就會遇到問題了。在 Swift 中怎麼檢查某個任意的 class 遵守某個 Swift protocol ?
Swift 中沒有直接提供class_conformsToProtocol
這樣的函數,不過我們可以通過 Swift Runtime 提供的標準函數和 Swift ABI 中定義的內存結構,完成同樣的功能。
這部分的實現可以參考代碼:_swift_typeIsTargetType。之後我會寫幾篇文章詳細講解 Swift ABI 的底層內容。
路由檢查這部分只在 DEBUG 模式下進行,因此可以放開折騰。
自動推斷返回值類型
還有最後一個問題,在 BeeHive 中使用[[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)]
獲取模塊時,返回值是一個id
類型,使用者需要手動指定返回變量的類型,在 Swift 中更是需要手動類型轉換,而這一步是可能出錯的,並且編譯器無法檢查。要實現最完備的類型檢查,就不能忽視這個問題。
有沒有一種方式能讓返回值的類型和 protocol 的類型對應呢?OC 中的泛型在這時候就發揮作用了。
可以在 router 上聲明模塊的泛型:
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *> : NSObject
@end
這裏使用了兩個泛型參數 Destination
和 RouteConfig
,分別表示此 router 所管理的模塊類型和路由 config 的類型。__covariant
則表示這個泛型支持協變,也就是子類型可以和父類型一樣使用。
聲明瞭泛型參數後,我們可以在方法中的參數聲明中使用泛型:
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *> : NSObject
- (nullable Destination)makeDestination;
- (nullable Destination)destinationWithConfiguration:(RouteConfig)configuration;
@end
此時在獲取 router 時,就可以把 protocol 的類型作爲 router 的泛型參數:
#define ZIKRouterToView(ViewProtocol) [ZIKViewRouter<id<ViewProtocol>,ZIKViewRouteConfiguration *> toView](ZIKRoutable(ViewProtocol))
使用ZIKRouterToView(EditorViewProtocol)
獲取的 router 類型就是ZIKViewRouter<id<EditorViewProtocol>,ZIKViewRouteConfiguration *>
。在這個 router 上調用makeDestination
時,返回值的類型就是id<EditorViewProtocol>
,從而實現了完整的類型傳遞。
而在 Swift 中,直接用函數泛型就能實現:
class Router {
static func to<Protocol>(_ routableView: RoutableView<Protocol>) -> ViewRouter<Protocol, ViewRouteConfig>?
}
使用Router.to(RoutableView<EditorViewProtocol>())
時,獲得的 router 類型就是ViewRouter<EditorViewProtocol, ViewRouteConfig>?
,在調用makeDestination
時,返回值類型就是EditorViewProtocol
,無需手動類型轉換。
如果你使用協議組合,還能同時指明多個類型:
typealias EditorViewProtocol = UIViewController & EditorViewInput
並且在 router 子類中重寫對應方法時,也能用泛型進一步確保類型正確:
class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ZIKViewRouteConfiguration> {
override func destination(with configuration: ZIKViewRouteConfiguration) -> EditorViewProtocol? {
// 函數重寫時,參數類型會和泛型一致,實現時能確保返回值的類型是正確的
return EditorViewController()
}
}
現在我們完成了一套完備的類型檢查機制,而且這套檢查同時支持 OC 和 Swift。
至此,一個基於接口的、類型安全的模塊管理工具就完成了。使用 makeDestination
創建模塊只是最基本的功能,我們可以在父類 router 中進行許多有用的功能擴展,例如依賴注入、界面跳轉、接口適配,來更好地進行面向接口的開發。
模塊解耦
那麼在面向接口編程時,我們還需要哪些功能呢?在擴展之前,我們先來討論一下如何使用接口進行模塊解耦,首先從理論層面梳理,再把理論轉化爲工具。
模塊分類
不同模塊對解耦的要求是不同的。模塊從層級上可以從低到高分類:
- 底層功能模塊,功能單一,有一定通用性,例如各種功能組件(日誌、數據庫)。底層模塊的主要目的是複用
- 中間層的通用業務模塊,可以在不同項目中通用。會引用各種底層模塊,以及和其他業務模塊通信
- 中間層的特殊功能模塊,提供了獨特的功能,沒有通用性,可能會引用一些底層模塊,例如性能監控模塊。這種模塊可以被其他模塊直接引用,不用太多考慮模塊間解耦的問題
- 上層的專有業務模塊,屬於某個項目中獨有的業務。會引用各種底層模塊,以及和其他業務模塊通信,和中間層的差別就是上層的解耦要求沒有中間層那麼高
什麼是解耦
首先明確一下什麼纔是解耦,梳理這個問題能夠幫助我們明確目標。
解耦的目的基本上就是兩個:提高代碼的可維護性、模塊重用。指導思想就是面向對象的設計原則。
解耦也有不同的程度,從低到高,差不多可以分爲3層:
- 模塊間使用抽象接口交互,沒有直接類型耦合,一個模塊內部的修改不會影響到另一個模塊 (單一職責、依賴倒置)
- 模塊可重用,可以被單獨編譯 (接口隔離、依賴倒置、控制反轉)
- 模塊可以隨時被另一個提供了相同功能的模塊替換 (開閉原則、依賴倒置、控制反轉)
第一層:抽象接口,提取依賴關係
第一層解耦,是爲了減少不同代碼間的依賴關係,讓代碼更容易維護。例如把類替換爲 protocol,隔絕模塊的私有接口,把依賴關係最小化。
解耦的整個過程,就是梳理和管理依賴的過程。因此模塊的內聚性越高越好,外部依賴越少越好,這樣維護起來才更簡單。
如果模塊不需要重用,那在這一層基本上就夠了。
第二層:模塊重用,管理模塊間通信
第二層解耦,是把代碼單獨抽離,做到了模塊重用,可以交給不同的成員維護,對模塊間通信提出了更高的要求。模塊需要在接口中聲明外部依賴,去除對特定類型的耦合。
此時影響最大的地方就是模塊間通信的方式,有時候即便是能夠單獨編譯了,也不意味着解耦。例如 URL 路由,只是放棄了編譯檢查,耦合關係還是存在於 URL 字符串中,一方的 URL 改變,其他方的代碼邏輯就會出錯,所以邏輯上仍然是耦合的。因此所有基於某種隱式調用約定的方案(例如字符串匹配),都只是解除編譯檢查,而不是真正的解耦。
有人說使用 protocol 進行模塊間通信,會導致模塊和 protocol 耦合。這個觀點是錯誤的。 protocol 恰恰是把模塊的依賴明確地提取出來,是一種更高效的方法。否則完全用隱式約定來進行通信,沒有編譯器的輔助,一旦模塊的接口名、參數類型、參數數量需要更新,將會非常難以維護。
而且,通過設計模式,是可以解除對特定 protocol 的依賴的,下文將會對此進行講解。
第三層:去除隱式約定
第三層解耦,模塊間做到了真正的解耦,只要兩個模塊提供了相同的功能,就可以無縫替換,並且調用方無需任何修改。被替換的模塊只需要提供相同功能的接口,通過適配器對接即可,沒有其他任何限制,不存在任何其他的隱式調用約定。
一般有這種解耦要求的,都是那些跨項目的通用模塊,而項目內專有的業務模塊則沒有這麼高的要求。不過那些跨多端的模塊和遠程模塊無法做到這樣的解耦,因爲跨多端時沒有統一的定義接口的方式,因此只能通過隱式約定或者網絡協議定義接口,例如 URL 路由。
總的來說,解耦的過程就是職責分離、依賴管理(依賴聲明和注入)、模塊通信 這三大部分。
模塊重用
要做到模塊重用,模塊需要儘量減少外部依賴,並且把依賴提取出來,體現到模塊的接口上,讓調用者主動注入。同時,把模塊的各種事件也提取出來,讓調用者進行處理。
這樣一來,模塊就只需要負責自身的邏輯,不需要關心調用者如何使用模塊。那些每個應用各自專有的應用層邏輯也就從模塊中分離出來了。
因此,要想做好模塊解耦,管理好依賴是非常重要的。而 protocol 接口就是管理依賴的最高效的方式。
依賴管理
依賴,就是模塊中用到的外部數據和外部模塊。接下來討論如何使用 protocol 管理依賴,並且演示如何用 router 實現。
依賴注入
先來複習一下依賴注入的概念。依賴注入和依賴查找是實現控制反轉思想的具體方式。
控制反轉是將對象依賴的獲取從主動變爲被動,從對象內部直接引用並獲取依賴,變爲由外部向對象提供對象所要求的依賴,把不屬於自己的職責移交出去,從而讓對象和其依賴解耦。此時控制流的主動權從內部轉移到了外部,因此稱爲控制反轉。
依賴注入就是指外部向對象傳入依賴。
一個類 A 在接口中體現出內部需要用到的一些依賴(例如內部需要用到類B的實例),從而讓使用者從外部注入這些依賴,而不是在類內部直接引用依賴並創建類 B。依賴可以用 protocol 的方式聲明,這樣就可以使類 A 和所使用的依賴類 B 進行解耦。
分離模塊創建和配置
那麼如何用 router 進行依賴注入呢?
模塊創建了實例後,經常還需要進行一些配置。模塊管理工具應該從設計上提供配置功能。
最簡單的方式,就是在destinationWithConfiguration:
中創建 destination 時進行配置。但是我們還可以更進一步,把 destination 的創建和配置分離開。分離之後,router 就可以單獨提供配置功能,去配置那些不是由 router 創建的 destination,例如 storyboard 中創建的 view、各種接口回調中返回的實例對象。這樣就可以覆蓋更多現存的使用場景,減少代碼修改。
Prepare Destination
可以在 router 子類中的prepareDestination:configuration:
中進行模塊配置,也就是依賴注入,而模塊的調用者無需關心這部分依賴是如何配置的:
// router 父類
class ZIKViewRouter<Destination, RouteConfig>: NSObject {
...
public class func makeDestination -> Destination? {
let router = self.init(with: ViewRouteConfig())
let destination = router.destination(with: router.configuration)
if let destination = destination {
// router 父類中調用模塊配置方法
router.prepareDestination(destination, configuration: router.configuration)
}
return destination
}
// 模塊創建,讓子類重寫
public func destination(with configuration: ViewRouteConfig) -> Destination? {
return nil
}
// 模塊配置,讓子類重寫
func prepareDestination(_ destination: Destination, configuration: RouteConfig) {
}
}
// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override func destination(with configuration: ViewRouteConfig) -> EditorViewController? {
let destination = EditorViewController()
return destination
}
// 配置模塊,注入靜態依賴
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依賴
destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
// 其他配置
destination.title = "默認標題"
}
}
<details><summary>Objective-C Sample</summary>
// router 父類
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *>: NSObject
@end
@implementation ZIKViewRouter
...
+ (id)makeDestination {
ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
id destination = [router destinationWithConfiguration:router.configuration];
if (destination) {
// router 父類中調用模塊配置方法
[router prepareDestination:destination configuration:router.configuration];
}
return destination;
}
// editor 模塊的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
// 配置模塊,注入靜態依賴
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// 注入 service 依賴
destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
// 其他配置
destination.title = @"默認標題";
}
@end
</details>
此時調用者中如果有某些對象不是創建自 router的,就可以直接用對應的 router 進行配置,執行依賴注入:
var destination: EditorViewProtocol = ...
Router.to(RoutableView<EditorViewProtocol>())?.prepare(destination: destination, configuring: { (config, _) in
})
<details><summary>Objective-C Sample</summary>
id<EditorViewProtocol> destination = ...
[ZIKRouterToView(EditorViewProtocol) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
}];
</details>
獨立的配置功能在某些場景下是非常有用的,尤其是在重構現有代碼的時候。有一些系統接口的設計就是在接口中返回對象,但是這些對象是由系統自動創建的,而不是通過 router 創建的,因此需要通過 router 對其進行配置,例如 storyboard 中創建的 view controller。此時將 view controller 模塊化後,依然可以保持現有代碼,只需要調用一句prepareDestination:configuration:
配置即可,模塊化的過程中就能讓代碼的修改最小化。
可選依賴:屬性注入和方法注入
當依賴是可選的,並不是創建對象所必需的,可以用屬性注入和方法注入。
屬性注入是指外部設置對象的屬性。方法注入是指外部調用對象的方法,從而傳入依賴。
protocol PersonType {
var wife: Person? { get set } // 可選的屬性依賴
func addChild(_ child: Person) -> Void // 可選的方法注入
}
protocol Child {
var parent: Person { get }
}
class Person: PersonType {
var wife: Person? = nil
var childs: Set<Child> = []
func addChild(_ child: Child) {
childs.insert(child)
}
}
<details><summary>Objective-C示例</summary>
@protocol PersonType: ZIKServiceRoutable
@property (nonatomic, strong, nullable) Person *wife; // 可選的屬性依賴
- (void)addChild:(Person *)child; // 可選的方法注入
@end
@protocol Child
@property (nonatomic, strong) Person *parent;
@end
@interface Person: NSObject <PersonType>
@property (nonatomic, strong, nullable) Person *wife;
@property (nonatomic, strong) NSSet<id<Child>> childs;
@end
</details>
在 router 裏,可以注入一些默認的依賴:
class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {
...
override func destination(with configuration: PerformRouteConfig) -> Person? {
let person = Person()
return person
}
// 配置模塊,注入靜態依賴
override func prepareDestination(_ destination: Person, configuration: PerformRouteConfig) {
if destination.wife != nil {
return
}
//設置默認值
let wife: Person = ...
person.wife = wife
}
}
<details><summary>Objective-C示例</summary>
@interface PersonRouter: ZIKServiceRouter<Person *, ZIKPerformRouteConfiguration *>
@end
@implementation PersonRouter
- (nullable Person *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
Person *person = [Person new];
return person;
}
// 配置模塊,注入靜態依賴
- (void)prepareDestination:(Person *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
if (destination.wife != nil) {
return;
}
Person *wife = ...
destination.wife = wife;
}
@end
</details>
模塊間參數傳遞
在執行路由操作的同時,調用者也可以用PersonType
動態地注入依賴,也就是向模塊傳參。
configuration 就是用來進行各種功能擴展的。Router 可以在 configuration 上提供prepareDestination
,讓調用者設置,就能讓調用者配置 destination。
let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), configuring: { (config, _) in
// 獲取模塊的同時進行配置
config.prepareDestination = { destination in
destination.wife = wife
destination.addChild(child)
}
})
<details><summary>Objective-C示例</summary>
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType)
makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration *config) {
// 獲取模塊的同時進行配置
config.prepareDestination = ^(id<PersonType> destination) {
destination.wife = wife;
[destination addChild:child];
};
}];
</details>
封裝一下就能變成更簡單的接口:
let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), preparation: { destination in
destination.wife = wife
destination.addChild(child)
})
<details><summary>Objective-C示例</summary>
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType)
makeDestinationWithPreparation:^(id<PersonType> destination) {
destination.wife = wife;
[destination addChild:child];
}];
</details>
必需依賴:工廠方法
有一些參數是在 destination 類創建前就需要傳入的必需參數,例如初始化方法中的參數,就是必需依賴。
class Person: PersonType {
let name: String
// 初始化方法,需要必需參數
init(name: String) {
self.name = name
}
}
<details><summary>Objective-C示例</summary>
@interface Person: NSObject <PersonType>
@property (nonatomic, strong) NSString *name;
// 初始化方法,需要必需參數
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
@end
</details>
這些必需參數有時候是由調用者提供的。在 URL 路由中,這種"必需"特性就無法體現出來,而用接口的方式就能簡單地實現。
傳遞必需依賴需要用工廠模式,在工廠方法上聲明必需參數和模塊接口。
protocol PersonTypeFactory {
// 工廠方法,聲明瞭必需參數 name,返回 PersonType 類型的 destination
func makeDestinationWith(_ name: String) -> PersonType?
}
<details><summary>Objective-C示例</summary>
@protocol PersonTypeFactory: ZIKServiceModuleRoutable
// 工廠方法,聲明瞭必需參數 name,返回 PersonType 類型的 destination
- (id<PersonType>)makeDestinationWith:(NSString *)name;
@end
</details>
那麼如何用 router 傳遞必需參數呢?
Router 的 configuration 可以用來進行自定義參數擴展。可以把必需參數保存到 configuration 上,或者更直接點,由 configuration 來提供工廠方法,然後使用工廠方法的 protocol 來獲取模塊:
// 通用 configuration,可以提供自定義工廠方法
class PersonModuleConfiguration: PerformRouteConfig, PersonTypeFactory {
// 工廠方法
public func makeDestinationWith(_ name: String) -> PersonType? {
self.makedDestination = Person(name: name)
return self.makedDestination
}
// 由工廠方法創建的 destination,提供給 router
public var makedDestination: Destination?
}
<details><summary>Objective-C示例</summary>
// 通用 configuration,可以提供自定義工廠方法
@interface PersonModuleConfiguration: ZIKPerformRouteConfiguration<PersonTypeFactory>
// 由工廠方法創建的 destination,提供給 router
@property (nonatomic, strong, nullable) id<PersonTypeFactory> makedDestination;
@end
@implementation PersonModuleConfiguration
// 工廠方法
-(id<PersonTypeFactory>)makeDestinationWith:(NSString *)name {
self.makedDestination = [[Person alloc] initWithName:name];
return self.makedDestination;
}
@end
</details>
在 router 中使用自定義 configuration:
class PersonRouter: ZIKServiceRouter<Person, PersonModuleConfiguration> {
// 重寫 defaultRouteConfiguration,使用自定義 configuration
override class func defaultRouteConfiguration() -> PersonModuleConfiguration {
return PersonModuleConfiguration()
}
override func destination(with configuration: PersonModuleConfiguration) -> Person? {
// 使用工廠方法創建的 destination
return config.makedDestination
}
}
<details><summary>Objective-C示例</summary>
@interface PersonRouter: ZIKServiceRouter<id<PersonType>, PersonModuleConfiguration *>
@end
@implementation PersonRouter
// 重寫 defaultRouteConfiguration,使用自定義 configuration
+ (PersonModuleConfiguration *)defaultRouteConfiguration {
return [PersonModuleConfiguration new];
}
- (nullable id<PersonType>)destinationWithConfiguration:(PersonModuleConfiguration *)configuration {
// 使用工廠方法創建的 destination
return configuration.makedDestination;
}
@end
</details>
然後把PersonTypeFactory
協議和 router 進行註冊:
PersonRouter.register(RoutableServiceModule<PersonTypeFactory>())
<details><summary>Objective-C示例</summary>
[PersonRouter registerModuleProtocol:ZIKRoutable(PersonTypeFactory)];
</details>
就可以用PersonTypeFactory
獲取模塊了:
let name: String = ...
Router.makeDestination(to: RoutableServiceModule<PersonTypeFactory>(), configuring: { (config, _) in
// config 遵守 PersonTypeFactory
config.makeDestinationWith(name)
})
<details><summary>Objective-C示例</summary>
NSString *name = ...
ZIKRouterToServiceModule(PersonTypeFactory) makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration<PersonTypeFactory> *config) {
// config 遵守 PersonTypeFactory
[config makeDestinationWith:name];
}]
</details>
用泛型代替 configuration 子類
如果你不需要在 configuration 上保存其他自定義參數,也不想創建過多的 configuration 子類,可以用一個通用的泛型類來實現子類重寫的效果。
泛型可以自定義參數類型,此時可以直接把工廠方法用 block 保存在 configuration 的屬性上。
// 通用 configuration,可以提供自定義工廠方法
class ServiceMakeableConfiguration<Destination, Constructor>: PerformRouteConfig {
public var makeDestinationWith: Constructor
public var makedDestination: Destination?
}
<details><summary>Objective-C示例</summary>
@interface ZIKServiceMakeableConfiguration<__covariant Destination>: ZIKPerformRouteConfiguration
@property (nonatomic, copy) Destination(^makeDestinationWith)();
@property (nonatomic, strong, nullable) Destination makedDestination;
@end
</details>
在 router 中使用自定義 configuration:
class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {
// 重寫 defaultRouteConfiguration,使用自定義 configuration
override class func defaultRouteConfiguration() -> PerformRouteConfig {
let config = ServiceMakeableConfiguration<PersonType, (String) -> PersonType>({ _ in})
// 設置工廠方法,讓調用者使用
config.makeDestinationWith = { [unowned config] name in
config.makedDestination = Person(name: name)
return config.makedDestination
}
return config
}
override func destination(with configuration: PerformRouteConfig) -> Person? {
if let config = configuration as? ServiceMakeableConfiguration<PersonType, (String) -> PersonType> {
// 使用工廠方法創建的 destination
return config.makedDestination
}
return nil
}
}
// 讓對應泛型的 configuration 遵守 PersonTypeFactory
extension ServiceMakeableConfiguration: PersonTypeFactory where Destination == PersonType, Constructor == (String) -> PersonType {
}
<details><summary>Objective-C示例</summary>
@interface PersonRouter: ZIKServiceRouter<id<PersonType>, ZIKServiceMakeableConfiguration *>
@end
@implementation PersonRouter
// 重寫 defaultRouteConfiguration,使用自定義 configuration
+ (ZIKServiceMakeableConfiguration *)defaultRouteConfiguration {
ZIKServiceMakeableConfiguration *config = [ZIKServiceMakeableConfiguration new];
__weak typeof(config) weakConfig = config;
// 設置工廠方法,讓調用者使用
config.makeDestinationWith = id ^(NSString *name) {
weakConfig.makedDestination = [[Person alloc] initWithName:name];
return weakConfig.makedDestination;
};
return config;
}
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
// 使用工廠方法創建的 destination
return configuration.makedDestination;
}
@end
</details>
避免接口污染
除了必需依賴,還有一些參數是不屬於 destination 類的,而是屬於模塊內其他組件的,也不能通過 destination 的接口來傳遞。例如 MVVM 和 VIPER 架構中,model 參數不能傳給 view,而是應該交給 view model 或者 interactor。此時可以使用相同的模式。
protocol EditorViewModuleInput {
// 工廠方法,聲明瞭參數 note,返回 EditorViewInput 類型的 destination
func makeDestinationWith(_ note: Note) -> EditorViewInput?
}
<details><summary>Objective-C示例</summary>
@protocol EditorViewModuleInput: ZIKViewModuleRoutable
// 工廠方法,聲明瞭參數 note,返回 EditorViewInput 類型的 destination
- (id<EditorViewInput>)makeDestinationWith:(Note *)note;
@end
</details>
class EditorViewRouter: ZIKViewRouter<EditorViewInput, ViewRouteConfig> {
// 重寫 defaultRouteConfiguration,使用自定義 configuration
override class func defaultRouteConfiguration() -> ViewRouteConfig {
let config = ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput>({ _ in})
// 設置工廠方法,讓調用者使用
config.makeDestinationWith = { [unowned config] note in
config.makedDestination = self.makeDestinationWith(note: note)
return config.makedDestination
}
return config
}
class func makeDestinationWith(note: Note) -> EditorViewInput {
let view = EditorViewController()
let presenter = EditorViewPresenter(view)
let interactor = EditorInteractor(Presenter)
// 把 model 傳遞給數據管理者,view 不接觸 model
interactor.note = note
return view
}
override func destination(with configuration: ViewRouteConfig) -> EditorViewInput? {
if let config = configuration as? ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput> {
// 使用工廠方法創建的 destination
return config.makedDestination
}
return nil
}
}
<details><summary>Objective-C示例</summary>
@interface EditorViewRouter: ZIKViewRouter<id<EditorViewInput>, ZIKViewMakeableConfiguration *>
@end
@implementation PersonRouter
// 重寫 defaultRouteConfiguration,使用自定義 configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
__weak typeof(config) weakConfig = config;
// 設置工廠方法,讓調用者使用
config.makeDestinationWith = id ^(Note *note) {
weakConfig.makedDestination = [self makeDestinationWith:note];
return weakConfig.makedDestination;
};
return config;
}
+ (id<EditorViewInput>)makeDestinationWith:(Note *)note {
EditorViewController *view = [[EditorViewController alloc] init];
EditorViewPresenter *presenter = [[EditorViewPresenter alloc] initWithView:view];
EditorInteractor *interactor = [[EditorInteractor alloc] initWithPresenter:presenter];
// 把 model 傳遞給數據管理者,view 不接觸 model
interactor.note = note;
return view;
}
- (nullable id<EditorViewInput>)destinationWithConfiguration:(ZIKViewMakeableConfiguration *)configuration {
// 使用工廠方法創建的 destination
return configuration.makedDestination;
}
@end
</details>
就可以用EditorViewModuleInput
獲取模塊了:
let note: Note = ...
Router.makeDestination(to: RoutableViewModule<EditorViewModuleInput>(), configuring: { (config, _) in
// config 遵守 EditorViewModuleInput
config.makeDestinationWith(note)
})
<details><summary>Objective-C示例</summary>
Note *note = ...
ZIKRouterToViewModule(EditorViewModuleInput) makeDestinationWithConfiguring:^(ZIKViewRouteConfiguration<EditorViewModuleInput> *config) {
// config 遵守 EditorViewModuleInput
config.makeDestinationWith(note);
}]
</details>
依賴查找
當模塊的必需依賴很多時,如果把依賴都放在初始化接口中,就會出現一個非常長的方法。
除了讓模塊把依賴聲明在接口中,模塊內部也可以用模塊管理工具動態查找依賴,例如用 router 查找 protocol 對應的模塊。如果要使用這種模式,那麼所有模塊都需要統一使用相同的模塊管理工具。
代碼如下:
class EditorViewController: UIViewController {
lazy var storageService: EditorStorageServiceInput {
return Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())!
}
}
<details><summary>Objective-C示例</summary>
@interface EditorViewController : UIViewController()
@property (nonatomic, strong) id<EditorStorageServiceInput> storageService;
@end
@implementation EditorViewController
- (id<EditorStorageServiceInput>)storageService {
if (!_storageService) {
_storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
}
return _storageService;
}
@end
</details>
循環依賴
使用依賴注入時,有些特殊情況需要處理,例如循環依賴的無限遞歸問題。
循環依賴是指兩個對象互相依賴。
在 router 內部動態注入依賴時,如果注入的依賴同時依賴於被注入的對象,則必須在 protocol 中聲明。
protocol Parent {
// Parent 依賴 Child
var child: Child { get set }
}
protocol Child {
// Child 依賴 Parent
var parent: Parent { get set }
}
class ParentObject: Parent {
var child: Child!
}
class ChildObject: Child {
var parent: Parent!
}
<details><summary>Objective-C示例</summary>
@protocol Parent <ZIKServiceRoutable>
// Parent 依賴 Child
@property (nonatomic, strong) id<Child> child;
@end
@protocol Child <ZIKServiceRoutable>
// Child 依賴 Parent
@property (nonatomic, strong) id<Parent> parent;
@end
@interface ParentObject: NSObject<Parent>
@end
@interface ParentObject: NSObject<Child>
@end
</details>
class ParentRouter: ZIKServiceRouter<ParentObject, PerformRouteConfig> {
override func destination(with configuration: PerformRouteConfig) -> ParentObject? {
return ParentObject()
}
override func prepareDestination(_ destination: ParentObject, configuration: PerformRouteConfig) {
guard destination.child == nil else {
return
}
// 只有在外部沒有設置 child 時,纔去主動尋找依賴
let child = Router.makeDestination(to RoutableService<Child>(), preparation { child in
// 設置 child 的依賴,防止 child 內部再去尋找 parent 依賴,導致循環
child.parent = destination
})
destination.child = child
}
}
class ChildRouter: ZIKServiceRouter<ChildObject, PerformRouteConfig> {
override func destination(with configuration: PerformRouteConfig) -> ChildObject? {
return ChildObject()
}
override func prepareDestination(_ destination: ChildObject, configuration: PerformRouteConfig) {
guard destination.parent == nil else {
return
}
// 只有在外部沒有設置 parent 時,纔去主動尋找依賴
let parent = Router.makeDestination(to RoutableService<Parent>(), preparation { parent in
// 設置 parent 的依賴,防止 parent 內部再去尋找 child 依賴,導致循環
parent.child = destination
})
destination.parent = parent
}
}
<details><summary>Objective-C示例</summary>
@interface ParentRouter: ZIKServiceRouter<ParentObject *, ZIKPerformRouteConfiguration *>
@end
@implementation ParentRouter
- (ParentObject *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
return [ParentObject new];
}
- (void)prepareDestination:(ParentObject *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
if (destination.child) {
return;
}
// 只有在外部沒有設置 child 時,纔去主動尋找依賴
destination.child = [ZIKRouterToService(Child) makeDestinationWithPreparation:^(id<Child> child) {
// 設置 child 的依賴,防止 child 內部再去尋找 parent 依賴,導致循環
child.parent = destination;
}];
}
@end
@interface ChildRouter: ZIKServiceRouter<ChildObject *, ZIKPerformRouteConfiguration *>
@end
@implementation ChildRouter
- (ChildObject *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
return [ChildObject new];
}
- (void)prepareDestination:(ChildObject *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
if (destination.parent) {
return;
}
// 只有在外部沒有設置 parent 時,纔去主動尋找依賴
destination.parent = [ZIKRouterToService(Parent) makeDestinationWithPreparation:^(id<Parent> parent) {
// 設置 parent 的依賴,防止 parent 內部再去尋找 child 依賴,導致循環
parent.child = destination;
}];
}
@end
</details>
這樣就能避免循環依賴導致的無限遞歸問題。
模塊適配器
當使用 protocol 管理模塊時,protocol 必定會出現在多個模塊中。那麼此時如何讓每個模塊單獨編譯呢?
一個方式是把 protocol 在每個用到的模塊裏複製一份,而且無需修改 protocol 名,Xcode 不會報錯。
另一個方式是使用適配器模式,可以讓不同模塊使用各自不同的 protocol 和同一個模塊交互。
required protocol 和 provided protocol
你可以爲同一個 router 註冊多個 protocol。
根據依賴關係,接口可以分爲required protocol
和provided protocol
。模塊本身提供的接口是provided protocol
,模塊的調用者需要使用的接口是required protocol
。
required protocol
是provided protocol
的子集,調用者只需要聲明自己用到的那些接口,不必引入整個provided protocol
,這樣可以讓模塊間的耦合進一步減少。
在 UML 的組件圖中,就很明確地表現出了這兩者的概念。下圖中的半圓就是Required Interface
,框外的圓圈就是Provided Interface
:
那麼如何實施Required Interface
和Provided Interface
?從架構分層上看,所有的模塊都是依附於一個更上層的宿主 app 環境存在的,應該由使用這些模塊的宿主 app 在一個 adapter 裏進行接口適配,從而使得調用者可以繼續在內部使用required protocol
,adapter 負責把required protocol
和修改後的provided protocol
進行適配。整個過程模塊都無感知。
這時候,調用者中定義的required protocol
就相當於是在聲明自己所依賴的外部模塊。
爲provided
模塊添加required protocol
模塊適配的工作全部由模塊的使用和裝配者 App Context 完成,最少時只需要兩行代碼。
例如,某個模塊需要展示一個登陸界面,而且這個登陸界面可以顯示一段自定義的提示語。
調用者模塊示例:
// 調用者中聲明的依賴接口,表明自身依賴一個登陸界面
protocol RequiredLoginViewInput {
var message: String? { get set } //顯示在登陸界面上的自定義提示語
}
// 調用者中調用 login 模塊
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
destination.message = "請登錄"
})
<details><summary>Objective-C示例</summary>
// 調用者中聲明的依賴接口,表明自身依賴一個登陸界面
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
// 調用者中調用 login 模塊
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
destination.message = @"請登錄";
}];
</details>
實際登陸界面提供的接口則是ProvidedLoginViewInput
:
// 實際登陸界面提供的接口
protocol ProvidedLoginViewInput {
var message: String? { get set }
}
<details><summary>Objective-C示例</summary>
// 實際登陸界面提供的接口
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
</details>
適配的代碼由宿主 app 實現,讓登陸界面支持 RequiredLoginViewInput
:
// 讓模塊支持 required protocol,只需要添加一個 protocol 擴展即可
extension LoginViewController: RequiredLoginViewInput {
}
<details><summary>Objective-C示例</summary>
// 讓模塊支持 required protocol,只需要添加一個 protocol 擴展即可
@interface LoginViewController (ModuleAAdapter) <RequiredLoginViewInput>
@end
@implementation LoginViewController (ModuleAAdapter)
@end
</details>
並且讓登陸界面的 router 也支持 RequiredLoginViewInput
:
// 如果可以獲取到 router 類,可以直接爲 router 添加 RequiredLoginViewInput
LoginViewRouter.register(RoutableView<RequiredLoginViewInput>())
// 如果不能得到對應模塊的 router,可以用 adapter 進行轉發
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
<details><summary>Objective-C示例</summary>
//如果可以獲取到 router 類,可以直接爲 router 添加 RequiredLoginViewInput
[LoginViewRouter registerViewProtocol:ZIKRoutable(RequiredLoginViewInput)];
//如果不能得到對應模塊的 router,可以註冊 adapter
[self registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
</details>
適配之後,RequiredLoginViewInput
就能和ProvidedLoginViewInput
一樣使用,獲取到同一個模塊了:
調用者模塊示例:
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
destination.message = "請登錄"
})
// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
Router.makeDestination(to: RoutableView<ProvidedLoginViewInput>(), preparation: {
destination.message = "請登錄"
})
<details><summary>Objective-C示例</summary>
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
destination.message = @"請登錄";
}];
// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<ProvidedLoginViewInput> destination) {
destination.message = @"請登錄";
}];
</details>
接口適配
有時候ProvidedLoginViewInput
和RequiredLoginViewInput
的接口名可能會稍有不同,此時需要用 category、extension、子類、proxy 類等方式進行接口適配。
protocol ProvidedLoginViewInput {
var notifyString: String? { get set } // 接口名不同
}
<details><summary>Objective-C示例</summary>
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString; // 接口名不同
@end
</details>
適配時需要進行接口轉發,讓登陸界面支持 RequiredLoginViewInput
:
extension LoginViewController: RequiredLoginViewInput {
var message: String? {
get {
return notifyString
}
set {
notifyString = newValue
}
}
}
<details><summary>Objective-C示例</summary>
@interface LoginViewController (ModuleAAdapter) <RequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapter)
- (void)setMessage:(NSString *)message {
self.notifyString = message;
}
- (NSString *)message {
return self.notifyString;
}
@end
</details>
用中介者轉發接口
如果不能直接爲模塊添加required protocol
,比如 protocol 裏的一些 delegate 需要兼容:
protocol RequiredLoginViewDelegate {
func didFinishLogin() -> Void
}
protocol RequiredLoginViewInput {
var message: String? { get set }
var delegate: RequiredLoginViewDelegate { get set }
}
<details><summary>Objective-C示例</summary>
@protocol RequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<RequiredLoginViewDelegate> delegate;
@end
</details>
而模塊裏的 delegate 接口不一樣:
protocol ProvidedLoginViewDelegate {
func didLogin() -> Void
}
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
var delegate: ProvidedLoginViewDelegate { get set }
}
<details><summary>Objective-C示例</summary>
@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<ProvidedLoginViewDelegate> delegate;
@end
</details>
相同方法有不同參數類型時,可以用一個新的 router 代替真正的 router,在新的 router 裏插入一箇中介者,負責轉發接口:
class ReqiredLoginViewRouter: ProvidedLoginViewRouter {
override func destination(with configuration: ZIKViewRouteConfiguration) -> RequiredLoginViewInput? {
let realDestination: ProvidedLoginViewInput = super.destination(with configuration)
// proxy 負責把 RequiredLoginViewInput 轉發爲 ProvidedLoginViewInput
let proxy: RequiredLoginViewInput = ProxyForDestination(realDestination)
return proxy
}
}
<details><summary>Objective-C示例</summary>
@interface ReqiredLoginViewRouter : ProvidedLoginViewRouter
@end
@implementation RequiredLoginViewRouter
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
id<ProvidedLoginViewInput> realDestination = [super destinationWithConfiguration:configuration];
// proxy 負責把 RequiredLoginViewInput 轉發爲 ProvidedLoginViewInput
id<RequiredLoginViewInput> proxy = ProxyForDestination(realDestination);
return mediator;
}
@end
</details>
對於普通OC類,proxy 可以用 NSProxy 來實現。對於 UIKit 中的那些複雜的 UI 類,或者 Swift 類,可以用子類,然後在子類中重寫方法,進行模塊適配。
聲明式依賴
利用之前的靜態路由檢查機制,模塊只需要聲明 required 接口,就能保證對應的模塊必定存在。
模塊無需在自己的接口裏聲明依賴,如果模塊需要新增依賴,只需要創建新的 required 接口即可,無需修改接口本身。這樣也能避免依賴變動導致的接口變化,減少接口維護的成本。
模塊提供默認的依賴配置
每次引入模塊,宿主 app 都需要寫一份適配代碼,雖然大多數情況下只有兩行,但是我們想盡量減少宿主 app 的維護職責。
此時,可以讓模塊提供一份默認的依賴,用宏定義包裹,繞過編譯檢查。
#if USE_DEFAULT_DEPENDENCY
import ProvidedLoginModule
public func registerDefaultDependency() {
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
}
extension ProvidedLoginViewController: RequiredLoginViewInput {
}
#endif
<details><summary>Objective-C示例</summary>
#if USE_DEFAULT_DEPENDENCY
@import ProvidedLoginModule;
static inline void registerDefaultDependency() {
[ZIKViewRouteAdapter registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
}
// 宏定義,默認的適配代碼
#define ADAPT_DEFAULT_DEPENDENCY \
@interface ProvidedLoginViewController (Adapter) <RequiredLoginViewInput> \
@end \
@implementation ProvidedLoginViewController (Adapter) \
@end \
#endif
</details>
如果宿主 app 要使用默認依賴,就在.xcconfig
裏設置Preprocessor Macros
,開啓宏定義:
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) USE_DEFAULT_DEPENDENCY=1
如果是 Swift 模塊,需要在模塊的 target 裏設置Active Compilation Conditions
,添加編譯宏USE_DEFAULT_DEPENDENCY
。
宿主 app 直接調用默認的適配代碼即可,不用再負責維護:
public func registerAdapters() {
// 註冊默認的依賴
registerDefaultDependency()
...
}
<details><summary>Objective-C示例</summary>
void registerAdapters() {
// 註冊默認的依賴
registerDefaultDependency();
...
}
// 使用默認的適配代碼
ADAPT_DEFAULT_DEPENDENCY
</details>
如果宿主 app 需要替換使用另一個 provided 模塊,可以關閉宏定義,再寫一份另外的適配代碼,即可替換依賴。
模塊化
區分了required protocol
和provided protocol
後,就可以實現真正的模塊化。在調用者聲明瞭所需要的required protocol
後,被調用模塊就可以隨時被替換成另一個相同功能的模塊。
參考 demo 中的ZIKLoginModule
示例模塊,登錄模塊依賴於一個彈窗模塊,而這個彈窗模塊在ZIKRouterDemo
和ZIKRouterDemo-macOS
中是不同的,而在切換彈窗模塊時,登錄模塊中的代碼不需要做任何改變。
使用 adapter 的規範
一般來說,並不需要立即把所有的 protocol 都分離爲required protocol
和provided protocol
。調用模塊和目的模塊可以暫時共用 protocol,或者只是簡單地改個名字,讓required protocol
作爲provided protocol
的子集,在第一次需要替換模塊的時候再用 category、extension、proxy、subclass 等技術進行接口適配。
接口適配也不能濫用,因爲成本比較高,而且並非所有的接口都能適配,例如同步接口和異步接口就難以適配。
對於模塊間耦合的處理,有這麼幾條建議:
- 如果依賴的是提供特定功能的模塊,沒有通用性,直接引用類即可
- 如果是依賴某些簡單的通用模塊(例如日誌模塊),可以在模塊的接口上把依賴交給外部來設置,例如 block 的形式
- 大部分需要解耦的模塊都是需要重用的業務模塊,如果你的模塊不需要重用,並且也不需要分工開發,直接引用對應類即可
- 大部分情況下建議共用 protocol,或者讓
required protocol
作爲provided protocol
的子集,接口名保持一致 - 只有在你的業務模塊的確允許使用者使用不同的依賴模塊時,才進行多個接口間的適配。例如需要跨平臺的模塊,例如登錄界面模塊允許不同的 app 使用不同的登陸 service 模塊
通過required protocol
和provided protocol
,我們就實現了模塊間的完全解耦。
模塊間通信
模塊間通信有多種方式,解耦程度也各有不同。這裏只討論接口交互的方式。
控制流 input 和 output
模塊的對外接口可以分爲 input 和 output。兩者的區別主要是控制流的主動權歸屬不同。
Input 是由外部主動調用的接口,控制流的發起者在外部,例如外部調用 view 的 UI 修改接口。
Output 是模塊內部主動調用外部實現的接口,控制流的發起者在內部,需要外部實現 output 所要求的方法。例如輸出 UI 事件、事件回調、獲取外部的 dataSource。iOS 中常用的 delegate 模式,也是一種 output。
設置 input 和 output
模塊設計好 input 和 output,然後在模塊創建的時候,設置好模塊之間的 input 和 output 關係,即可配置好模塊間通信,同時充分解耦。
class NoteListViewController: UIViewController, EditorViewOutput {
func showEditor() {
let destination = Router.makeDestination(to: RoutableView<EditorViewInput>(), preparation: { [weak self] destination in
destination.output = self
})
present(destination, animated: true)
}
}
protocol EditorViewInput {
weak var output: EditorViewOutput? { get set }
}
子模塊
大部分方案都沒有討論子模塊存在的情況。如果使用了 MVVM 或者 VIPER 架構,此時一個 view controller 使用了 child view controller,那多個模塊的 view model 和 interactor 之間如何交互?子模塊由誰初始化、由誰管理?
有些方案是直接在父 view model 裏創建和使用子 view model,但是這樣就導致了 view 的實現方式影響了view model 的實現,如果父 view 裏替換使用了另一個子 view,那父 view model 裏的代碼也需要修改。
子模塊的來源
子模塊的來源有:
- 父 view 引用了一個封裝好的子 view 控件,連帶着引入了子 view 的整個 MVVM 或者 VIPER 模塊
- View model 或者 interactor 裏使用了一個 Service
通信方式
子 view 可能是一個 UIView,也可能是一個 Child UIViewController。因此子 view 有可能需要向外部請求數據,也可能獨立完成所有任務,不需要依賴父模塊。
如果子 view 可以獨立,那在子模塊裏不會出現和父模塊交互的邏輯,只有把一些事件通過 output 傳遞出去的接口。這時只需要把子 view 的 input 接口封裝在父 view 的 input 接口裏即可,父 view model / presenter / interactor 是不知道父 view 提供的這幾個接口是通過子 view 實現的。
如果父模塊需要調用子模塊的業務接口,或接收子模塊的數據或業務事件,並且不想影響 view 的接口,可以把子 view model / presenter / interactor 作爲父 view model / presenter / interactor 的一個 service,在引入子模塊時,注入到父 view model / presenter / interactor,從而繞過 view 層。這樣子模塊和父模塊就能通過 service 的形式進行通信了,而這時,父模塊也不知道這個 service 是來自子模塊裏的。
在這樣的設計下,子模塊和父模塊是不知道彼此的存在的,只是通過接口進行交互。好處是父 view 如果想要更換爲另一個相同功能的子 view 控件,就只需要在父 view 裏修改,不會影響其他的 view model / presenter / interactor。
父模塊:
class EditorViewController: UIViewController {
var viewModel: EditorViewModel!
func addTextView() {
let textViewController = Router.makeDestination(to: RoutableView<TextViewInput>()) { (destination) in
// 設置模塊間交互
// 原本父 view 是無法接觸到子模塊的 view model / presenter / interactor
// 此時子模塊是把這些內部組件作爲業務 input 開放給了外部
self.viewModel.textService = destination.viewModel
destination.viewModel.output = self.viewModel
}
addChildViewController(textViewController)
view.addSubview(textViewController.view)
textViewController.didMove(toParentViewController: self)
}
}
<details><summary>Objective-C Sample</summary>
@interface EditorViewController: UIViewController
@property (nonatomic, strong) id<EditorViewModel> viewModel;
@end
@implementation EditorViewController
- (void)addTextView {
UIViewController *textViewController = [ZIKRouterToView(TextViewInput) makeDestinationWithPreparation:^(id<TextViewInput> destination) {
// 設置模塊間交互
// 原本父 view 是無法接觸到子模塊的 view model / presenter / interactor
// 此時子模塊是把這些內部組件作爲業務 input 開放給了外部
self.viewModel.textService = destination.viewModel;
destination.viewModel.output = self.viewModel;
}];
[self addChildViewController:textViewController];
[self.view addSubview: textViewController.view];
[textViewController didMoveToParentViewController: self];
}
@end
</details>
子模塊:
protocol TextViewInput {
weak var output: TextViewModuleOutput? { get set }
var viewModel: TextViewModel { get }
}
class TextViewController: UIViewController, TextViewInput {
weak var output: TextViewModuleOutput?
var viewModel: TextViewModel!
}
<details><summary>Objective-C Sample</summary>
@protocol TextViewInput <ZIKViewRoutable>
@property (nonatomic, weak) id<TextViewModuleOutput> output;
@property (nonatomic, strong) id<TextViewModel> viewModel;
@end
@interface TextViewController: UIViewController <TextViewInput>
@property (nonatomic, weak) id<TextViewModuleOutput> output;
@property (nonatomic, strong) id<TextViewModel> viewModel;
@end
</details>
Output 的適配
在使用 output 時,模塊適配會帶來一定麻煩。
例如這樣一對 required-provided protocol:
protocol RequiredEditorViewInput {
weak var output: RequiredEditorViewOutput? { get set }
}
protocol ProvidedEditorViewInput {
weak var output: ProvidedEditorViewOutput? { get set }
}
<details><summary>Objective-C Sample</summary>
@protocol RequiredEditorViewInput <NSObject>
@property (nonatomic, weak) id<RequiredEditorViewOutput> output;
@end
@protocol ProvidedEditorViewInput <NSObject>
@property (nonatomic, weak) id<ProvidedEditorViewOutput> output;
@end
</details>
由於 output 的實現者不是固定的,因此無法讓所有的 output 類都同時適配RequiredEditorViewOutput
和ProvidedEditorViewOutput
。此時建議直接使用對應的 protocol,不使用 required-provided 模式。
如果你仍然想要使用 required-provided 模式,那就需要用工廠模式來傳遞 output ,在內部用 proxy 進行適配。
實際模塊的 router:
protocol ProvidedEditorViewModuleInput {
var makeDestinationWith(_ output: ProvidedEditorViewOutput?) -> ProvidedEditorViewInput? { get set }
}
class ProvidedEditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
register(RoutableViewModule<ProvidedEditorViewModuleInput>())
}
override class func defaultRouteConfiguration() -> ViewRouteConfig {
let config = ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?) -> ProvidedViewInput?>({ _ in})
config.makeDestinationWith = { [unowned config] output in
// 設置 output
let viewModel = EditorViewModel(output: output)
config.makedDestination = EditorViewController(viewModel: viewModel)
return config.makedDestination
}
return config
}
override func destination(with configuration: ViewRouteConfig) -> EditorViewController? {
if let config = configuration as? ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?) {
return config.makedDestination
}
return nil
}
}
<details><summary>Objective-C Sample</summary>
@protocol ProvidedEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic, readonly) id<ProvidedEditorViewInput> (makeDestinationWith)(id<ProvidedEditorViewOutput> output);
@end
@interface ProvidedEditorViewRouter: ZIKViewRouter
@end
@implementation ProvidedEditorViewRouter
+ (void)registerRoutableDestination {
[self registerModuleProtocol:ZIKRoutable(ProvidedEditorViewModuleInput)];
}
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
__weak typeof(config) weakConfig = config;
config.makeDestinationWith = id ^(id<ProvidedEditorViewOutput> output) {
// 設置 output
EditorViewModel *viewModel = [[EditorViewModel alloc] initWithOutput:output];
weakConfig.makedDestination = [[EditorViewController alloc] initWithViewModel:viewModel];
return weakConfig.makedDestination;
};
return config;
}
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
return configuration.makedDestination;
}
@end
</details>
適配代碼:
protocol RequiredEditorViewModuleInput {
var makeDestinationWith(_ output: RequiredEditorViewOutput?) -> RequiredEditorViewInput? { get set }
}
// 用於適配的 required router
class RequiredEditorViewRouter: ProvidedEditorViewRouter {
override class func registerRoutableDestination() {
register(RoutableViewModule<RequiredEditorViewModuleInput>())
}
// 兼容 configuration
override class func defaultRouteConfiguration() -> PerformRouteConfig {
let config = super.defaultRouteConfiguration()
let makeDestinationWith = config.makeDestinationWith
config.makeDestinationWith = { requiredOutput in
// proxy 負責把 RequiredEditorViewOutput 轉爲 ProvidedEditorViewOutput
let providedOutput = EditorOutputProxy(forwarding: requiredOutput)
return makeDestinationWith(providedOutput)
}
return config
}
}
class EditorOutputProxy: ProvidedEditorViewOutput {
let forwarding: RequiredEditorViewOutput
// 實現 ProvidedEditorViewOutput,轉發給 forwarding
}
<details><summary>Objective-C Sample</summary>
@protocol RequiredEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic, readonly) id<RequiredEditorViewInput> (makeDestinationWith)(id<RequiredEditorViewOutput> output);
@end
// 用於適配的 required router
@interface RequiredEditorViewRouter: ProvidedEditorViewRouter
@end
@implementation RequiredEditorViewRouter
+ (void)registerRoutableDestination {
[self registerModuleProtocol:ZIKRoutable(RequiredEditorViewModuleInput)];
}
// 兼容 configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
ZIKViewMakeableConfiguration *config = [super defaultRouteConfiguration];
id<ProvidedEditorViewInput>(^makeDestinationWith)(id<ProvidedEditorViewOutput>) = config.makeDestinationWith;
config.makeDestinationWith = id ^(id<RequiredEditorViewOutput> requiredOutput) {
// proxy 負責把 RequiredEditorViewOutput 轉爲 ProvidedEditorViewOutput
EditorOutputProxy *providedOutput = [[EditorOutputProxy alloc] initWithForwarding: requiredOutput];
return makeDestinationWith(providedOutput);
};
return config;
}
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
return configuration.makedDestination;
}
@end
// 實現 ProvidedEditorViewOutput,轉發給 forwarding
@interface EditorOutputProxy: NSProxy <ProvidedEditorViewOutput>
@property (nonatomic, strong) id forwarding;
@end
@implementation EditorOutputProxy
- (instancetype)initWithForwarding:(id)forwarding {
if (self = [super init]) {
_forwarding = forwarding;
}
return self;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self.forwarding respondsToSelector:aSelector];
}
- (BOOL)conformsToProtocol:(Protocol *)protocol {
return [self.forwarding conformsToProtocol:protocol];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.forwarding;
}
@end
</details>
可以看到,output 的適配有些繁瑣。因此除非你的模塊是通用模塊,有實際的解耦需求,否則直接使用 provided protocol 即可。
文章內容過長,因簡書字數限制,繼續閱讀請看下一篇打造完備的 iOS 組件化方案:如何面向接口進行模塊解耦?(二)。