打造完備的iOS組件化方案:如何面向接口進行模塊解耦?

關於組件化的探討已經有很多了,在以前的文章iOS VIPER架構實踐(三):面向接口的路由設計中,綜合比較了各類方案後,我傾向於使用面向接口的方式進行組件化。html

這是一篇從代碼層面講解模塊解耦的文章,會全方位地展現如何實踐面向接口的思想,儘可能全面地探討在模塊管理和解耦的過程當中,須要考慮到的各類問題,而且給出實際的解決方案,以及對應的模塊管理開源工具:ZIKRouter。你也能夠根據本文的內容改造本身現有的方案,即便你的項目不進行組件化,也能夠參考本文進行代碼解耦。ios

文章主要內容:git

  • 如何衡量模塊解耦的程度
  • 對比不一樣方案的優劣
  • 在編譯時進行靜態路由檢查,避免使用不存在的模塊
  • 如何進行模塊解耦,包括模塊重用、模塊適配、模塊間通訊、子模塊交互
  • 模塊的接口和依賴管理
  • 管理界面跳轉邏輯

目錄

  • 什麼是組件化
  • 爲何要組件化
  • 你的項目是否須要組件化
  • 組件化方案的8條指標
  • 方案對比
    • URL 路由
    • Target-Action 方案
    • 基於 protocol 匹配的方案
  • Protocol-Router 匹配方案
  • 動態化的風險
  • 靜態路由檢查
  • 模塊解耦
    • 模塊分類
    • 什麼是解耦
    • 模塊重用
  • 依賴管理
    • 依賴注入
    • 分離模塊建立和配置
    • 可選依賴:屬性注入和方法注入
    • 必需依賴:工廠方法
    • 避免接口污染
    • 依賴查找
    • 循環依賴
  • 模塊適配器
    • required protocol 和 provided protocol
  • 模塊間通訊
    • 控制流 input 和 output
    • 設置 input 和 output
    • 子模塊
    • Output 的適配
  • 功能擴展
    • 自動註冊
    • 封裝界面跳轉
    • 自定義跳轉
    • 支持 storyboard
    • URL 路由
    • 用 router 對象代替 router 子類
    • 簡化 router 實現
    • 事件處理
  • 單元測試
  • 接口版本管理
  • 最終形態
  • 基於接口進行解耦的優點

什麼是組件化

將模塊單獨抽離、分層,並制定模塊間通訊的方式,從而實現解耦,以及適應團隊開發。github

爲何須要組件化

主要有4個緣由:objective-c

  • 模塊間解耦
  • 模塊重用
  • 提升團隊協做開發效率
  • 單元測試

當項目愈來愈大的時候,各個模塊之間若是是直接互相引用,就會產生許多耦合,致使接口濫用,當某天須要進行修改時,就會牽一髮而動全身,難以維護。數據庫

問題主要體如今:編程

  • 修改某個模塊的功能時,須要修改許多其餘模塊的代碼,由於這個模塊被其餘模塊引用
  • 模塊對外的接口不明確,外部甚至會調用不該暴露的私有接口,修改時會耗費大量時間
  • 修改的模塊涉及範圍較廣,很容易影響其餘團隊成員的開發,產生代碼衝突
  • 當須要抽離模塊到其餘地方重用時,會發現耦合致使根本沒法單獨抽離
  • 模塊間的耦合致使接口和依賴混亂,難以編寫單元測試

因此須要減小模塊之間的耦合,用更規範的方式進行模塊間交互。這就是組件化,也能夠叫作模塊化。swift

你的項目是否須要組件化

組件化也不是必須的,有些狀況下並不須要組件化:設計模式

  • 項目較小,模塊間交互簡單,耦合少
  • 模塊沒有被多個外部模塊引用,只是一個單獨的小模塊
  • 模塊不須要重用,代碼也不多被修改
  • 團隊規模很小
  • 不須要編寫單元測試

組件化也是有必定成本的,你須要花時間設計接口,分離代碼,因此並非全部的模塊都須要組件化。api

不過,當你發現這幾個跡象時,就須要考慮組件化了:

  • 模塊邏輯複雜,多個模塊間頻繁互相引用
  • 項目規模逐漸變大,修改代碼變得愈來愈困難
  • 團隊人數變多,提交的代碼常常和其餘成員衝突
  • 項目編譯耗時較大
  • 模塊的單元測試常常因爲其餘模塊的修改而失敗

