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

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

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

這是一篇從代碼層面講解模塊解耦的文章,會全方位地展示如何實踐面向接口的思想,儘量全面地探討在模塊管理和解耦的過程中,需要考慮到的各種問題,並且給出實際的解決方案,以及對應的模塊管理開源工具:ZIKRouter。你也可以根據本文的內容改造自己現有的方案。

文章主要內容:

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

目錄

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

什麼是組件化

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

爲什麼需要組件化

主要有4個原因:

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

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

問題主要體現在:

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

所以需要減少模塊之間的耦合,用更規範的方式進行模塊間交互。這就是組件化,也可以叫做模塊化。

你的項目是否需要組件化

組件化也不是必須的,有些情況下並不需要組件化:

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

組件化也是有一定成本的,你需要花時間設計接口,分離代碼,所以並不是所有的模塊都需要組件化。

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

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

組件化方案的8條指標

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

  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個"支持模塊單獨編譯",另外在和接口相關的第3、5、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
    }
}

<details><summary>Objective-C Sample</summary>

@interface ZIKViewRouter: NSObject
@end
  
@implementation ZIKViewRouter
  
...
// 獲取模塊
+ (id)makeDestination {
    ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
    return [router destinationWithConfiguration:router.configuration];
}

// 讓子類重寫
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    return nil;
}
@end

</details>

每個模塊各自編寫自己的 router 子類:

// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter {
    // 子類重寫,創建模塊
    override func destination(with configuration: ViewRouteConfig) -> Any? {
        let destination = EditorViewController()
        return destination
    }
}

<details><summary>Objective-C Sample</summary>

// editor 模塊的 router
@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

// 子類重寫,創建模塊
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    EditorViewController *destination = [[EditorViewController alloc] init];
    return destination;
}

@end

</details>

把 protocol 和 router 類進行註冊綁定:

EditorViewRouter.register(RoutableView<EditorViewProtocol>())

<details><summary>Objective-C Sample</summary>

// 註冊 protocol 和 router
[EditorViewRouter registerViewProtocol:@protocol(EditorViewProtocol)];

</details>

然後就可以用 protocol 獲取 router 類,再進一步獲取模塊:

// 獲取模塊的 router 類
let routerClass = Router.to(RoutableView<EditorViewProtocol>())
// 獲取 EditorViewProtocol 模塊
let destination = routerClass?.makeDestination()

<details><summary>Objective-C Sample</summary>

// 獲取模塊的 router 類
Class routerClass = ZIKViewRouter.toView(@protocol(EditorViewProtocol));
// 獲取 EditorViewProtocol 模塊
id<EditorViewProtocol> destination = [routerClass makeDestination];

</details>

加了一層 router 中間層之後,解耦能力一下子就增強了:

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

動態化的風險

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

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

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

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

答案是 YES。

靜態路由檢查

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

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

Swift 中使用未聲明的 protocol:

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

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

  • 只有被聲明爲可路由的 protocol 才能用於路由,否則會產生編譯錯誤
  • 可路由的 protocol 必定有一個對應的模塊存在

下面就一步步講解,怎麼在保持動態解耦特性的同時,實現一套完備的靜態類型檢查的機制。

路由聲明

怎麼才能聲明一個 protocol 是可以用於路由的呢?

要實現第一個機制,關鍵就是要爲 protocol 添加特殊的屬性或者類型,使用時,如果 protocol 不符合特定類型,就產生編譯錯誤。

原生 Xcode 並不支持這樣的靜態檢查,這時候就要考驗我們的創造力了。

Objective-C:protocol 繼承鏈

在 Objective-C 中,可以要求 protocol 必須繼承自某個特定的父 protocol,並且通過宏定義 + protocol 限定,對 protocol 的父 protocol 繼承鏈進行靜態檢查。

例如 ZIKRouter 中獲取 router 類的方法是這樣的:

@protocol ZIKViewRoutable
@end

@interface ZIKViewRouter()
@property (nonatomic, class, readonly) ZIKViewRouterType *(^toView)(Protocol<ZIKViewRoutable> *viewProtocol);
@end

toView用類屬性的方式提供,以方便鏈式調用,這個 block 接收一個Protocol<ZIKViewRoutable> *類型的 protocol,返回對應的 router 類。

Protocol<ZIKViewRoutable> *表示這個 protocol 必須繼承自ZIKViewRoutable。普通 protocol 的類型是Protocol *,所以如果傳入@protocol(EditorViewProtocol)就會產生編譯警告。

而如果用宏定義再給 protocol 變量加上一個 protocol 限定,進行一次類型轉換,就可以利用編譯器檢查 protocol 的繼承鏈:

// 聲明時繼承自 ZIKViewRoutable
@protocol EditorViewProtocol <ZIKViewRoutable>
@end

// 宏定義,爲 protocol 變量添加 protocol 限定
#define ZIKRoutable(RoutableProtocol) (Protocol<RoutableProtocol>*)@protocol(RoutableProtocol)

// 用 protocol 獲取 router
ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))

ZIKRoutable(EditorViewProtocol)展開後是(Protocol<EditorViewProtocol> *)@protocol(EditorViewProtocol),類型爲Protocol<EditorViewProtocol> *。在 Objective-C 中Protocol<EditorViewProtocol> *Protocol<ZIKViewRoutable> *的子類型,編譯器將不會有警告。

