關於組件化的探討已經有很多了,在以前的文章iOS VIPER架構實踐(三):面向接口的路由設計中,綜合比較了各類方案後,我傾向於使用面向接口的方式進行組件化。html
這是一篇從代碼層面講解模塊解耦的文章,會全方位地展現如何實踐面向接口的思想,儘可能全面地探討在模塊管理和解耦的過程當中,須要考慮到的各類問題,而且給出實際的解決方案,以及對應的模塊管理開源工具:ZIKRouter。你也能夠根據本文的內容改造本身現有的方案,即便你的項目不進行組件化,也能夠參考本文進行代碼解耦。ios
文章主要內容:git
將模塊單獨抽離、分層,並制定模塊間通訊的方式,從而實現解耦,以及適應團隊開發。github
主要有4個緣由:objective-c
當項目愈來愈大的時候,各個模塊之間若是是直接互相引用,就會產生許多耦合,致使接口濫用,當某天須要進行修改時,就會牽一髮而動全身,難以維護。數據庫
問題主要體如今:編程
因此須要減小模塊之間的耦合,用更規範的方式進行模塊間交互。這就是組件化,也能夠叫作模塊化。swift
組件化也不是必須的,有些狀況下並不須要組件化:設計模式
組件化也是有必定成本的,你須要花時間設計接口,分離代碼,因此並非全部的模塊都須要組件化。api
不過,當你發現這幾個跡象時,就須要考慮組件化了:
決定了要開始組件化之路後,就須要思考咱們的目標了。一個組件化方案須要達到怎樣的效果呢?我在這裏給出8個理想狀況下的指標:
前4條用於衡量一個模塊是否真正解耦,後4條用於衡量在項目實踐中的易用程度。最後一條必須支持 Swift,是由於 Swift 是一個必然的趨勢,若是你的方案不支持 Swift,說明這個方案在未來的某個時刻一定要改進改變,而到時候全部基於這個方案實現的模塊都會受到影響。
基於這8個指標,咱們就能在必定程度上對咱們的方案作出衡量了。
如今主要有3種組件化方案:URL 路由、target-action、protocol 匹配。
接下來咱們就比較一下這幾種組件化方案,看看它們各有什麼優缺點。這部分在以前的文章中已經探討過,這裏再從新比較一次,補充一些細節。必需要先說明的是,沒有一個完美的方案能知足全部場景下的需求,須要根據每一個項目的需求選擇最適合的方案。
目前 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 的優勢:
URL router 的缺點:
若是用上面的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 框架對接。
有一些模塊管理工具基於 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
複製代碼
優勢:
缺點:
字典傳參時沒法保證參數的數量和類型,只能依賴調用約定,就和字符串傳參同樣,一旦某一方作出修改,另外一方也必須修改。
相比於 URL 路由,target-action 經過 category 的接口把字符串管理的問題縮小到了 mediator 內部,不過並無徹底消除,並且在其餘方面仍然有不少改進空間。上面的8個指標中其實只能知足第2個"支持模塊單獨編譯",另外在和接口相關的第三、五、6點上,比 URL 路由要有改善。
Target-Action 方案最大的優勢就是整個方案實現輕量,而且也必定程度上明確了模塊的接口。只是這些接口都須要經過 Target-Action 封裝一次,而且每一個模塊都要建立一個 target 類,既然如此,直接用 protocol 進行接口管理會更加簡單。
ZIKRouter 避免使用 runtime 獲取和調用模塊,所以能夠適配 OC 和 swift。同時,基於 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-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 匹配後,代碼將會變成這樣:
一個 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
}
}
複製代碼
@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
複製代碼
每一個模塊各自編寫本身的 router 子類:
// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter {
// 子類重寫,建立模塊
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}
}
複製代碼
// editor 模塊的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
// 子類重寫,建立模塊
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
@end
複製代碼
把 protocol 和 router 類進行註冊綁定:
EditorViewRouter.register(RoutableView<EditorViewProtocol>())
複製代碼
// 註冊 protocol 和 router
[EditorViewRouter registerViewProtocol:@protocol(EditorViewProtocol)];
複製代碼
而後就能夠用 protocol 獲取 router 類,再進一步獲取模塊:
// 獲取模塊的 router 類
let routerClass = Router.to(RoutableView<EditorViewProtocol>())
// 獲取 EditorViewProtocol 模塊
let destination = routerClass?.makeDestination()
複製代碼
// 獲取模塊的 router 類
Class routerClass = ZIKViewRouter.toView(@protocol(EditorViewProtocol));
// 獲取 EditorViewProtocol 模塊
id<EditorViewProtocol> destination = [routerClass makeDestination];
複製代碼
加了一層 router 中間層以後,解耦能力一會兒就加強了:
大部分組件化方案都會帶來一個問題,就是減弱甚至拋棄編譯檢查,由於模塊已經變得高度動態化了。
當調用一個模塊時,怎麼能保證這個模塊必定存在?直接引用類時,若是類不存在,編譯器會給出引用錯誤,可是動態組件就沒法在靜態時檢查了。
例如 URL 地址變化了,可是代碼中的某些 URL 沒有及時更新;使用 protocol 獲取模塊時,protocol 並無註冊對應的模塊。這些問題都只能在運行時才能發現。
那麼有沒有一種方式,可讓模塊既高度解耦,又能在編譯時保證調用的模塊必定存在呢?
答案是 YES。
ZIKRouter 最特別的功能,就是可以保證所使用的 protocol 必定存在,在編譯階段就能防止使用不存在的模塊。這個功能可讓你更安全、更簡單地管理所使用的路由接口,沒必要再用其餘複雜的方式進行檢查和維護。
當使用了錯誤的 protocol 時,會產生編譯錯誤。
Swift 中使用未聲明的 protocol:
Objective-C 中使用未聲明的 protocol:
這個特性經過兩個機制來實現:
下面就一步步講解,怎麼在保持動態解耦特性的同時,實現一套完備的靜態類型檢查的機制。
怎麼才能聲明一個 protocol 是能夠用於路由的呢?
要實現第一個機制,關鍵就是要爲 protocol 添加特殊的屬性或者類型,使用時,若是 protocol 不符合特定類型,就產生編譯錯誤。
原生 Xcode 並不支持這樣的靜態檢查,這時候就要考驗咱們的創造力了。
在 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 中不支持宏定義,也不能隨意進行類型轉換,所以須要換一種方式來進行編譯檢查。
能夠用 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 在啓動時檢查動態庫的方式,咱們能夠在啓動階段實現這個功能。
在 app 以 DEBUG 模式啓動時,咱們能夠遍歷全部繼承自 ZIKViewRoutable 的 protocol,在註冊表中檢查是否有對應的 router,若是沒有,就給出斷言錯誤。
另外,還可讓 router 同時註冊建立模塊時用到類:
EditorViewRouter.registerView(EditorViewController.self)
複製代碼
// 註冊 protocol 和 router
[EditorViewRouter registerView:[EditorViewController class]];
複製代碼
從而進一步檢查 router 中的 class 是否遵照對應的 protocol。這時整個類型檢查過程就完整了。
可是 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。
可是若是要進一步檢查 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、各類接口回調中返回的實例對象。這樣就能夠覆蓋更多現存的使用場景,減小代碼修改。
能夠在 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 = "默認標題"
}
}
複製代碼
// 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;
}
// 模塊建立,讓子類重寫
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return nil;
}
// 模塊配置,讓子類重寫
- (void)prepareDestination:(id)destination configuration:(ZIKViewRouteConfiguration *)configuration {
}
@end
// 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
複製代碼
此時調用者中若是有某些對象不是建立自 router的,就能夠直接用對應的 router 進行配置,執行依賴注入:
var destination: EditorViewProtocol = ...
Router.to(RoutableView<EditorViewProtocol>())?.prepare(destination: destination, configuring: { (config, _) in
})
複製代碼
id<EditorViewProtocol> destination = ...
[ZIKRouterToView(EditorViewProtocol) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
}];
複製代碼
獨立的配置功能在某些場景下是很是有用的,尤爲是在重構現有代碼的時候。有一些系統接口的設計就是在接口中返回對象,可是這些對象是由系統自動建立的,而不是經過 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)
}
}
複製代碼
@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
複製代碼
在 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
}
}
複製代碼
@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
複製代碼
在執行路由操做的同時,調用者也能夠用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)
}
})
複製代碼
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType)
makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration *config) {
// 獲取模塊的同時進行配置
config.prepareDestination = ^(id<PersonType> destination) {
destination.wife = wife;
[destination addChild:child];
};
}];
複製代碼
封裝一下就能變成更簡單的接口:
let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), preparation: { destination in
destination.wife = wife
destination.addChild(child)
})
複製代碼
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType)
makeDestinationWithPreparation:^(id<PersonType> destination) {
destination.wife = wife;
[destination addChild:child];
}];
複製代碼
有一些參數是在 destination 類建立前就須要傳入的必需參數,例如初始化方法中的參數,就是必需依賴。
class Person: PersonType {
let name: String
// 初始化方法,須要必需參數
init(name: String) {
self.name = name
}
}
複製代碼
@interface Person: NSObject <PersonType>
@property (nonatomic, strong) NSString *name;
// 初始化方法,須要必需參數
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
@end
複製代碼
這些必需參數有時候是由調用者提供的。在 URL 路由中,這種"必需"特性就沒法體現出來,而用接口的方式就能簡單地實現。
傳遞必需依賴須要用工廠模式,在工廠方法上聲明必需參數和模塊接口。
protocol PersonTypeFactory {
// 工廠方法,聲明瞭必需參數 name,返回 PersonType 類型的 destination
func makeDestinationWith(_ name: String) -> PersonType?
}
複製代碼
@protocol PersonTypeFactory: ZIKServiceModuleRoutable
// 工廠方法,聲明瞭必需參數 name,返回 PersonType 類型的 destination
- (id<PersonType>)makeDestinationWith:(NSString *)name;
@end
複製代碼
那麼如何用 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?
}
複製代碼
// 通用 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
複製代碼
在 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
}
}
複製代碼
@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
複製代碼
而後把PersonTypeFactory
協議和 router 進行註冊:
PersonRouter.register(RoutableServiceModule<PersonTypeFactory>())
複製代碼
[PersonRouter registerModuleProtocol:ZIKRoutable(PersonTypeFactory)];
複製代碼
就能夠用PersonTypeFactory
獲取模塊了:
let name: String = ...
Router.makeDestination(to: RoutableServiceModule<PersonTypeFactory>(), configuring: { (config, _) in
// config 遵照 PersonTypeFactory
config.makeDestinationWith(name)
})
複製代碼
NSString *name = ...
ZIKRouterToServiceModule(PersonTypeFactory) makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration<PersonTypeFactory> *config) {
// config 遵照 PersonTypeFactory
[config makeDestinationWith:name];
}]
複製代碼
若是你不須要在 configuration 上保存其餘自定義參數,也不想建立過多的 configuration 子類,能夠用一個通用的泛型類來實現子類重寫的效果。
泛型能夠自定義參數類型,此時能夠直接把工廠方法用 block 保存在 configuration 的屬性上。
// 通用 configuration,能夠提供自定義工廠方法
class ServiceMakeableConfiguration<Destination, Constructor>: PerformRouteConfig {
public var makeDestinationWith: Constructor
public var makedDestination: Destination?
}
複製代碼
@interface ZIKServiceMakeableConfiguration<__covariant Destination>: ZIKPerformRouteConfiguration
@property (nonatomic, copy) Destination(^makeDestinationWith)();
@property (nonatomic, strong, nullable) Destination makedDestination;
@end
複製代碼
在 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 {
}
複製代碼
@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
複製代碼
除了必需依賴,還有一些參數是不屬於 destination 類的,而是屬於模塊內其餘組件的,也不能經過 destination 的接口來傳遞。例如 MVVM 和 VIPER 架構中,model 參數不能傳給 view,而是應該交給 view model 或者 interactor。此時可使用相同的模式。
protocol EditorViewModuleInput {
// 工廠方法,聲明瞭參數 note,返回 EditorViewInput 類型的 destination
func makeDestinationWith(_ note: Note) -> EditorViewInput?
}
複製代碼
@protocol EditorViewModuleInput: ZIKViewModuleRoutable
// 工廠方法,聲明瞭參數 note,返回 EditorViewInput 類型的 destination
- (id<EditorViewInput>)makeDestinationWith:(Note *)note;
@end
複製代碼
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
}
}
複製代碼
@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
複製代碼
就能夠用EditorViewModuleInput
獲取模塊了:
let note: Note = ...
Router.makeDestination(to: RoutableViewModule<EditorViewModuleInput>(), configuring: { (config, _) in
// config 遵照 EditorViewModuleInput
config.makeDestinationWith(note)
})
複製代碼
Note *note = ...
ZIKRouterToViewModule(EditorViewModuleInput) makeDestinationWithConfiguring:^(ZIKViewRouteConfiguration<EditorViewModuleInput> *config) {
// config 遵照 EditorViewModuleInput
config.makeDestinationWith(note);
}]
複製代碼
當模塊的必需依賴不少時,若是把依賴都放在初始化接口中,就會出現一個很是長的方法。
除了讓模塊把依賴聲明在接口中,模塊內部也能夠用模塊管理工具動態查找依賴,例如用 router 查找 protocol 對應的模塊。若是要使用這種模式,那麼全部模塊都須要統一使用相同的模塊管理工具。
代碼以下:
class EditorViewController: UIViewController {
lazy var storageService: EditorStorageServiceInput {
return Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())!
}
}
複製代碼
@interface EditorViewController : UIViewController()
@property (nonatomic, strong) id<EditorStorageServiceInput> storageService;
@end
@implementation EditorViewController
- (id<EditorStorageServiceInput>)storageService {
if (!_storageService) {
_storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
}
return _storageService;
}
@end
複製代碼
使用依賴注入時,有些特殊狀況須要處理,例如循環依賴的無限遞歸問題。
循環依賴是指兩個對象互相依賴。
在 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!
}
複製代碼
@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
複製代碼
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
}
}
複製代碼
@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
複製代碼
這樣就能避免循環依賴致使的無限遞歸問題。
當使用 protocol 管理模塊時,protocol 一定會出如今多個模塊中。那麼此時如何讓每一個模塊單獨編譯呢?
一個方式是把 protocol 在每一個用到的模塊裏複製一份,並且無需修改 protocol 名,Xcode 不會報錯。
另外一個方式是使用適配器模式,可讓不一樣模塊使用各自不一樣的 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 = "請登陸"
})
複製代碼
// 調用者中聲明的依賴接口,代表自身依賴一個登錄界面
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
// 調用者中調用 login 模塊
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
destination.message = @"請登陸";
}];
複製代碼
實際登錄界面提供的接口則是ProvidedLoginViewInput
:
// 實際登錄界面提供的接口
protocol ProvidedLoginViewInput {
var message: String? { get set }
}
複製代碼
// 實際登錄界面提供的接口
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
複製代碼
適配的代碼由宿主 app 實現,讓登錄界面支持 RequiredLoginViewInput
:
// 讓模塊支持 required protocol,只須要添加一個 protocol 擴展便可
extension LoginViewController: RequiredLoginViewInput {
}
複製代碼
而且讓登錄界面的 router 也支持 RequiredLoginViewInput
:
// 若是能夠獲取到 router 類,能夠直接爲 router 添加 RequiredLoginViewInput
LoginViewRouter.register(RoutableView<RequiredLoginViewInput>())
// 若是不能獲得對應模塊的 router,能夠用 adapter 進行轉發
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
複製代碼
適配以後,RequiredLoginViewInput
就能和ProvidedLoginViewInput
同樣使用,獲取到同一個模塊了:
調用者模塊示例:
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
destination.message = "請登陸"
})
// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
Router.makeDestination(to: RoutableView<ProvidedLoginViewInput>(), preparation: {
destination.message = "請登陸"
})
複製代碼
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
destination.message = @"請登陸";
}];
// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<ProvidedLoginViewInput> destination) {
destination.message = @"請登陸";
}];
複製代碼
有時候ProvidedLoginViewInput
和RequiredLoginViewInput
的接口名可能會稍有不一樣,此時須要用 category、extension、子類、proxy 類等方式進行接口適配。
protocol ProvidedLoginViewInput {
var notifyString: String? { get set } // 接口名不一樣
}
複製代碼
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString; // 接口名不一樣
@end
複製代碼
適配時須要進行接口轉發,讓登錄界面支持 RequiredLoginViewInput
:
extension LoginViewController: RequiredLoginViewInput {
var message: String? {
get {
return notifyString
}
set {
notifyString = newValue
}
}
}
複製代碼
@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
複製代碼
若是不能直接爲模塊添加required protocol
,好比 protocol 裏的一些 delegate 須要兼容:
protocol RequiredLoginViewDelegate {
func didFinishLogin() -> Void
}
protocol RequiredLoginViewInput {
var message: String? { get set }
var delegate: RequiredLoginViewDelegate { get set }
}
複製代碼
@protocol RequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<RequiredLoginViewDelegate> delegate;
@end
複製代碼
而模塊裏的 delegate 接口不同:
protocol ProvidedLoginViewDelegate {
func didLogin() -> Void
}
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
var delegate: ProvidedLoginViewDelegate { get set }
}
複製代碼
@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<ProvidedLoginViewDelegate> delegate;
@end
複製代碼
相同方法有不一樣參數類型時,能夠用一個新的 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
}
}
複製代碼
@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
複製代碼
對於普通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
複製代碼
#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
複製代碼
若是宿主 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()
...
}
複製代碼
void registerAdapters() {
// 註冊默認的依賴
registerDefaultDependency();
...
}
// 使用默認的適配代碼
ADAPT_DEFAULT_DEPENDENCY
複製代碼
若是宿主 app 須要替換使用另外一個 provided 模塊,能夠關閉宏定義,再寫一份另外的適配代碼,便可替換依賴。
區分了required protocol
和provided protocol
後,就能夠實現真正的模塊化。在調用者聲明瞭所須要的required protocol
後,被調用模塊就能夠隨時被替換成另外一個相同功能的模塊。
參考 demo 中的ZIKLoginModule
示例模塊,登陸模塊依賴於一個彈窗模塊,而這個彈窗模塊在ZIKRouterDemo
和ZIKRouterDemo-macOS
中是不一樣的,而在切換彈窗模塊時,登陸模塊中的代碼不須要作任何改變。
通常來講,並不須要當即把全部的 protocol 都分離爲required protocol
和provided protocol
。調用模塊和目的模塊能夠暫時共用 protocol,或者只是簡單地改個名字,讓required protocol
做爲provided protocol
的子集,在第一次須要替換模塊的時候再用 category、extension、proxy、subclass 等技術進行接口適配。
接口適配也不能濫用,由於成本比較高,並且並不是全部的接口都能適配,例如同步接口和異步接口就難以適配。
對於模塊間耦合的處理,有這麼幾條建議:
required protocol
做爲provided protocol
的子集,接口名保持一致經過required protocol
和provided protocol
,咱們就實現了模塊間的徹底解耦。
模塊間通訊有多種方式,解耦程度也各有不一樣。這裏只討論接口交互的方式。
模塊的對外接口能夠分爲 input 和 output。二者的區別主要是控制流的主動權歸屬不一樣。
Input 是由外部主動調用的接口,控制流的發起者在外部,例如外部調用 view 的 UI 修改接口。
Output 是模塊內部主動調用外部實現的接口,控制流的發起者在內部,須要外部實現 output 所要求的方法。例如輸出 UI 事件、事件回調、獲取外部的 dataSource。iOS 中經常使用的 delegate 模式,也是一種 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 多是一個 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)
}
}
複製代碼
@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
複製代碼
子模塊:
protocol TextViewInput {
weak var output: TextViewModuleOutput? { get set }
var viewModel: TextViewModel { get }
}
class TextViewController: UIViewController, TextViewInput {
weak var output: TextViewModuleOutput?
var viewModel: TextViewModel!
}
複製代碼
@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
複製代碼
在使用 output 時,模塊適配會帶來必定麻煩。
例如這樣一對 required-provided protocol:
protocol RequiredEditorViewInput {
weak var output: RequiredEditorViewOutput? { get set }
}
protocol ProvidedEditorViewInput {
weak var output: ProvidedEditorViewOutput? { get set }
}
複製代碼
@protocol RequiredEditorViewInput <NSObject>
@property (nonatomic, weak) id<RequiredEditorViewOutput> output;
@end
@protocol ProvidedEditorViewInput <NSObject>
@property (nonatomic, weak) id<ProvidedEditorViewOutput> output;
@end
複製代碼
因爲 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
}
}
複製代碼
@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
複製代碼
適配代碼:
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
}
複製代碼
@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
複製代碼
能夠看到,output 的適配有些繁瑣。所以除非你的模塊是通用模塊,有實際的解耦需求,不然直接使用 provided protocol 便可。
總結完使用接口進行模塊解耦和依賴管理的方法,咱們能夠進一步對 router 進行擴展了。上面使用 makeDestination
建立模塊是最基本的功能,使用 router 子類後,咱們能夠進行許多有用的功能擴展,這裏給出一些示範。
編寫 router 代碼時,須要註冊 router 和 protocol 。在 OC 中能夠在 +load 方法中註冊,可是 Swift 裏已經不能使用 +load 方法,並且分散在 +load 中的註冊代碼也很差管理。BeeHive 中經過宏定義和__attribute((used, section("__DATA,""BeehiveServices""")))
,把註冊信息添加到了 mach-O 中的自定義區域,而後在啓動時讀取並自動註冊,惋惜這種方式在 Swift 中也沒法使用了。
咱們能夠把註冊代碼寫在 router 的+registerRoutableDestination
方法裏,而後逐個調用每一個 router 類的+registerRoutableDestination
方法便可。還能夠更進一步,用 runtime 技術遍歷 mach-O 中的__DATA,__objc_classlist
區域的類列表,獲取全部的 router 類,自動調用全部的+registerRoutableDestination
方法。
把註冊代碼統一管理以後,若是不想使用自動註冊,也能隨時切換爲手動註冊。
// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView<EditorViewProtocol>())
}
}
複製代碼
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}
@end
複製代碼
iOS 中模塊間耦合的緣由之一,就是界面跳轉的邏輯是經過 UIViewController 進行的,跳轉功能被限制在了 view controller 上,致使數據流經常都繞不開 view 層。要想更好地管理跳轉邏輯,就須要進行封裝。
封裝界面跳轉能夠屏蔽 UIKit 的細節,此時界面跳轉的代碼就能夠放在非 view 層(例如 presenter、view model、interactor、service),而且可以跨平臺,也能輕易地經過配置切換跳轉方式。
若是是普通的模塊,就用ZIKServiceRouter
,而若是是界面模塊,例如 UIViewController
和 UIView
,就能夠用ZIKViewRouter
,在其中封裝了界面跳轉功能。
封裝界面跳轉後,使用方式以下:
class TestViewController: UIViewController {
//直接跳轉到 editor 界面
func showEditor() {
Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}
//跳轉到 editor 界面,跳轉前用 protocol 配置界面
func prepareAndShowEditor() {
Router.perform(
to: RoutableView<EditorViewProtocol>(),
path: .push(from: self),
preparation: { destination in
// 跳轉前進行配置
// destination 自動推斷爲 EditorViewProtocol
})
}
}
複製代碼
@implementation TestViewController
- (void)showEditor {
//直接跳轉到 editor 界面
[ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
- (void)prepareAndShowEditor {
//跳轉到 editor 界面,跳轉前用 protocol 配置界面
[ZIKRouterToView(EditorViewProtocol)
performPath:ZIKViewRoutePath.pushFrom(self)
preparation:^(id<EditorViewProtocol> destination) {
// 跳轉前進行配置
// destination 自動推斷爲 EditorViewProtocol
}];
}
@end
複製代碼
能夠用 ViewRoutePath
一鍵切換不一樣的跳轉方式:
enum ViewRoutePath {
case push(from: UIViewController)
case presentModally(from: UIViewController)
case presentAsPopover(from: UIViewController, configure: ZIKViewRoutePopoverConfigure)
case performSegue(from: UIViewController, identifier: String, sender: Any?)
case show(from: UIViewController)
case showDetail(from: UIViewController)
case addAsChildViewController(from: UIViewController, addingChildViewHandler: (UIViewController, @escaping () -> Void) -> Void)
case addAsSubview(from: UIView)
case custom(from: ZIKViewRouteSource?)
case makeDestination
case extensible(path: ZIKViewRoutePath)
}
複製代碼
並且在界面跳轉後,還能夠根據跳轉時的跳轉方式,一鍵回退界面,無需再手動區分 dismiss、pop 等各類狀況:
class TestViewController: UIViewController {
var router: DestinationViewRouter<EditorViewProtocol>?
func showEditor() {
// 持有 router
router = Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}
// Router 會對 editor view controller 執行 pop 操做,移除界面
func removeEditor() {
guard let router = router, router.canRemove else {
return
}
router.removeRoute()
router = nil
}
}
複製代碼
@interface TestViewController()
@property (nonatomic, strong) ZIKDestinationViewRouter(id<EditorViewProtocol>) *router;
@end
@implementation TestViewController
- (void)showEditor {
// 持有 router
self.router = [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
// Router 會對 editor view controller 執行 pop 操做,移除界面
- (void)removeEditor {
if (![self.router canRemove]) {
return;
}
[self.router removeRoute];
self.router = nil;
}
@end
複製代碼
有些界面的跳轉方式很特殊,例如 tabbar 上的界面,須要經過切換 tabbar item 來進行。也有的界面有自定義的跳轉動畫,此時能夠在 router 子類中重寫對應方法,進行自定義跳轉。
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override func destination(with configuration: ViewRouteConfig) -> Any? {
return EditorViewController()
}
override func canPerformCustomRoute() -> Bool {
return true
}
override func performCustomRoute(onDestination destination: EditorViewController, fromSource source: Any?, configuration: ViewRouteConfig) {
beginPerformRoute()
// 自定義跳轉
CustomAnimator.transition(from: source, to: destination) {
self.endPerformRouteWithSuccess()
}
}
override func canRemoveCustomRoute() -> Bool {
return true
}
override func removeCustomRoute(onDestination destination: EditorViewController, fromSource source: Any?, removeConfiguration: ViewRemoveConfig, configuration: ViewRouteConfig) {
beginRemoveRoute(fromSource: source)
// 移除自定義跳轉
CustomAnimator.dismiss(destination) {
self.endRemoveRouteWithSuccess(onDestination: destination, fromSource: source)
}
}
override class func supportedRouteTypes() -> ZIKViewRouteTypeMask {
return [.custom, .viewControllerDefault]
}
}
複製代碼
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return [[EditorViewController alloc] init];
}
- (BOOL)canPerformCustomRoute {
return YES;
}
- (void)performCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source configuration:(ZIKViewRouteConfiguration *)configuration {
[self beginPerformRoute];
// 自定義跳轉
[CustomAnimator transitionFrom:source to:destination completion:^{
[self endPerformRouteWithSuccess];
}];
}
- (BOOL)canRemoveCustomRoute {
return YES;
}
- (void)removeCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source removeConfiguration:(ZIKViewRemoveConfiguration *)removeConfiguration configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
[self beginRemoveRouteFromSource:source];
// 移除自定義跳轉
[CustomAnimator dismiss:destination completion:^{
[self endRemoveRouteWithSuccessOnDestination:destination fromSource:source];
}];
}
+ (ZIKViewRouteTypeMask)supportedRouteTypes {
return ZIKViewRouteTypeMaskCustom|ZIKViewRouteTypeMaskViewControllerDefault;
}
@end
複製代碼
不少項目使用了 storyboard,在進行模塊化時,確定不能要求全部使用 storyboard 的模塊都改成使用代碼。所以咱們能夠 hook 一些 storyboard 相關的方法,例如-prepareSegue:sender:
,在其中調用prepareDestination:configuring:
便可。
雖然以前列出了 URL 路由的許多缺點,可是若是你的模塊須要從 h5 界面調用,例如電商 app 須要實現跨平臺的動態路由規則,那麼 URL 路由就是最佳的方案。
可是咱們並不想爲了實現 URL 路由,使用另外一套框架再從新封裝一次模塊。只須要在 router 上擴展 URL 路由的功能,便可同時用接口和 URL 管理模塊。
你能夠給 router 註冊 url:
class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ViewRouteConfig> {
override class func registerRoutableDestination() {
// 註冊 url
registerURLPattern("app://editor/:title")
}
}
複製代碼
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
// 註冊 url
[self registerURLPattern:@"app://editor/:title"];
}
@end
複製代碼
以後就能夠用相應的 url 獲取 router:
ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
複製代碼
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
複製代碼
以及處理 URL Scheme:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let urlString = url.absoluteString
if let _ = ZIKAnyViewRouter.performURL(urlString, fromSource: self.rootViewController) {
return true
} else if let _ = ZIKAnyServiceRouter.performURL(urlString) {
return true
}
return false
}
複製代碼
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
return YES;
} else if ([ZIKAnyServiceRouter performURL:urlString]) {
return YES;
}
return NO;
}
複製代碼
每一個 router 子類還能各自對 url 進行進一步處理,例如處理 url 中的參數、經過 url 執行對應方法、執行路由後發送返回值給調用者等。
每一個項目對 URL 路由的需求都不同,基於 ZIKRouter 強大的可擴展性,你也能夠按照項目需求實現本身的 URL 路由規則。
除了建立 router 子類,也可使用通用的 router 實例對象,在每一個對象的 block 屬性中提供和 router 子類同樣的功能,所以沒必要擔憂類過多的問題。原理就和用泛型 configuration 代替 configuration 子類同樣。
ZIKViewRoute 對象經過 block 屬性實現子類重寫的效果,代碼能夠用鏈式調用:
ZIKViewRoute<EditorViewController, ViewRouteConfig>
.make(withDestination: EditorViewController.self, makeDestination: ({ (config, router) -> EditorViewController? in
return EditorViewController()
}))
.prepareDestination({ (destination, config, router) in
}).didFinishPrepareDestination({ (destination, config, router) in
})
.register(RoutableView<EditorViewProtocol>())
複製代碼
[ZIKDestinationViewRoute(id<EditorViewProtocol>)
makeRouteWithDestination:[ZIKInfoViewController class]
makeDestination:^id<EditorViewProtocol> _Nullable(ZIKViewRouteConfig *config, ZIKRouter *router) {
return [[EditorViewController alloc] init];
}]
.prepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.didFinishPrepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));
複製代碼
基於 ZIKViewRoute 對象實現的 router,能夠進一步簡化 router 的實現代碼。
若是你的類很簡單,並不須要用到 router 子類,直接一行代碼註冊類便可:
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), forMakingView: EditorViewController.self)
複製代碼
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];
複製代碼
或者用 block 自定義建立對象的方式:
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),
forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
return EditorViewController()
}
複製代碼
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
return [[EditorViewController alloc] init];
}];
複製代碼
或者指定用 C 函數建立對象:
function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
return EditorViewController()
}
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),
forMakingView: EditorViewController.self, making: makeEditorViewController)
複製代碼
id<EditorViewController> makeEditorViewController(ZIKViewRouteConfiguration *config) {
return [[EditorViewController alloc] init];
}
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
factory:makeEditorViewController];
複製代碼
有時候模塊須要處理一些系統事件或者 app 的自定義事件,此時可讓 router 子類實現,再進行遍歷分發。
class SomeServiceRouter: ZIKServiceRouter {
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
複製代碼
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
Router.enumerateAllViewRouters { (routerType) in
if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
}
}
Router.enumerateAllServiceRouters { (routerType) in
if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
}
}
}
}
複製代碼
@interface SomeServiceRouter : ZIKServiceRouter
@end
@implementation SomeServiceRouter
+ (void)applicationDidEnterBackground:(UIApplication *)application {
// handle applicationDidEnterBackground event
}
@end
複製代碼
@interface AppDelegate ()
@end
@implementation AppDelegate
- (void)applicationDidEnterBackground:(UIApplication *)application {
[ZIKAnyViewRouter enumerateAllViewRouters:^(Class routerClass) {
if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
[routerClass applicationDidEnterBackground:application];
}
}];
[ZIKAnyServiceRouter enumerateAllServiceRouters:^(Class routerClass) {
if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
[routerClass applicationDidEnterBackground:application];
}
}];
}
@end
複製代碼
藉助於使用接口管理依賴的方案,咱們在對模塊進行單元測試時,能夠自由配置 mock 依賴,並且無需 hook 模塊內部的代碼。
例如這樣一個依賴於網絡模塊的登錄模塊:
// 登陸模塊
class LoginService {
func login(account: String, password: String, completion: (Result<LoginError>) -> Void) {
// 內部使用 RequiredNetServiceInput 進行網絡訪問
let netService = Router.makeDestination(to: RoutableService<RequiredNetServiceInput
>())
let request = makeLoginRequest(account: account, password: password)
netService?.POST(request: request, completion: completion)
}
}
// 聲明依賴
extension RoutableService where Protocol == RequiredNetServiceInput {
init() {}
}
複製代碼
// 登陸模塊
@interface LoginService : NSObject
@end
@implementation LoginService
- (void)loginWithAccount:(NSString *)account password:(NSString *)password completion:(void(^)(Result *result))completion {
// 內部使用 RequiredNetServiceInput 進行網絡訪問
id<RequiredNetServiceInput> netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination];
Request *request = makeLoginRequest(account, password);
[netService POSTRequest:request completion: completion];
}
@end
// 聲明依賴
@protocol RequiredNetServiceInput <ZIKServiceRoutable>
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion;
@end
複製代碼
在編寫單元測試時,不須要引入真實的網絡模塊,能夠提供一個自定義的 mock 網絡模塊:
class MockNetService: RequiredNetServiceInput {
func POST(request: Request, completion: (Result<NetError>) {
completion(.success)
}
}
複製代碼
// 註冊 mock 依賴
ZIKAnyServiceRouter.register(RoutableService<RequiredNetServiceInput>(),
forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? in
return MockNetService()
}
複製代碼
@interface MockNetService : NSObject <RequiredNetServiceInput>
@end
@implementation MockNetService
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
completion([Result success]);
}
@end
複製代碼
// 註冊 mock 依賴
[ZIKServiceRouter registerServiceProtocol:ZIKRoutable(EditorViewInput) forMakingService:[MockNetService class]];
複製代碼
對於那些沒有接口交互的外部依賴,例如只是簡單的跳轉到對應界面,則只需註冊一個空白的 proxy。
單元測試代碼:
class LoginServiceTests: XCTestCase {
func testLoginSuccess() {
let expectation = expectation(description: "end login")
let loginService = LoginService()
loginService.login(account: "account", password: "pwd") { result in
expectation.fulfill()
}
waitForExpectations(timeout: 5, handler: { if let error = $0 {print(error)}})
}
}
複製代碼
@interface LoginServiceTests : XCTestCase
@end
@implementation LoginServiceTests
- (void)testLoginSuccess {
XCTestExpectation *expectation = [self expectationWithDescription:@"end login"];
[[LoginService new] loginWithAccount:@"" password:@"" completion:^(Result *result) {
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
!error? : NSLog(@"%@", error);
}];
}
@end
複製代碼
使用接口管理依賴,能夠更容易 mock,剝除外部依賴對測試的影響,讓單元測試更穩定。
使用接口管理模塊時,還有一個問題須要注意。接口是會隨着模塊更新而變化的,這個接口已經被不少外部使用了,要如何減小接口變化產生的影響?
此時須要區分新接口和舊接口,區分版本,推出新接口的同時,保留舊接口,並將舊接口標記爲廢棄。這樣使用者就能夠暫時使用舊接口,漸進式地修改代碼。
這部分能夠參考 Swift 和 OC 中的版本管理宏。
接口廢棄,能夠暫時使用,建議儘快使用新接口代替:
// Swift
@available(iOS, deprecated: 8.0, message: "Use new interface instead")
複製代碼
// Objective-C
API_DEPRECATED_WITH_REPLACEMENT("performPath:configuring:", ios(7.0, 7.0));
複製代碼
接口已經無效:
// Swift
@available(iOS, unavailable)
複製代碼
// Objective-C
NS_UNAVAILABLE
複製代碼
最後,一個 router 的最終形態就是下面這樣:
// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView<EditorViewProtocol>())
registerURLPattern("app://editor/:title")
}
override func processUserInfo(_ userInfo: [AnyHashable : Any] = [:], from url: URL) {
let title = userInfo["title"]
// 處理 url 中的參數
}
// 子類重寫,建立模塊
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}
// 配置模塊,注入靜態依賴
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依賴
destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
// 其餘配置
// 處理來自 url 的參數
if let title = configuration.userInfo["title"] as? String {
destination.title = title
} else {
destination.title = "默認標題"
}
}
// 事件處理
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
複製代碼
// editor 模塊的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
[self registerURLPattern:@"app://editor/:title"];
}
- (void)processUserInfo:(NSDictionary *)userInfo fromURL:(NSURL *)url {
NSString *title = userInfo[@"title"];
// 處理 url 中的參數
}
// 子類重寫,建立模塊
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
// 配置模塊,注入靜態依賴
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// 注入 service 依賴
destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
// 其餘配置
// 處理來自 url 的參數
NSString *title = configuration.userInfo[@"title"];
if (title) {
destination.title = title;
} else {
destination.title = @"默認標題";
}
}
// 事件處理
+ (void)applicationDidEnterBackground:(UIApplication *)application {
// handle applicationDidEnterBackground event
}
@end
複製代碼
咱們能夠看到基於接口管理模塊的優點:
回過頭看以前的 8 個解耦指標,ZIKRouter 已經徹底知足。而 router 提供的多種模塊管理方式(makeDestination、prepareDestination、依賴注入、頁面跳轉、storyboard 支持),可以覆蓋大多數現有的場景,從而實現漸進式的模塊化,減輕重構現有代碼的成本。