組件化方案的8條指標

決定了要開始組件化之路後,就須要思考咱們的目標了。一個組件化方案須要達到怎樣的效果呢?我在這裏給出8個理想狀況下的指標:

  1. 模塊間沒有直接耦合,一個模塊內部的修改不會影響到另外一個模塊
  2. 模塊能夠被單獨編譯
  3. 模塊間可以清晰地進行數據傳遞
  4. 模塊能夠隨時被另外一個提供了相同功能的模塊替換
  5. 模塊的對外接口容易查找和維護
  6. 當模塊的接口改變時,使用此模塊的外部代碼可以被高效地重構
  7. 儘可能用最少的修改和代碼,讓現有的項目實現模塊化
  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個"支持模塊單獨編譯",另外在和接口相關的第三、五、6點上,比 URL 路由要有改善。

表明框架

CTMediator

改進:避免字典傳參

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

Swinject

改進:離散式管理

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
    }
}
複製代碼
Objective-C Sample
@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
    }
}
複製代碼
Objective-C Sample
// 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>())
複製代碼
Objective-C Sample
// 註冊 protocol 和 router
[EditorViewRouter registerViewProtocol:@protocol(EditorViewProtocol)];
複製代碼

而後就能夠用 protocol 獲取 router 類,再進一步獲取模塊:

// 獲取模塊的 router 類
let routerClass = Router.to(RoutableView<EditorViewProtocol>())
// 獲取 EditorViewProtocol 模塊
let destination = routerClass?.makeDestination()
複製代碼
Objective-C Sample
// 獲取模塊的 router 類
Class routerClass = ZIKViewRouter.toView(@protocol(EditorViewProtocol));
// 獲取 EditorViewProtocol 模塊
id<EditorViewProtocol> destination = [routerClass makeDestination];
複製代碼

加了一層 router 中間層以後,解耦能力一會兒就加強了:

  • 能夠在 router 上添加許多通用的擴展接口,例如建立模塊、依賴注入、界面跳轉、界面移除,甚至增長 URL 路由支持
  • 在每一個 router 子類中能夠進行更詳細的依賴注入和自定義操做
  • 能夠自定義建立對象的方式,例如自定義初始化方法、工廠方法,在重構時能夠直接搬運現有的建立代碼,無需在原來的類上增長或修改接口,減小模塊化過程當中的工做量
  • 可讓多個 protocol 和同一個模塊進行匹配
  • 可讓模塊進行接口適配,容許外部作完適配後,爲 router 添加新的 protocol,解決編譯依賴的問題
  • 返回的對象只需符合 protocol,再也不和某個單一的類綁定。所以能夠根據條件,返回不一樣的對象,例如適配不一樣系統版本時,返回不一樣的控件,讓外部只關注接口

動態化的風險

大部分組件化方案都會帶來一個問題,就是減弱甚至拋棄編譯檢查,由於模塊已經變得高度動態化了。

當調用一個模塊時,怎麼能保證這個模塊必定存在?直接引用類時,若是類不存在,編譯器會給出引用錯誤,可是動態組件就沒法在靜態時檢查了。

例如 URL 地址變化了,可是代碼中的某些 URL 沒有及時更新;使用 protocol 獲取模塊時,protocol 並無註冊對應的模塊。這些問題都只能在運行時才能發現。

那麼有沒有一種方式,可讓模塊既高度解耦,又能在編譯時保證調用的模塊必定存在呢?

答案是 YES。

靜態路由檢查

ZIKRouter 最特別的功能,就是可以保證所使用的 protocol 必定存在,在編譯階段就能防止使用不存在的模塊。這個功能可讓你更安全、更簡單地管理所使用的路由接口,沒必要再用其餘複雜的方式進行檢查和維護。

當使用了錯誤的 protocol 時,會產生編譯錯誤。

Swift 中使用未聲明的 protocol:

Swift路由檢查

Objective-C 中使用未聲明的 protocol:

OC路由檢查

這個特性經過兩個機制來實現:

  • 只有被聲明爲可路由的 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)
複製代碼
Objective-C Sample
// 註冊 protocol 和 router
[EditorViewRouter registerView:[EditorViewController class]];
複製代碼