但是當傳入的 protocol 沒有繼承自ZIKViewRoutable時,例如ZIKRoutable(UndeclaredProtocol)的類型是Protocol<UndeclaredProtocol> *,編譯器在檢查 protocol 的繼承鏈時,由於UndeclaredProtocol沒有繼承自ZIKViewRoutable,因此Protocol<UndeclaredProtocol> *不是Protocol<ZIKViewRoutable> *的子類型,編譯器會給出類型錯誤的警告。在Build Settings中可以把incompatible pointer types警告變成編譯錯誤。

最後,把ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))用宏定義簡化一下,變成ZIKViewRouterToView(EditorViewProtocol),就能在獲取 router 的時候方便地靜態檢查 protocol 的類型了。

Swift:條件擴展

Swift 中不支持宏定義,也不能隨意進行類型轉換,因此需要換一種方式來進行編譯檢查。

可以用 struct 的泛型傳遞 protocol,然後用條件擴展爲特定泛型的 struct 添加初始化方法,從而讓沒有聲明過的泛型類型不能直接創建 struct。

例如:

// 用 RoutableView 的泛型來傳遞 protocol
struct RoutableView<Protocol> {
    // 禁止默認的初始化方法
    @available(*, unavailable, message: "Protocol is not declared as routable")
    public init() { }
}

// 泛型爲 EditorViewProtocol 的擴展
extension RoutableView where Protocol == EditorViewProtocol {
    // 允許初始化
    init() { }
}

// 泛型爲 EditorViewProtocol 時可以初始化
RoutableView<EditorViewProtocol>()

// 沒有聲明過的泛型無法初始化,會產生編譯錯誤
RoutableView<UndeclaredProtocol>()

此時 Xcode 還可以給出自動補全,列出所有聲明過的 protocol:

路由檢查

通過路由聲明,我們做到了在編譯時對所使用的 protocol 做出限制。下一步就是保證聲明過的 protocol 必定有對應的模塊,類似於程序在 link 階段,會檢查頭文件中聲明過的類必定有對應的實現。

這一步是無法直接在編譯階段實現的,不過可以參考 iOS 在啓動時檢查動態庫的方式,我們可以在啓動階段實現這個功能。

Objective-C: protocol 遍歷

在 app 以 DEBUG 模式啓動時,我們可以遍歷所有繼承自 ZIKViewRoutable 的 protocol,在註冊表中檢查是否有對應的 router,如果沒有,就給出斷言錯誤。

另外,還可以讓 router 同時註冊創建模塊時用到類:

EditorViewRouter.registerView(EditorViewController.self)

<details><summary>Objective-C Sample</summary>

// 註冊 protocol 和 router
[EditorViewRouter registerView:[EditorViewController class]];

</details>

從而進一步檢查 router 中的 class 是否遵守對應的 protocol。這時整個類型檢查過程就完整了。

Swift: 符號遍歷

但是 Swift 中的 protocol 是靜態類型,並不能通過 OC runtime 直接遍歷。是不是就無法動態檢查了呢?其實只要發揮創造力,一樣能做到。

Swift 的泛型名會在符號名中體現出來。例如上面聲明的 init 方法:

// MyApp 中,泛型爲 EditorViewProtocol 的擴展
extension RoutableView where Protocol == EditorViewProtocol {
    // 允許初始化
    init() { }
}

在還原符號後就是(extension in MyApp):ZRouter.RoutableView<A where A == MyApp.EditorViewProtocol>.init() -> ZRouter.RoutableView<MyApp.EditorViewProtocol>

此時我們可以遍歷 app 的符號表,來查找 RoutableView 的所有擴展,從而提取出所有聲明過的 protocol 類型,再去檢查是否有對應的 router。

Swift Runtime 和 ABI

但是如果要進一步檢查 router 中的 class 是否遵守 router 中的 protocol,就會遇到問題了。在 Swift 中怎麼檢查某個任意的 class 遵守某個 Swift protocol ?

Swift 中沒有直接提供class_conformsToProtocol這樣的函數,不過我們可以通過 Swift Runtime 提供的標準函數和 Swift ABI 中定義的內存結構,完成同樣的功能。

這部分的實現可以參考代碼:_swift_typeIsTargetType。之後我會寫幾篇文章詳細講解 Swift ABI 的底層內容。

路由檢查這部分只在 DEBUG 模式下進行,因此可以放開折騰。

自動推斷返回值類型

還有最後一個問題,在 BeeHive 中使用[[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)]獲取模塊時,返回值是一個id類型,使用者需要手動指定返回變量的類型,在 Swift 中更是需要手動類型轉換,而這一步是可能出錯的,並且編譯器無法檢查。要實現最完備的類型檢查,就不能忽視這個問題。

有沒有一種方式能讓返回值的類型和 protocol 的類型對應呢?OC 中的泛型在這時候就發揮作用了。

可以在 router 上聲明模塊的泛型:

@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *> : NSObject
@end

這裏使用了兩個泛型參數 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 = "默認標題"
    }
}

<details><summary>Objective-C Sample</summary>

// router 父類
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *>: NSObject
@end
@implementation ZIKViewRouter
  
...
+ (id)makeDestination {
    ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
    id destination = [router destinationWithConfiguration:router.configuration];
    if (destination) {
        // router 父類中調用模塊配置方法
        [router prepareDestination:destination configuration:router.configuration];
    }
    return destination;
}

// editor 模塊的 router
@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    EditorViewController *destination = [[EditorViewController alloc] init];
    return destination;
}

// 配置模塊,注入靜態依賴
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
    // 注入 service 依賴
    destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
    // 其他配置
    destination.title = @"默認標題";
}

@end

</details>

此時調用者中如果有某些對象不是創建自 router的,就可以直接用對應的 router 進行配置,執行依賴注入:

var destination: EditorViewProtocol = ...
Router.to(RoutableView<EditorViewProtocol>())?.prepare(destination: destination, configuring: { (config, _) in
    
})


<details><summary>Objective-C Sample</summary>

id<EditorViewProtocol> destination = ...
[ZIKRouterToView(EditorViewProtocol) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
    
}];

</details>

獨立的配置功能在某些場景下是非常有用的,尤其是在重構現有代碼的時候。有一些系統接口的設計就是在接口中返回對象,但是這些對象是由系統自動創建的,而不是通過 router 創建的,因此需要通過 router 對其進行配置,例如 storyboard 中創建的 view controller。此時將 view controller 模塊化後,依然可以保持現有代碼,只需要調用一句prepareDestination:configuration:配置即可,模塊化的過程中就能讓代碼的修改最小化。

可選依賴:屬性注入和方法注入

當依賴是可選的,並不是創建對象所必需的,可以用屬性注入和方法注入。

屬性注入是指外部設置對象的屬性。方法注入是指外部調用對象的方法,從而傳入依賴。

protocol PersonType {
    var wife: Person? { get set } // 可選的屬性依賴
    func addChild(_ child: Person) -> Void // 可選的方法注入
}
protocol Child {
    var parent: Person { get }
}

class Person: PersonType {
    var wife: Person? = nil
    var childs: Set<Child> = []
    func addChild(_ child: Child) {
        childs.insert(child)
    }
}

<details><summary>Objective-C示例</summary>

@protocol PersonType: ZIKServiceRoutable
@property (nonatomic, strong, nullable) Person *wife; // 可選的屬性依賴
- (void)addChild:(Person *)child; // 可選的方法注入
@end
@protocol Child
@property (nonatomic, strong) Person *parent;
@end

@interface Person: NSObject <PersonType>
@property (nonatomic, strong, nullable) Person *wife;
@property (nonatomic, strong) NSSet<id<Child>> childs;
@end

</details>

在 router 裏,可以注入一些默認的依賴:

class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {
    ...    
    override func destination(with configuration: PerformRouteConfig) -> Person? {
        let person = Person()
        return person
    }

    // 配置模塊,注入靜態依賴
    override func prepareDestination(_ destination: Person, configuration: PerformRouteConfig) {
        if destination.wife != nil {
            return
        }
        //設置默認值
        let wife: Person = ...
        person.wife = wife
    }
}

<details><summary>Objective-C示例</summary>

@interface PersonRouter: ZIKServiceRouter<Person *, ZIKPerformRouteConfiguration *>
@end
@implementation PersonRouter

- (nullable Person *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
    Person *person = [Person new];
    return person;
}
// 配置模塊,注入靜態依賴
- (void)prepareDestination:(Person *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
    if (destination.wife != nil) {
        return;
    }
    Person *wife = ...
    destination.wife = wife;
}

@end

</details>

模塊間參數傳遞

在執行路由操作的同時,調用者也可以用PersonType動態地注入依賴,也就是向模塊傳參。

configuration 就是用來進行各種功能擴展的。Router 可以在 configuration 上提供prepareDestination,讓調用者設置,就能讓調用者配置 destination。

let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), configuring: { (config, _) in
    // 獲取模塊的同時進行配置
    config.prepareDestination = { destination in
        destination.wife = wife
        destination.addChild(child)
    }
})

<details><summary>Objective-C示例</summary>

Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType) 
         makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration *config) {
    // 獲取模塊的同時進行配置
    config.prepareDestination = ^(id<PersonType> destination) {
        destination.wife = wife;
        [destination addChild:child];
    };
}];

</details>

封裝一下就能變成更簡單的接口:

let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), preparation: { destination in
            destination.wife = wife
            destination.addChild(child)
        })

<details><summary>Objective-C示例</summary>

Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType) 
         makeDestinationWithPreparation:^(id<PersonType> destination) {
            destination.wife = wife;
            [destination addChild:child];
        }];

</details>

必需依賴:工廠方法

有一些參數是在 destination 類創建前就需要傳入的必需參數,例如初始化方法中的參數,就是必需依賴。

class Person: PersonType {
    let name: String
    // 初始化方法,需要必需參數
    init(name: String) {
        self.name = name
    }
}

<details><summary>Objective-C示例</summary>

@interface Person: NSObject <PersonType>
@property (nonatomic, strong) NSString *name;
// 初始化方法,需要必需參數
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
@end

</details>

這些必需參數有時候是由調用者提供的。在 URL 路由中,這種"必需"特性就無法體現出來,而用接口的方式就能簡單地實現。

傳遞必需依賴需要用工廠模式,在工廠方法上聲明必需參數和模塊接口。

protocol PersonTypeFactory {
  // 工廠方法,聲明瞭必需參數 name,返回 PersonType 類型的 destination
    func makeDestinationWith(_ name: String) -> PersonType?
}

<details><summary>Objective-C示例</summary>

@protocol PersonTypeFactory: ZIKServiceModuleRoutable
// 工廠方法,聲明瞭必需參數 name,返回 PersonType 類型的 destination
- (id<PersonType>)makeDestinationWith:(NSString *)name;
@end

</details>

那麼如何用 router 傳遞必需參數呢?

Router 的 configuration 可以用來進行自定義參數擴展。可以把必需參數保存到 configuration 上,或者更直接點,由 configuration 來提供工廠方法,然後使用工廠方法的 protocol 來獲取模塊:

// 通用 configuration,可以提供自定義工廠方法
class PersonModuleConfiguration: PerformRouteConfig, PersonTypeFactory {
    // 工廠方法
    public func makeDestinationWith(_ name: String) -> PersonType? {
        self.makedDestination = Person(name: name)
        return self.makedDestination
    }
    // 由工廠方法創建的 destination,提供給 router
    public var makedDestination: Destination?
}

<details><summary>Objective-C示例</summary>

// 通用 configuration,可以提供自定義工廠方法
@interface PersonModuleConfiguration: ZIKPerformRouteConfiguration<PersonTypeFactory>
// 由工廠方法創建的 destination,提供給 router
@property (nonatomic, strong, nullable) id<PersonTypeFactory> makedDestination;
@end
  
@implementation PersonModuleConfiguration
// 工廠方法
-(id<PersonTypeFactory>)makeDestinationWith:(NSString *)name {
    self.makedDestination = [[Person alloc] initWithName:name];
    return self.makedDestination;
}
@end

</details>

在 router 中使用自定義 configuration:

class PersonRouter: ZIKServiceRouter<Person, PersonModuleConfiguration> {
    // 重寫 defaultRouteConfiguration,使用自定義 configuration
    override class func defaultRouteConfiguration() -> PersonModuleConfiguration {
        return PersonModuleConfiguration()
    }

    override func destination(with configuration: PersonModuleConfiguration) -> Person? {
        // 使用工廠方法創建的 destination
        return config.makedDestination
    }
}

<details><summary>Objective-C示例</summary>

@interface PersonRouter: ZIKServiceRouter<id<PersonType>, PersonModuleConfiguration *>
@end
@implementation PersonRouter
  
// 重寫 defaultRouteConfiguration,使用自定義 configuration
+ (PersonModuleConfiguration *)defaultRouteConfiguration {
    return [PersonModuleConfiguration new];
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(PersonModuleConfiguration *)configuration {
    // 使用工廠方法創建的 destination
    return configuration.makedDestination;
}

@end

</details>

然後把PersonTypeFactory協議和 router 進行註冊:

PersonRouter.register(RoutableServiceModule<PersonTypeFactory>())

<details><summary>Objective-C示例</summary>

[PersonRouter registerModuleProtocol:ZIKRoutable(PersonTypeFactory)];

</details>

就可以用PersonTypeFactory獲取模塊了:

let name: String = ...
Router.makeDestination(to: RoutableServiceModule<PersonTypeFactory>(), configuring: { (config, _) in
    // config 遵守 PersonTypeFactory
    config.makeDestinationWith(name)
})

<details><summary>Objective-C示例</summary>

NSString *name = ...
ZIKRouterToServiceModule(PersonTypeFactory) makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration<PersonTypeFactory> *config) {
    // config 遵守 PersonTypeFactory
    [config makeDestinationWith:name];
}]

</details>

用泛型代替 configuration 子類

如果你不需要在 configuration 上保存其他自定義參數,也不想創建過多的 configuration 子類,可以用一個通用的泛型類來實現子類重寫的效果。

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

// 通用 configuration,可以提供自定義工廠方法
class ServiceMakeableConfiguration<Destination, Constructor>: PerformRouteConfig {    
    public var makeDestinationWith: Constructor
    public var makedDestination: Destination?
}

<details><summary>Objective-C示例</summary>

@interface ZIKServiceMakeableConfiguration<__covariant Destination>: ZIKPerformRouteConfiguration
@property (nonatomic, copy) Destination(^makeDestinationWith)();
@property (nonatomic, strong, nullable) Destination makedDestination;
@end

</details>

在 router 中使用自定義 configuration:

class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {
    
    // 重寫 defaultRouteConfiguration,使用自定義 configuration
    override class func defaultRouteConfiguration() -> PerformRouteConfig {
        let config = ServiceMakeableConfiguration<PersonType, (String) -> PersonType>({ _ in})
        // 設置工廠方法,讓調用者使用
        config.makeDestinationWith = { [unowned config] name in
            config.makedDestination = Person(name: name)
            return config.makedDestination
        }
        return config
    }

    override func destination(with configuration: PerformRouteConfig) -> Person? {
        if let config = configuration as? ServiceMakeableConfiguration<PersonType, (String) -> PersonType> {
            // 使用工廠方法創建的 destination
            return config.makedDestination
        }
        return nil
    }
}

// 讓對應泛型的 configuration 遵守 PersonTypeFactory
extension ServiceMakeableConfiguration: PersonTypeFactory where Destination == PersonType, Constructor == (String) -> PersonType {
    
}

<details><summary>Objective-C示例</summary>

@interface PersonRouter: ZIKServiceRouter<id<PersonType>, ZIKServiceMakeableConfiguration *>
@end
@implementation PersonRouter

// 重寫 defaultRouteConfiguration,使用自定義 configuration
+ (ZIKServiceMakeableConfiguration *)defaultRouteConfiguration {
    ZIKServiceMakeableConfiguration *config = [ZIKServiceMakeableConfiguration new];
    __weak typeof(config) weakConfig = config;
    // 設置工廠方法,讓調用者使用
    config.makeDestinationWith = id ^(NSString *name) {
        weakConfig.makedDestination = [[Person alloc] initWithName:name];
        return weakConfig.makedDestination;
    };
    return config;
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
    // 使用工廠方法創建的 destination
    return configuration.makedDestination;
}

@end

</details>

避免接口污染

除了必需依賴,還有一些參數是不屬於 destination 類的,而是屬於模塊內其他組件的,也不能通過 destination 的接口來傳遞。例如 MVVM 和 VIPER 架構中,model 參數不能傳給 view,而是應該交給 view model 或者 interactor。此時可以使用相同的模式。