從而進一步檢查 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
複製代碼

這裏使用了兩個泛型參數 DestinationRouteConfig,分別表示此 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層:

  1. 模塊間使用抽象接口交互,沒有直接類型耦合,一個模塊內部的修改不會影響到另外一個模塊 (單一職責、依賴倒置)
  2. 模塊可重用,能夠被單獨編譯 (接口隔離、依賴倒置、控制反轉)
  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 = "默認標題"
    }
}
複製代碼
Objective-C Sample
// 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
    
})
複製代碼
Objective-C Sample
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)
    }
}
複製代碼
Objective-C示例
@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
    }
}
複製代碼
Objective-C示例
@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)
    }
})
複製代碼
Objective-C示例
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)
        })
複製代碼
Objective-C示例
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
    }
}
複製代碼
Objective-C示例
@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?
}
複製代碼
Objective-C示例
@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?
}
複製代碼
Objective-C示例
// 通用 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
    }
}
複製代碼
Objective-C示例
@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>())
複製代碼
Objective-C示例
[PersonRouter registerModuleProtocol:ZIKRoutable(PersonTypeFactory)];
複製代碼

就能夠用PersonTypeFactory獲取模塊了:

let name: String = ...
Router.makeDestination(to: RoutableServiceModule<PersonTypeFactory>(), configuring: { (config, _) in
    // config 遵照 PersonTypeFactory
    config.makeDestinationWith(name)
})
複製代碼
Objective-C示例
NSString *name = ...
ZIKRouterToServiceModule(PersonTypeFactory) makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration<PersonTypeFactory> *config) {
    // config 遵照 PersonTypeFactory
    [config makeDestinationWith:name];
}]
複製代碼

用泛型代替 configuration 子類

若是你不須要在 configuration 上保存其餘自定義參數,也不想建立過多的 configuration 子類,能夠用一個通用的泛型類來實現子類重寫的效果。

泛型能夠自定義參數類型,此時能夠直接把工廠方法用 block 保存在 configuration 的屬性上。

// 通用 configuration,能夠提供自定義工廠方法
class ServiceMakeableConfiguration<Destination, Constructor>: PerformRouteConfig {    
    public var makeDestinationWith: Constructor
    public var makedDestination: Destination?
}
複製代碼
Objective-C示例
@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 {
    
}
複製代碼
Objective-C示例
@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?
}
複製代碼
Objective-C示例
@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
    }
}
複製代碼
Objective-C示例
@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)
})
複製代碼
Objective-C示例
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>())!
    }
}
複製代碼
Objective-C示例
@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!
}
複製代碼
Objective-C示例
@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
    }
}
複製代碼
Objective-C示例
@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 和同一個模塊交互。

required protocol 和 provided protocol

你能夠爲同一個 router 註冊多個 protocol。

根據依賴關係,接口能夠分爲required protocolprovided protocol。模塊自己提供的接口是provided protocol,模塊的調用者須要使用的接口是required protocol

required protocolprovided protocol的子集,調用者只須要聲明本身用到的那些接口,沒必要引入整個provided protocol,這樣可讓模塊間的耦合進一步減小。

在 UML 的組件圖中,就很明確地表現出了這二者的概念。下圖中的半圓就是Required Interface,框外的圓圈就是Provided Interface

組件圖

那麼如何實施Required InterfaceProvided 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 = "請登陸"
})
複製代碼
Objective-C示例
// 調用者中聲明的依賴接口,代表自身依賴一個登錄界面
@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 }
}
複製代碼
Objective-C示例
// 實際登錄界面提供的接口
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
複製代碼

適配的代碼由宿主 app 實現,讓登錄界面支持 RequiredLoginViewInput

// 讓模塊支持 required protocol,只須要添加一個 protocol 擴展便可
extension LoginViewController: RequiredLoginViewInput {
}
複製代碼
Objective-C示例 ```objectivec // 讓模塊支持 required protocol,只須要添加一個 protocol 擴展便可 @interface LoginViewController (ModuleAAdapter) @end @implementation LoginViewController (ModuleAAdapter) @end ```

而且讓登錄界面的 router 也支持 RequiredLoginViewInput