protocol EditorViewModuleInput {
  // 工廠方法,聲明瞭參數 note,返回 EditorViewInput 類型的 destination
    func makeDestinationWith(_ note: Note) -> EditorViewInput?
}

<details><summary>Objective-C示例</summary>

@protocol EditorViewModuleInput: ZIKViewModuleRoutable
// 工廠方法,聲明瞭參數 note,返回 EditorViewInput 類型的 destination
- (id<EditorViewInput>)makeDestinationWith:(Note *)note;
@end

</details>

class EditorViewRouter: ZIKViewRouter<EditorViewInput, ViewRouteConfig> {
    
    // 重寫 defaultRouteConfiguration,使用自定義 configuration
    override class func defaultRouteConfiguration() -> ViewRouteConfig {
        let config = ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput>({ _ in})
        // 設置工廠方法,讓調用者使用
        config.makeDestinationWith = { [unowned config] note in            
            config.makedDestination = self.makeDestinationWith(note: note)
            return config.makedDestination
        }
        return config
    }
    
    class func makeDestinationWith(note: Note) -> EditorViewInput {
        let view = EditorViewController()
        let presenter = EditorViewPresenter(view)
        let interactor = EditorInteractor(Presenter)
        // 把 model 傳遞給數據管理者,view 不接觸 model
        interactor.note = note
        return view
    }

    override func destination(with configuration: ViewRouteConfig) -> EditorViewInput? {
        if let config = configuration as? ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput> {
            // 使用工廠方法創建的 destination
            return config.makedDestination
        }
        return nil
    }
}

<details><summary>Objective-C示例</summary>

@interface EditorViewRouter: ZIKViewRouter<id<EditorViewInput>, ZIKViewMakeableConfiguration *>
@end
@implementation PersonRouter

// 重寫 defaultRouteConfiguration,使用自定義 configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
    ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
    __weak typeof(config) weakConfig = config;
    // 設置工廠方法,讓調用者使用
    config.makeDestinationWith = id ^(Note *note) {
        weakConfig.makedDestination = [self makeDestinationWith:note];
        return weakConfig.makedDestination;
    };
    return config;
}

+ (id<EditorViewInput>)makeDestinationWith:(Note *)note {
    EditorViewController *view = [[EditorViewController alloc] init];
    EditorViewPresenter *presenter = [[EditorViewPresenter alloc] initWithView:view];
    EditorInteractor *interactor = [[EditorInteractor alloc] initWithPresenter:presenter];
    // 把 model 傳遞給數據管理者,view 不接觸 model
    interactor.note = note;
    return view;
}
  
- (nullable id<EditorViewInput>)destinationWithConfiguration:(ZIKViewMakeableConfiguration *)configuration {
    // 使用工廠方法創建的 destination
    return configuration.makedDestination;
}

@end

</details>

就可以用EditorViewModuleInput獲取模塊了:

let note: Note = ...
Router.makeDestination(to: RoutableViewModule<EditorViewModuleInput>(), configuring: { (config, _) in
    // config 遵守 EditorViewModuleInput
    config.makeDestinationWith(note)
})

<details><summary>Objective-C示例</summary>

Note *note = ...
ZIKRouterToViewModule(EditorViewModuleInput) makeDestinationWithConfiguring:^(ZIKViewRouteConfiguration<EditorViewModuleInput> *config) {
    // config 遵守 EditorViewModuleInput
    config.makeDestinationWith(note);
}]

</details>

依賴查找

當模塊的必需依賴很多時,如果把依賴都放在初始化接口中,就會出現一個非常長的方法。

除了讓模塊把依賴聲明在接口中,模塊內部也可以用模塊管理工具動態查找依賴,例如用 router 查找 protocol 對應的模塊。如果要使用這種模式,那麼所有模塊都需要統一使用相同的模塊管理工具。

代碼如下:

class EditorViewController: UIViewController {
    lazy var storageService: EditorStorageServiceInput {
        return Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())!
    }
}

<details><summary>Objective-C示例</summary>

@interface EditorViewController : UIViewController()
@property (nonatomic, strong) id<EditorStorageServiceInput> storageService;
@end
@implementation EditorViewController
  
- (id<EditorStorageServiceInput>)storageService {
    if (!_storageService) {
        _storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
    }
    return _storageService;
}
  
@end

</details>

循環依賴

使用依賴注入時,有些特殊情況需要處理,例如循環依賴的無限遞歸問題。

循環依賴是指兩個對象互相依賴。

在 router 內部動態注入依賴時,如果注入的依賴同時依賴於被注入的對象,則必須在 protocol 中聲明。

protocol Parent {
    // Parent 依賴 Child
    var child: Child { get set }
}

protocol Child {
    // Child 依賴 Parent
    var parent: Parent { get set }
}

class ParentObject: Parent {
    var child: Child!
}

class ChildObject: Child {
    var parent: Parent!
}

<details><summary>Objective-C示例</summary>

@protocol Parent <ZIKServiceRoutable>
// Parent 依賴 Child
@property (nonatomic, strong) id<Child> child;
@end

@protocol Child <ZIKServiceRoutable>
// Child 依賴 Parent
@property (nonatomic, strong) id<Parent> parent;
@end

@interface ParentObject: NSObject<Parent>
@end

@interface ParentObject: NSObject<Child>
@end

</details>

class ParentRouter: ZIKServiceRouter<ParentObject, PerformRouteConfig> {
    
    override func destination(with configuration: PerformRouteConfig) -> ParentObject? {
        return ParentObject()
    }
    override func prepareDestination(_ destination: ParentObject, configuration: PerformRouteConfig) {
        guard destination.child == nil else {
            return
        }
        // 只有在外部沒有設置 child 時,纔去主動尋找依賴
        let child = Router.makeDestination(to RoutableService<Child>(), preparation { child in
            // 設置 child 的依賴,防止 child 內部再去尋找 parent 依賴,導致循環
            child.parent = destination
        })
        destination.child = child
    }
}

class ChildRouter: ZIKServiceRouter<ChildObject, PerformRouteConfig> {
      
    override func destination(with configuration: PerformRouteConfig) -> ChildObject? {
        return ChildObject()
    }
    override func prepareDestination(_ destination: ChildObject, configuration: PerformRouteConfig) {
        guard destination.parent == nil else {
            return
        }
        // 只有在外部沒有設置 parent 時,纔去主動尋找依賴
        let parent = Router.makeDestination(to RoutableService<Parent>(), preparation { parent in
            // 設置 parent 的依賴,防止 parent 內部再去尋找 child 依賴,導致循環
            parent.child = destination
        })
        destination.parent = parent
    }
}

<details><summary>Objective-C示例</summary>

@interface ParentRouter: ZIKServiceRouter<ParentObject *, ZIKPerformRouteConfiguration *>
@end
@implementation ParentRouter

- (ParentObject *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
    return [ParentObject new];
}

- (void)prepareDestination:(ParentObject *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
    if (destination.child) {
        return;
    }
    // 只有在外部沒有設置 child 時,纔去主動尋找依賴
    destination.child = [ZIKRouterToService(Child) makeDestinationWithPreparation:^(id<Child> child) {
        // 設置 child 的依賴,防止 child 內部再去尋找 parent 依賴,導致循環
        child.parent = destination;
    }];
}

@end

@interface ChildRouter: ZIKServiceRouter<ChildObject *, ZIKPerformRouteConfiguration *>
@end
@implementation ChildRouter

- (ChildObject *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
    return [ChildObject new];
}

- (void)prepareDestination:(ChildObject *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
    if (destination.parent) {
        return;
    }
    // 只有在外部沒有設置 parent 時,纔去主動尋找依賴
    destination.parent = [ZIKRouterToService(Parent) makeDestinationWithPreparation:^(id<Parent> parent) {
        // 設置 parent 的依賴,防止 parent 內部再去尋找 child 依賴,導致循環
        parent.child = destination;
    }];
}

@end

</details>

這樣就能避免循環依賴導致的無限遞歸問題。

模塊適配器

當使用 protocol 管理模塊時,protocol 必定會出現在多個模塊中。那麼此時如何讓每個模塊單獨編譯呢?

一個方式是把 protocol 在每個用到的模塊裏複製一份,而且無需修改 protocol 名,Xcode 不會報錯。

另一個方式是使用適配器模式,可以讓不同模塊使用各自不同的 protocol 和同一個模塊交互。

required protocol 和 provided protocol

你可以爲同一個 router 註冊多個 protocol。

根據依賴關係,接口可以分爲required 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 = "請登錄"
})

<details><summary>Objective-C示例</summary>

// 調用者中聲明的依賴接口,表明自身依賴一個登陸界面
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end

// 調用者中調用 login 模塊
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
    destination.message = @"請登錄";
}];

</details>

實際登陸界面提供的接口則是ProvidedLoginViewInput

// 實際登陸界面提供的接口
protocol ProvidedLoginViewInput {
   var message: String? { get set }
}

<details><summary>Objective-C示例</summary>

// 實際登陸界面提供的接口
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end

</details>

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

// 讓模塊支持 required protocol,只需要添加一個 protocol 擴展即可
extension LoginViewController: RequiredLoginViewInput {
}

<details><summary>Objective-C示例</summary>

// 讓模塊支持 required protocol,只需要添加一個 protocol 擴展即可
@interface LoginViewController (ModuleAAdapter) <RequiredLoginViewInput>
@end
@implementation LoginViewController (ModuleAAdapter)
@end

</details>

並且讓登陸界面的 router 也支持 RequiredLoginViewInput

// 如果可以獲取到 router 類,可以直接爲 router 添加 RequiredLoginViewInput
LoginViewRouter.register(RoutableView<RequiredLoginViewInput>())
// 如果不能得到對應模塊的 router,可以用 adapter 進行轉發
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())

<details><summary>Objective-C示例</summary>

//如果可以獲取到 router 類,可以直接爲 router 添加 RequiredLoginViewInput
[LoginViewRouter registerViewProtocol:ZIKRoutable(RequiredLoginViewInput)];
//如果不能得到對應模塊的 router,可以註冊 adapter
[self registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];

</details>

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

調用者模塊示例:

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

// ProvidedLoginViewInput 和 RequiredLoginViewInput 能獲取到同一個 router
Router.makeDestination(to: RoutableView<ProvidedLoginViewInput>(), preparation: {
    destination.message = "請登錄"
})

<details><summary>Objective-C示例</summary>

[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
    destination.message = @"請登錄";
}];

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

</details>

接口適配

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

protocol ProvidedLoginViewInput {
   var notifyString: String? { get set } // 接口名不同
}

<details><summary>Objective-C示例</summary>

@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString; // 接口名不同
@end

</details>

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

extension LoginViewController: RequiredLoginViewInput {
    var message: String? {
        get {
            return notifyString
        }
        set {
            notifyString = newValue
        }
    }
}