// 若是能夠獲取到 router 類,能夠直接爲 router 添加 RequiredLoginViewInput
LoginViewRouter.register(RoutableView<RequiredLoginViewInput>())
// 若是不能獲得對應模塊的 router,能夠用 adapter 進行轉發
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
複製代碼
Objective-C示例 ```objectivec //若是能夠獲取到 router 類,能夠直接爲 router 添加 RequiredLoginViewInput [LoginViewRouter registerViewProtocol:ZIKRoutable(RequiredLoginViewInput)]; //若是不能獲得對應模塊的 router,能夠註冊 adapter [self registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)]; ```

適配以後,RequiredLoginViewInput就能和ProvidedLoginViewInput同樣使用,獲取到同一個模塊了:

調用者模塊示例:

Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
    destination.message = "請登陸"
})

// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
Router.makeDestination(to: RoutableView<ProvidedLoginViewInput>(), preparation: {
    destination.message = "請登陸"
})
複製代碼
Objective-C示例
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
    destination.message = @"請登陸";
}];

// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<ProvidedLoginViewInput> destination) {
    destination.message = @"請登陸";
}];
複製代碼

接口適配

有時候ProvidedLoginViewInputRequiredLoginViewInput的接口名可能會稍有不一樣,此時須要用 category、extension、子類、proxy 類等方式進行接口適配。

protocol ProvidedLoginViewInput {
   var notifyString: String? { get set } // 接口名不一樣
}
複製代碼
Objective-C示例
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString; // 接口名不一樣
@end
複製代碼

適配時須要進行接口轉發,讓登錄界面支持 RequiredLoginViewInput

extension LoginViewController: RequiredLoginViewInput {
    var message: String? {
        get {
            return notifyString
        }
        set {
            notifyString = newValue
        }
    }
}
複製代碼
Objective-C示例
@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 }
}
複製代碼
Objective-C示例
@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 }
}
複製代碼
Objective-C示例
@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
   }
}

複製代碼
Objective-C示例
@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
複製代碼
Objective-C示例
#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()
    ...
}
複製代碼
Objective-C示例
void registerAdapters() {
    // 註冊默認的依賴
    registerDefaultDependency();
    ...
}

// 使用默認的適配代碼
ADAPT_DEFAULT_DEPENDENCY
複製代碼

若是宿主 app 須要替換使用另外一個 provided 模塊,能夠關閉宏定義,再寫一份另外的適配代碼,便可替換依賴。

模塊化

區分了required protocolprovided protocol後,就能夠實現真正的模塊化。在調用者聲明瞭所須要的required protocol後,被調用模塊就能夠隨時被替換成另外一個相同功能的模塊。

參考 demo 中的ZIKLoginModule示例模塊,登陸模塊依賴於一個彈窗模塊,而這個彈窗模塊在ZIKRouterDemoZIKRouterDemo-macOS中是不一樣的,而在切換彈窗模塊時,登陸模塊中的代碼不須要作任何改變。

使用 adapter 的規範

通常來講,並不須要當即把全部的 protocol 都分離爲required protocolprovided protocol。調用模塊和目的模塊能夠暫時共用 protocol,或者只是簡單地改個名字,讓required protocol做爲provided protocol的子集,在第一次須要替換模塊的時候再用 category、extension、proxy、subclass 等技術進行接口適配。

接口適配也不能濫用,由於成本比較高,並且並不是全部的接口都能適配,例如同步接口和異步接口就難以適配。

對於模塊間耦合的處理,有這麼幾條建議:

  • 若是依賴的是提供特定功能的模塊,沒有通用性,直接引用類便可
  • 若是是依賴某些簡單的通用模塊(例如日誌模塊),能夠在模塊的接口上把依賴交給外部來設置,例如 block 的形式
  • 大部分須要解耦的模塊都是須要重用的業務模塊,若是你的模塊不須要重用,而且也不須要分工開發,直接引用對應類便可
  • 大部分狀況下建議共用 protocol,或者讓required protocol做爲provided protocol的子集,接口名保持一致
  • 只有在你的業務模塊的確容許使用者使用不一樣的依賴模塊時,才進行多個接口間的適配。例如須要跨平臺的模塊,例如登陸界面模塊容許不一樣的 app 使用不一樣的登錄 service 模塊

經過required protocolprovided 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)
    }
}
複製代碼
Objective-C Sample
@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!
}
複製代碼
Objective-C Sample
@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 的適配