<details><summary>Objective-C示例</summary>

@interface LoginViewController (ModuleAAdapter) <RequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapter)
- (void)setMessage:(NSString *)message {
    self.notifyString = message;
}
- (NSString *)message {
    return self.notifyString;
}
@end

</details>

用中介者轉發接口

如果不能直接爲模塊添加required protocol,比如 protocol 裏的一些 delegate 需要兼容:

protocol RequiredLoginViewDelegate {
    func didFinishLogin() -> Void
}
protocol RequiredLoginViewInput {
  var message: String? { get set }
  var delegate: RequiredLoginViewDelegate { get set }
}

<details><summary>Objective-C示例</summary>

@protocol RequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end

@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<RequiredLoginViewDelegate> delegate;
@end

</details>

而模塊裏的 delegate 接口不一樣:

protocol ProvidedLoginViewDelegate {
    func didLogin() -> Void
}
protocol ProvidedLoginViewInput {
  var notifyString: String? { get set }
  var delegate: ProvidedLoginViewDelegate { get set }
}

<details><summary>Objective-C示例</summary>

@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end

@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<ProvidedLoginViewDelegate> delegate;
@end

</details>

相同方法有不同參數類型時,可以用一個新的 router 代替真正的 router,在新的 router 裏插入一箇中介者,負責轉發接口:

class ReqiredLoginViewRouter: ProvidedLoginViewRouter {

   override func destination(with configuration: ZIKViewRouteConfiguration) -> RequiredLoginViewInput? {
       let realDestination: ProvidedLoginViewInput = super.destination(with configuration)
       // proxy 負責把 RequiredLoginViewInput 轉發爲 ProvidedLoginViewInput
       let proxy: RequiredLoginViewInput = ProxyForDestination(realDestination)
       return proxy
   }
}


<details><summary>Objective-C示例</summary>

@interface ReqiredLoginViewRouter : ProvidedLoginViewRouter
@end
@implementation RequiredLoginViewRouter

- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
   id<ProvidedLoginViewInput> realDestination = [super destinationWithConfiguration:configuration];
    // proxy 負責把 RequiredLoginViewInput 轉發爲 ProvidedLoginViewInput
    id<RequiredLoginViewInput> proxy = ProxyForDestination(realDestination);
    return mediator;
}
@end

</details>

對於普通OC類,proxy 可以用 NSProxy 來實現。對於 UIKit 中的那些複雜的 UI 類,或者 Swift 類,可以用子類,然後在子類中重寫方法,進行模塊適配。

聲明式依賴

利用之前的靜態路由檢查機制,模塊只需要聲明 required 接口,就能保證對應的模塊必定存在。

模塊無需在自己的接口裏聲明依賴,如果模塊需要新增依賴,只需要創建新的 required 接口即可,無需修改接口本身。這樣也能避免依賴變動導致的接口變化,減少接口維護的成本。

模塊提供默認的依賴配置

每次引入模塊,宿主 app 都需要寫一份適配代碼,雖然大多數情況下只有兩行,但是我們想盡量減少宿主 app 的維護職責。

此時,可以讓模塊提供一份默認的依賴,用宏定義包裹,繞過編譯檢查。

#if USE_DEFAULT_DEPENDENCY

import ProvidedLoginModule

public func registerDefaultDependency() {
    ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
}

extension ProvidedLoginViewController: RequiredLoginViewInput {

}

#endif

<details><summary>Objective-C示例</summary>

#if USE_DEFAULT_DEPENDENCY

@import ProvidedLoginModule;

static inline void registerDefaultDependency() {
    [ZIKViewRouteAdapter registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
}

// 宏定義,默認的適配代碼
#define ADAPT_DEFAULT_DEPENDENCY    \
@interface ProvidedLoginViewController (Adapter) <RequiredLoginViewInput>    \
@end    \
@implementation ProvidedLoginViewController (Adapter) \
@end    \

#endif

</details>

如果宿主 app 要使用默認依賴,就在.xcconfig裏設置Preprocessor Macros,開啓宏定義:

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) USE_DEFAULT_DEPENDENCY=1

如果是 Swift 模塊,需要在模塊的 target 裏設置Active Compilation Conditions,添加編譯宏USE_DEFAULT_DEPENDENCY

宿主 app 直接調用默認的適配代碼即可,不用再負責維護:

public func registerAdapters() {
    // 註冊默認的依賴
    registerDefaultDependency()
    ...
}

<details><summary>Objective-C示例</summary>

void registerAdapters() {
    // 註冊默認的依賴
    registerDefaultDependency();
    ...
}

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

</details>

如果宿主 app 需要替換使用另一個 provided 模塊,可以關閉宏定義,再寫一份另外的適配代碼,即可替換依賴。

模塊化

區分了required 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)
    }
}

<details><summary>Objective-C Sample</summary>

@interface EditorViewController: UIViewController
@property (nonatomic, strong) id<EditorViewModel> viewModel;
@end
@implementation EditorViewController
  
- (void)addTextView {
    UIViewController *textViewController = [ZIKRouterToView(TextViewInput) makeDestinationWithPreparation:^(id<TextViewInput> destination) {
        // 設置模塊間交互
        // 原本父 view 是無法接觸到子模塊的 view model / presenter / interactor
        // 此時子模塊是把這些內部組件作爲業務 input 開放給了外部        
        self.viewModel.textService = destination.viewModel;
        destination.viewModel.output = self.viewModel;
    }];

    [self addChildViewController:textViewController];
    [self.view addSubview: textViewController.view];
    [textViewController didMoveToParentViewController: self];
}

@end

</details>

子模塊:

protocol TextViewInput {
    weak var output: TextViewModuleOutput? { get set }
    var viewModel: TextViewModel { get }
}

class TextViewController: UIViewController, TextViewInput {
    weak var output: TextViewModuleOutput?
    var viewModel: TextViewModel!
}

<details><summary>Objective-C Sample</summary>

@protocol TextViewInput <ZIKViewRoutable>
@property (nonatomic, weak) id<TextViewModuleOutput> output;
@property (nonatomic, strong) id<TextViewModel> viewModel;
@end

@interface TextViewController: UIViewController <TextViewInput>
@property (nonatomic, weak) id<TextViewModuleOutput> output;
@property (nonatomic, strong) id<TextViewModel> viewModel;
@end

</details>

Output 的適配

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

例如這樣一對 required-provided protocol:

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

protocol ProvidedEditorViewInput {
    weak var output: ProvidedEditorViewOutput? { get set }
}

<details><summary>Objective-C Sample</summary>

@protocol RequiredEditorViewInput <NSObject>
@property (nonatomic, weak) id<RequiredEditorViewOutput> output;
@end

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

</details>

由於 output 的實現者不是固定的,因此無法讓所有的 output 類都同時適配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
    }
}

<details><summary>Objective-C Sample</summary>

@protocol ProvidedEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic, readonly) id<ProvidedEditorViewInput> (makeDestinationWith)(id<ProvidedEditorViewOutput> output);
@end
  
@interface ProvidedEditorViewRouter: ZIKViewRouter
@end
@implementation ProvidedEditorViewRouter

+ (void)registerRoutableDestination {
    [self registerModuleProtocol:ZIKRoutable(ProvidedEditorViewModuleInput)];  
}

+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
    ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
    __weak typeof(config) weakConfig = config;
    
    config.makeDestinationWith = id ^(id<ProvidedEditorViewOutput> output) {
        // 設置 output
        EditorViewModel *viewModel = [[EditorViewModel alloc] initWithOutput:output];
        weakConfig.makedDestination = [[EditorViewController alloc] initWithViewModel:viewModel];
        return weakConfig.makedDestination;
    };
    return config;
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
    return configuration.makedDestination;
}

@end

</details>

適配代碼:

protocol RequiredEditorViewModuleInput {
    var makeDestinationWith(_ output: RequiredEditorViewOutput?) -> RequiredEditorViewInput? { get set }
}

// 用於適配的 required router
class RequiredEditorViewRouter: ProvidedEditorViewRouter {
    
    override class func registerRoutableDestination() {
        register(RoutableViewModule<RequiredEditorViewModuleInput>())
    }
  
    // 兼容 configuration
    override class func defaultRouteConfiguration() -> PerformRouteConfig {
        let config = super.defaultRouteConfiguration()
        let makeDestinationWith = config.makeDestinationWith
        
        config.makeDestinationWith = { requiredOutput in
            // proxy 負責把 RequiredEditorViewOutput 轉爲 ProvidedEditorViewOutput
            let providedOutput = EditorOutputProxy(forwarding: requiredOutput)
            return makeDestinationWith(providedOutput)
        }
        return config
    }
}

class EditorOutputProxy: ProvidedEditorViewOutput {
    let forwarding: RequiredEditorViewOutput
    // 實現 ProvidedEditorViewOutput,轉發給 forwarding
}

<details><summary>Objective-C Sample</summary>

@protocol RequiredEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic, readonly) id<RequiredEditorViewInput> (makeDestinationWith)(id<RequiredEditorViewOutput> output);
@end

// 用於適配的 required router
@interface RequiredEditorViewRouter: ProvidedEditorViewRouter
@end
@implementation RequiredEditorViewRouter

+ (void)registerRoutableDestination {
    [self registerModuleProtocol:ZIKRoutable(RequiredEditorViewModuleInput)];  
}
// 兼容 configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
    ZIKViewMakeableConfiguration *config = [super defaultRouteConfiguration];
    id<ProvidedEditorViewInput>(^makeDestinationWith)(id<ProvidedEditorViewOutput>) = config.makeDestinationWith;
    
    config.makeDestinationWith = id ^(id<RequiredEditorViewOutput> requiredOutput) {
        // proxy 負責把 RequiredEditorViewOutput 轉爲 ProvidedEditorViewOutput
        EditorOutputProxy *providedOutput = [[EditorOutputProxy alloc] initWithForwarding: requiredOutput];
        return makeDestinationWith(providedOutput);
    };
    return config;
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
    return configuration.makedDestination;
}

@end
  
// 實現 ProvidedEditorViewOutput,轉發給 forwarding
@interface EditorOutputProxy: NSProxy <ProvidedEditorViewOutput>
@property (nonatomic, strong) id forwarding;
@end
@implementation EditorOutputProxy
  
- (instancetype)initWithForwarding:(id)forwarding {
    if (self = [super init]) {
        _forwarding = forwarding;
    }
    return self;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.forwarding respondsToSelector:aSelector];
}

- (BOOL)conformsToProtocol:(Protocol *)protocol {
    return [self.forwarding conformsToProtocol:protocol];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.forwarding;
}

@end

</details>

可以看到,output 的適配有些繁瑣。因此除非你的模塊是通用模塊,有實際的解耦需求,否則直接使用 provided protocol 即可。

文章內容過長,因簡書字數限制,繼續閱讀請看下一篇打造完備的 iOS 組件化方案:如何面向接口進行模塊解耦?(二)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章