在使用 output 時,模塊適配會帶來必定麻煩。

例如這樣一對 required-provided protocol:

protocol RequiredEditorViewInput {
    weak var output: RequiredEditorViewOutput? { get set }
}

protocol ProvidedEditorViewInput {
    weak var output: ProvidedEditorViewOutput? { get set }
}
複製代碼
Objective-C Sample
@protocol RequiredEditorViewInput <NSObject>
@property (nonatomic, weak) id<RequiredEditorViewOutput> output;
@end

@protocol ProvidedEditorViewInput <NSObject>
@property (nonatomic, weak) id<ProvidedEditorViewOutput> output;
@end
複製代碼

因爲 output 的實現者不是固定的,所以沒法讓全部的 output 類都同時適配RequiredEditorViewOutputProvidedEditorViewOutput。此時建議直接使用對應的 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
    }
}
複製代碼
Objective-C Sample
@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
}
複製代碼
Objective-C Sample
@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>())
    }

}
複製代碼
Objective-C Sample
@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,而若是是界面模塊,例如 UIViewControllerUIView,就能夠用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
            })
    }
}
複製代碼
Objective-C Sample
@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
    }
}
複製代碼
Objective-C Sample
@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]
    }
}
複製代碼
Objective-C Sample
@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,在進行模塊化時,確定不能要求全部使用 storyboard 的模塊都改成使用代碼。所以咱們能夠 hook 一些 storyboard 相關的方法,例如-prepareSegue:sender:,在其中調用prepareDestination:configuring:便可。

URL 路由

雖然以前列出了 URL 路由的許多缺點,可是若是你的模塊須要從 h5 界面調用,例如電商 app 須要實現跨平臺的動態路由規則,那麼 URL 路由就是最佳的方案。

可是咱們並不想爲了實現 URL 路由,使用另外一套框架再從新封裝一次模塊。只須要在 router 上擴展 URL 路由的功能,便可同時用接口和 URL 管理模塊。

你能夠給 router 註冊 url:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ViewRouteConfig> {
    override class func registerRoutableDestination() {
        // 註冊 url
        registerURLPattern("app://editor/:title")
    }
}
複製代碼
Objective-C Sample
@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    // 註冊 url
    [self registerURLPattern:@"app://editor/:title"];
}

@end
複製代碼

以後就能夠用相應的 url 獲取 router:

ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
複製代碼
Objective-C Sample
[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
}
複製代碼
Objective-C Sample
- (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 子類

除了建立 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>())
複製代碼
Objective-C Sample
[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));
複製代碼

簡化 router 實現

基於 ZIKViewRoute 對象實現的 router,能夠進一步簡化 router 的實現代碼。

若是你的類很簡單,並不須要用到 router 子類,直接一行代碼註冊類便可:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), forMakingView: EditorViewController.self)
複製代碼
Objective-C Sample
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];
複製代碼

或者用 block 自定義建立對象的方式:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), 
                 forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
                     return EditorViewController()
        }

複製代碼
Objective-C Sample
[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)
複製代碼
Objective-C Sample
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)
            }
        }
    }

}
複製代碼
Objective-C Sample
@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() {}
}
複製代碼
Objective-C Sample
// 登陸模塊
@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()
        }
複製代碼
Objective-C Sample
@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)}})
    }
    
}
複製代碼
Objective-C Sample
@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
    }
}
複製代碼
Objective-C Sample
// 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
複製代碼

基於接口進行解耦的優點

咱們能夠看到基於接口管理模塊的優點:

  • 依賴編譯檢查,實現嚴格的類型安全
  • 依賴編譯檢查,減小重構時的成本
  • 經過接口明確聲明模塊所需的依賴,容許外部進行依賴注入
  • 保持動態特性的同時,進行路由檢查,避免使用不存在的路由模塊
  • 利用接口,區分 required protocol 和 provided protocol,進行明確的模塊適配,實現完全解耦

回過頭看以前的 8 個解耦指標,ZIKRouter 已經徹底知足。而 router 提供的多種模塊管理方式(makeDestination、prepareDestination、依賴注入、頁面跳轉、storyboard 支持),可以覆蓋大多數現有的場景,從而實現漸進式的模塊化,減輕重構現有代碼的成本。

相關文章
相關標籤/搜索