日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

swift 组件化_打造完备的iOS组件化方案:如何面向接口进行模块解耦?

發(fā)布時間:2025/3/20 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 swift 组件化_打造完备的iOS组件化方案:如何面向接口进行模块解耦? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

作者 | 黑超熊貓zuik,一個修行中的 iOS 開發(fā),喜歡搞點別人沒搞過的東西,鉆研過逆向工程、VIPER 架構(gòu)和組件化。

關于組件化的探討已經(jīng)有不少了,在之前的文章?iOS VIPER架構(gòu)實踐(三):面向接口的路由設計[1]?中,綜合比較了各種方案后,我傾向于使用面向接口的方式進行組件化。

這是一篇從代碼層面講解模塊解耦的文章,會全方位地展示如何實踐面向接口的思想,盡量全面地探討在模塊管理和解耦的過程中,需要考慮到的各種問題,并且給出實際的解決方案,以及對應的模塊管理開源工具:ZIKRouter[2]。你也可以根據(jù)本文的內(nèi)容改造自己現(xiàn)有的方案,即使你的項目不進行組件化,也可以參考本文進行代碼解耦。
文章主要內(nèi)容:

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

什么是組件化

將模塊單獨抽離、分層,并制定模塊間通信的方式,從而實現(xiàn)解耦,以及適應團隊開發(fā)。

為什么需要組件化

主要有4個原因:

? 模塊間解耦
? 模塊重用
? 提高團隊協(xié)作開發(fā)效率
? 單元測試

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

問題主要體現(xiàn)在:

? 修改某個模塊的功能時,需要修改許多其他模塊的代碼,因為這個模塊被其他模塊引用

? 模塊對外的接口不明確,外部甚至會調(diào)用不應暴露的私有接口,修改時會耗費大量時間

? 修改的模塊涉及范圍較廣,很容易影響其他團隊成員的開發(fā),產(chǎn)生代碼沖突

? 當需要抽離模塊到其他地方重用時,會發(fā)現(xiàn)耦合導致根本無法單獨抽離

? 模塊間的耦合導致接口和依賴混亂,難以編寫單元測試

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

你的項目是否需要組件化

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

? 項目較小,模塊間交互簡單,耦合少

? 模塊沒有被多個外部模塊引用,只是一個單獨的小模塊

? 模塊不需要重用,代碼也很少被修改

? 團隊規(guī)模很小

? 不需要編寫單元測試

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

不過,當你發(fā)現(xiàn)這幾個跡象時,就需要考慮組件化了:

? 模塊邏輯復雜,多個模塊間頻繁互相引用

? 項目規(guī)模逐漸變大,修改代碼變得越來越困難

? 團隊人數(shù)變多,提交的代碼經(jīng)常和其他成員沖突

? 項目編譯耗時較大

? 模塊的單元測試經(jīng)常由于其他模塊的修改而失敗

組件化方案的8條指標

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

1) 模塊間沒有直接耦合,一個模塊內(nèi)部的修改不會影響到另一個模塊

2) 模塊可以被單獨編譯

3) 模塊間能夠清晰地進行數(shù)據(jù)傳遞

4) 模塊可以隨時被另一個提供了相同功能的模塊替換

5) 模塊的對外接口容易查找和維護

6) 當模塊的接口改變時,使用此模塊的外部代碼能夠被高效地重構(gòu)

7) 盡量用最少的修改和代碼,讓現(xiàn)有的項目實現(xiàn)模塊化

8) 支持 Objective-C 和 Swift,以及混編

前4條用于衡量一個模塊是否真正解耦,后4條用于衡量在項目實踐中的易用程度。最后一條必須支持 Swift,是因為 Swift 是一個必然的趨勢,如果你的方案不支持 Swift,說明這個方案在將來的某個時刻必定要改進改變,而到時候所有基于這個方案實現(xiàn)的模塊都會受到影響。

基于這8個指標,我們就能在一定程度上對我們的方案做出衡量了。

方案對比

現(xiàn)在主要有3種組件化方案:URL 路由、target-action、protocol 匹配。

接下來我們就比較一下這幾種組件化方案,看看它們各有什么優(yōu)缺點。這部分在之前的文章中已經(jīng)探討過,這里再重新比較一次,補充一些細節(jié)。必須要先說明的是,沒有一個完美的方案能滿足所有場景下的需求,需要根據(jù)每個項目的需求選擇最適合的方案。

URL 路由

目前 iOS 上絕大部分的路由工具都是基于 URL 匹配的,或者是根據(jù)命名約定,用 runtime 方法進行動態(tài)調(diào)用。

這些動態(tài)化的方案的優(yōu)點是實現(xiàn)簡單,缺點是需要維護字符串表,或者依賴于命名約定,無法在編譯時暴露出所有問題,需要在運行時才能發(fā)現(xiàn)錯誤。
代碼示例:

// 注冊某個URL
[URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {
UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];
return editorViewController;
}];
// 調(diào)用路由
[URLRouter openURL:@"app://editor/?debug=true" completion:^(NSDictionary *info) {

}];

URL router 的優(yōu)點:

? 極高的動態(tài)性,適合經(jīng)常開展運營活動的 app,例如電商
? 方便地統(tǒng)一管理多平臺的路由規(guī)則
? 易于適配 URL Scheme

URL router 的缺點:

? 傳參方式有限,并且無法利用編譯器進行參數(shù)類型檢查,因此所有的參數(shù)都只能從字符串中轉(zhuǎn)換而來
? 只適用于界面模塊,不適用于通用模塊
? 不能使用 designated initializer 聲明必需參數(shù)
? 要讓 view controller 支持 url,需要為其新增初始化方法,因此需要對模塊做出修改
? 不支持 storyboard
? 無法明確聲明模塊提供的接口,只能依賴于接口文檔,重構(gòu)時無法確保修改正確
? 依賴于字符串硬編碼,難以管理
? 無法保證所使用的模塊一定存在
? 解耦能力有限,url 的"注冊"、"實現(xiàn)"、"使用"必須用相同的字符規(guī)則,一旦任何一方做出修改都會導致其他方的代碼失效,并且重構(gòu)難度大

字符串解耦的問題

如果用上面的8個指標來衡量,URL 路由只能滿足"支持模塊單獨編譯"、"支持 OC 和 Swift"兩條。它的解耦程度非常一般。

所有基于字符串的解耦方案其實都可以說是偽解耦,它們只是放棄了編譯依賴,但是當代碼變化之后,即便能夠編譯運行,邏輯仍然是錯誤的。

例如修改了模塊定義時的 URL:

// 注冊某個URL
[URLRouter registerURL:@"app://editorView" handler:^(NSDictionary *userInfo) {
...
}];

那么調(diào)用者的 URL 也必須修改,代碼仍然是有耦合的,只不過此時編譯器無法檢查而已。這會導致維護更加困難,一旦 URL 中的參數(shù)有了增減,或者決定替換為另一個模塊,參數(shù)命名有了變化,幾乎沒有高效的方式來重構(gòu)代碼。可以使用宏定義來管理字符串,不過這要求所有模塊都使用同一個頭文件,并且也無法解決參數(shù)類型和數(shù)量變化的問題。

URL 路由適合用來做遠程模塊的網(wǎng)絡協(xié)議交互,而在管理本地模塊時,最大的甚至是唯一的優(yōu)勢,就是適合經(jīng)常跨多端運營活動的 app,因為可以由運營人員統(tǒng)一管理多平臺的路由規(guī)則。

代表框架

? routable-ios
? JLRoutes
? MGJRouter
? HHRouter

改進:避免字符串管理

改進 URL 路由的方式,就是避免使用字符串,通過接口管理模塊。

參數(shù)可以通過 protocol 直接傳遞,能夠利用編譯器檢查參數(shù)類型,并且在 ZIKRouter 中,能通過路由聲明和編譯檢查,保證所使用的模塊一定存在。在為模塊創(chuàng)建路由時,也無需修改模塊的代碼。

但是必須要承認的是,盡管 URL 路由缺點多多,但它在跨平臺路由管理上的確是最適合的方案。因此 ZIKRouter 也對 URL 路由做出了支持,在用 protocol 管理的同時,可以通過字符串匹配 router,也能和其他 URL router 框架對接。

Target-Action 方案

有一些模塊管理工具基于 Objective-C 的 runtime、category 特性動態(tài)獲取模塊。例如通過NSClassFromString獲取類并創(chuàng)建實例,通過performSelector: NSInvocation動態(tài)調(diào)用方法。

例如基于 target-action 模式的設計,大致是利用 category 為路由工具添加新接口,在接口中通過字符串獲取對應的類,再用 runtime 創(chuàng)建實例,動態(tài)調(diào)用實例的方法。

示例代碼:

// 模塊管理者,提供了動態(tài)調(diào)用 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 動態(tài)創(chuàng)建 Target_Editor,并調(diào)用 Action_viewController:
UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];
return viewController;
}

@end

// 調(diào)用者通過 Mediator 的接口調(diào)用模塊
UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
// 模塊提供者提供 target-action 的調(diào)用方式
@interface Target_Editor : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end

@implementation Target_Editor

- (UIViewController *)Action_viewController:(NSDictionary *)params {
// 參數(shù)通過字典傳遞,無法保證類型安全
EditorViewController *viewController = [[EditorViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}

@end

優(yōu)點:

? 利用 category 可以明確聲明接口,進行編譯檢查
? 實現(xiàn)方式輕量

缺點:

? 需要在 mediator 和 target 中重新添加每一個接口,模塊化時代碼較為繁瑣

? 在 category 中仍然引入了字符串硬編碼,內(nèi)部使用字典傳參,一定程度上也存在和 URL 路由相同的問題

? 無法保證所使用的模塊一定存在,target 模塊在修改后,使用者只有在運行時才能發(fā)現(xiàn)錯誤

? 過于依賴 runtime 特性,無法應用到純 Swift 上。在 Swift 中擴展 mediator 時,無法使用純 Swift 類型的參數(shù)

? 可能會創(chuàng)建過多的 target 類

使用 runtime 相關的接口調(diào)用任意類的任意方法,需要注意別被蘋果的審核誤傷。參考:Are performSelector and respondsToSelector banned by App Store?[3]

字典傳參的問題

字典傳參時無法保證參數(shù)的數(shù)量和類型,只能依賴調(diào)用約定,就和字符串傳參一樣,一旦某一方做出修改,另一方也必須修改。

相比于 URL 路由,target-action 通過 category 的接口把字符串管理的問題縮小到了 mediator 內(nèi)部,不過并沒有完全消除,而且在其他方面仍然有很多改進空間。上面的8個指標中其實只能滿足第2個"支持模塊單獨編譯",另外在和接口相關的第3、5、6點上,比 URL 路由要有改善。

代表框架

? CTMediator

改進:避免字典傳參

Target-Action 方案最大的優(yōu)點就是整個方案實現(xiàn)輕量,并且也一定程度上明確了模塊的接口。只是這些接口都需要通過 Target-Action 封裝一次,并且每個模塊都要創(chuàng)建一個 target 類,既然如此,直接用 protocol 進行接口管理會更加簡單。

ZIKRouter 避免使用 runtime 獲取和調(diào)用模塊,因此可以適配 OC 和 swift。同時,基于 protocol 匹配的方式,避免引入字符串硬編碼,能夠更好地管理模塊,也避免了字典傳參。

基于 protocol 匹配的方案

有一些模塊管理工具或者依賴注入工具,也實現(xiàn)了基于接口的管理方式。實現(xiàn)思路是將 protocol 和對應的類進行字典匹配,之后就可以用 protocol 獲取 class,再動態(tài)創(chuàng)建實例。

BeeHive 示例代碼:

// 注冊模塊 (protocol-class 匹配)
[[BeeHive shareInstance] registerService:@protocol(EditorViewProtocol) service:[EditorViewController class]];// 獲取模塊 (用 runtime 創(chuàng)建 EditorViewController 實例)
id editor = [[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)];

優(yōu)點:

? 利用接口調(diào)用,實現(xiàn)了參數(shù)傳遞時的類型安全

? 直接使用模塊的 protocol 接口,無需再重復封裝

缺點:

? 由框架來創(chuàng)建所有對象,創(chuàng)建方式有限,例如不支持外部傳入?yún)?shù),再調(diào)用自定義初始化方法

? 用 OC runtime 創(chuàng)建對象,不支持 Swift

? 只做了 protocol 和 class 的匹配,不支持更復雜的創(chuàng)建方式和依賴注入

? 無法保證所使用的 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 單例來創(chuàng)建,使用場景十分有限,例如不支持純 Swift 類型,不支持使用自定義初始化方法以及額外的依賴注入。

ZIKRouter 進行了進一步的改進,并不是直接對 protocol 和 class 進行匹配,而是將 protocol 和 router 子類或者 router 對象進行匹配,在 router 子類中再提供創(chuàng)建模塊的實例的方式。這時,模塊的創(chuàng)建職責就從 BeeHive 單例上轉(zhuǎn)到了每個單獨的 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
}
}

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

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

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

EditorViewRouter.register(RoutableView())

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

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

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

? 可以在 router 上添加許多通用的擴展接口,例如創(chuàng)建模塊、依賴注入、界面跳轉(zhuǎn)、界面移除,甚至增加 URL 路由支持

? 在每個 router 子類中可以進行更詳細的依賴注入和自定義操作

? 可以自定義創(chuàng)建對象的方式,例如自定義初始化方法、工廠方法,在重構(gòu)時可以直接搬運現(xiàn)有的創(chuàng)建代碼,無需在原來的類上增加或修改接口,減少模塊化過程中的工作量

? 可以讓多個 protocol 和同一個模塊進行匹配

? 可以讓模塊進行接口適配,允許外部做完適配后,為 router 添加新的 protocol,解決編譯依賴的問題

? 返回的對象只需符合 protocol,不再和某個單一的類綁定。因此可以根據(jù)條件,返回不同的對象,例如適配不同系統(tǒng)版本時,返回不同的控件,讓外部只關注接口

動態(tài)化的風險

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

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

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

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

答案是 YES。

靜態(tài)路由檢查

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

當使用了錯誤的 protocol 時,會產(chǎn)生編譯錯誤。

Swift 中使用未聲明的 protocol:

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

這個特性通過兩個機制來實現(xiàn):

? 只有被聲明為可路由的 protocol 才能用于路由,否則會產(chǎn)生編譯錯誤

? 可路由的 protocol 必定有一個對應的模塊存在

下面就一步步講解,怎么在保持動態(tài)解耦特性的同時,實現(xiàn)一套完備的靜態(tài)類型檢查的機制。

路由聲明

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

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

原生 Xcode 并不支持這樣的靜態(tài)檢查,這時候就要考驗我們的創(chuàng)造力了。

Objective-C:protocol 繼承鏈

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

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

@protocol ZIKViewRoutable
@end

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

toView用類屬性的方式提供,以方便鏈式調(diào)用,這個 block 接收一個Protocol?*類型的 protocol,返回對應的 router 類。

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

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

// 聲明時繼承自 ZIKViewRoutable
@protocol EditorViewProtocol @end// 宏定義,為 protocol 變量添加 protocol 限定
#define ZIKRoutable(RoutableProtocol) (Protocol*)@protocol(RoutableProtocol)// 用 protocol 獲取 router
ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))

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

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

Swift:條件擴展

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

可以用 struct 的泛型傳遞 protocol,然后用條件擴展為特定泛型的 struct 添加初始化方法,從而讓沒有聲明過的泛型類型不能直接創(chuàng)建 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()// 沒有聲明過的泛型無法初始化,會產(chǎn)生編譯錯誤
RoutableView()

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

路由檢查

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

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

Objective-C: protocol 遍歷

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

另外,還可以讓 router 同時注冊創(chuàng)建模塊時用到類:

EditorViewRouter.registerView(EditorViewController.self)

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

Swift: 符號遍歷

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

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

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

在還原符號后就是(extension in MyApp):ZRouter.RoutableView.init() -> ZRouter.RoutableView。

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

Swift Runtime 和 ABI

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

Swift 中沒有直接提供class_conformsToProtocol這樣的函數(shù),不過我們可以通過 Swift Runtime 提供的標準函數(shù)和 Swift ABI 中定義的內(nèi)存結(jié)構(gòu),完成同樣的功能。

這部分的實現(xiàn)可以參考代碼:_swift_typeIsTargetType[4]。之后我會寫幾篇文章詳細講解 Swift ABI 的底層內(nèi)容。

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

自動推斷返回值類型

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

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

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

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

這里使用了兩個泛型參數(shù) Destination 和 RouteConfig,分別表示此 router 所管理的模塊類型和路由 config 的類型。__covariant則表示這個泛型支持協(xié)變,也就是子類型可以和父類型一樣使用。

聲明了泛型參數(shù)后,我們可以在方法中的參數(shù)聲明中使用泛型:

@interface ZIKViewRouter<__covariant destination __covariant>RouteConfig: ZIKViewRouteConfiguration *> : NSObject
- (nullable Destination)makeDestination;
- (nullable Destination)destinationWithConfiguration:(RouteConfig)configuration;@end

此時在獲取 router 時,就可以把 protocol 的類型作為 router 的泛型參數(shù):

#define ZIKRouterToView(ViewProtocol) [ZIKViewRouter,ZIKViewRouteConfiguration *> toView](ZIKRoutable(ViewProtocol))

使用ZIKRouterToView(EditorViewProtocol)獲取的 router 類型就是ZIKViewRouter,ZIKViewRouteConfiguration *>。在這個 router 上調(diào)用makeDestination時,返回值的類型就是id,從而實現(xiàn)了完整的類型傳遞。

而在 Swift 中,直接用函數(shù)泛型就能實現(xiàn):

class Router {

static func to(_ routableView: RoutableView<Protocol>) -> ViewRouter<Protocol, ViewRouteConfig>?

}

使用Router.to(RoutableView())時,獲得的 router 類型就是ViewRouter?,在調(diào)用makeDestination時,返回值類型就是EditorViewProtocol,無需手動類型轉(zhuǎn)換。

如果你使用協(xié)議組合,還能同時指明多個類型:

typealias EditorViewProtocol = UIViewController & EditorViewInput

并且在 router 子類中重寫對應方法時,也能用泛型進一步確保類型正確:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ZIKViewRouteConfiguration> {

override func destination(with configuration: ZIKViewRouteConfiguration) -> EditorViewProtocol? {
// 函數(shù)重寫時,參數(shù)類型會和泛型一致,實現(xiàn)時能確保返回值的類型是正確的
return EditorViewController()
}

}

現(xiàn)在我們完成了一套完備的類型檢查機制,而且這套檢查同時支持 OC 和 Swift。

至此,一個基于接口的、類型安全的模塊管理工具就完成了。使用 makeDestination 創(chuàng)建模塊只是最基本的功能,我們可以在父類 router 中進行許多有用的功能擴展,例如依賴注入、界面跳轉(zhuǎn)、接口適配,來更好地進行面向接口的開發(fā)。

模塊解耦

那么在面向接口編程時,我們還需要哪些功能呢?在擴展之前,我們先來討論一下如何使用接口進行模塊解耦,首先從理論層面梳理,再把理論轉(zhuǎn)化為工具。

模塊分類

不同模塊對解耦的要求是不同的。模塊從層級上可以從低到高分類:

? 底層功能模塊,功能單一,有一定通用性,例如各種功能組件(日志、數(shù)據(jù)庫)。底層模塊的主要目的是復用

? 中間層的通用業(yè)務模塊,可以在不同項目中通用。會引用各種底層模塊,以及和其他業(yè)務模塊通信

? 中間層的特殊功能模塊,提供了獨特的功能,沒有通用性,可能會引用一些底層模塊,例如性能監(jiān)控模塊。這種模塊可以被其他模塊直接引用,不用太多考慮模塊間解耦的問題

? 上層的專有業(yè)務模塊,屬于某個項目中獨有的業(yè)務。會引用各種底層模塊,以及和其他業(yè)務模塊通信,和中間層的差別就是上層的解耦要求沒有中間層那么高

什么是解耦

首先明確一下什么才是解耦,梳理這個問題能夠幫助我們明確目標。

解耦的目的基本上就是兩個:提高代碼的可維護性、模塊重用。指導思想就是面向?qū)ο蟮脑O計原則。

解耦也有不同的程度,從低到高,差不多可以分為3層:

1) 模塊間使用抽象接口交互,沒有直接類型耦合,一個模塊內(nèi)部的修改不會影響到另一個模塊 (單一職責、依賴倒置)

2) 模塊可重用,可以被單獨編譯 (接口隔離、依賴倒置、控制反轉(zhuǎn))

3) 模塊可以隨時被另一個提供了相同功能的模塊替換 (開閉原則、依賴倒置、控制反轉(zhuǎn))

第一層:抽象接口,提取依賴關系

第一層解耦,是為了減少不同代碼間的依賴關系,讓代碼更容易維護。例如把類替換為 protocol,隔絕模塊的私有接口,把依賴關系最小化。

解耦的整個過程,就是梳理和管理依賴的過程。因此模塊的內(nèi)聚性越高越好,外部依賴越少越好,這樣維護起來才更簡單。

如果模塊不需要重用,那在這一層基本上就夠了。

第二層:模塊重用,管理模塊間通信

第二層解耦,是把代碼單獨抽離,做到了模塊重用,可以交給不同的成員維護,對模塊間通信提出了更高的要求。模塊需要在接口中聲明外部依賴,去除對特定類型的耦合。

此時影響最大的地方就是模塊間通信的方式,有時候即便是能夠單獨編譯了,也不意味著解耦。例如 URL 路由,只是放棄了編譯檢查,耦合關系還是存在于 URL 字符串中,一方的 URL 改變,其他方的代碼邏輯就會出錯,所以邏輯上仍然是耦合的。因此所有基于某種隱式調(diào)用約定的方案(例如字符串匹配),都只是解除編譯檢查,而不是真正的解耦。

有人說使用 protocol 進行模塊間通信,會導致模塊和 protocol 耦合。這個觀點是錯誤的。protocol 恰恰是把模塊的依賴明確地提取出來,是一種更高效的方法。否則完全用隱式約定來進行通信,沒有編譯器的輔助,一旦模塊的接口名、參數(shù)類型、參數(shù)數(shù)量需要更新,將會非常難以維護。

而且,通過設計模式,是可以解除對特定 protocol 的依賴的,下文將會對此進行講解。

第三層:去除隱式約定

第三層解耦,模塊間做到了真正的解耦,只要兩個模塊提供了相同的功能,就可以無縫替換,并且調(diào)用方無需任何修改。被替換的模塊只需要提供相同功能的接口,通過適配器對接即可,沒有其他任何限制,不存在任何其他的隱式調(diào)用約定。

一般有這種解耦要求的,都是那些跨項目的通用模塊,而項目內(nèi)專有的業(yè)務模塊則沒有這么高的要求。不過那些跨多端的模塊和遠程模塊無法做到這樣的解耦,因為跨多端時沒有統(tǒng)一的定義接口的方式,因此只能通過隱式約定或者網(wǎng)絡協(xié)議定義接口,例如 URL 路由。

總的來說,解耦的過程就是職責分離、依賴管理(依賴聲明和注入)、模塊通信 這三大部分。

模塊重用

要做到模塊重用,模塊需要盡量減少外部依賴,并且把依賴提取出來,體現(xiàn)到模塊的接口上,讓調(diào)用者主動注入。同時,把模塊的各種事件也提取出來,讓調(diào)用者進行處理。

這樣一來,模塊就只需要負責自身的邏輯,不需要關心調(diào)用者如何使用模塊。那些每個應用各自專有的應用層邏輯也就從模塊中分離出來了。

因此,要想做好模塊解耦,管理好依賴是非常重要的。而 protocol 接口就是管理依賴的最高效的方式。

依賴管理

依賴,就是模塊中用到的外部數(shù)據(jù)和外部模塊。接下來討論如何使用 protocol 管理依賴,并且演示如何用 router 實現(xiàn)。

依賴注入

先來復習一下依賴注入的概念。依賴注入和依賴查找是實現(xiàn)控制反轉(zhuǎn)思想的具體方式。

控制反轉(zhuǎn)是將對象依賴的獲取從主動變?yōu)楸粍?#xff0c;從對象內(nèi)部直接引用并獲取依賴,變?yōu)橛赏獠肯驅(qū)ο筇峁ο笏蟮囊蕾?#xff0c;把不屬于自己的職責移交出去,從而讓對象和其依賴解耦。此時控制流的主動權從內(nèi)部轉(zhuǎn)移到了外部,因此稱為控制反轉(zhuǎn)。

依賴注入就是指外部向?qū)ο髠魅胍蕾嚒?/p>

一個類 A 在接口中體現(xiàn)出內(nèi)部需要用到的一些依賴(例如內(nèi)部需要用到類B的實例),從而讓使用者從外部注入這些依賴,而不是在類內(nèi)部直接引用依賴并創(chuàng)建類 B。依賴可以用 protocol 的方式聲明,這樣就可以使類 A 和所使用的依賴類 B 進行解耦。

分離模塊創(chuàng)建和配置

那么如何用 router 進行依賴注入呢?

模塊創(chuàng)建了實例后,經(jīng)常還需要進行一些配置。模塊管理工具應該從設計上提供配置功能。

最簡單的方式,就是在destinationWithConfiguration:中創(chuàng)建 destination 時進行配置。但是我們還可以更進一步,把 destination 的創(chuàng)建和配置分離開。分離之后,router 就可以單獨提供配置功能,去配置那些不是由 router 創(chuàng)建的 destination,例如 storyboard 中創(chuàng)建的 view、各種接口回調(diào)中返回的實例對象。這樣就可以覆蓋更多現(xiàn)存的使用場景,減少代碼修改。

Prepare Destination

可以在 router 子類中的prepareDestination:configuration:中進行模塊配置,也就是依賴注入,而模塊的調(diào)用者無需關心這部分依賴是如何配置的:

// 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 父類中調(diào)用模塊配置方法
router.prepareDestination(destination, configuration: router.configuration)
}
return destination
}

// 模塊創(chuàng)建,讓子類重寫
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
}
// 配置模塊,注入靜態(tài)依賴
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依賴
destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
// 其他配置
destination.title = "默認標題"
}
}

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

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

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

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

當依賴是可選的,并不是創(chuàng)建對象所必需的,可以用屬性注入和方法注入。

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

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)
}
}

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

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

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

模塊間參數(shù)傳遞

在執(zhí)行路由操作的同時,調(diào)用者也可以用PersonType動態(tài)地注入依賴,也就是向模塊傳參。

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

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

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

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

必需依賴:工廠方法

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

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

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

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

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

那么如何用 router 傳遞必需參數(shù)呢?

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

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

在 router 中使用自定義 configuration:

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

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

然后把PersonTypeFactory協(xié)議和 router 進行注冊:

PersonRouter.register(RoutableServiceModule())

就可以用PersonTypeFactory獲取模塊了:

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

用泛型代替 configuration 子類

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

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

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

在 router 中使用自定義 configuration:

class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {

// 重寫 defaultRouteConfiguration,使用自定義 configuration
override class func defaultRouteConfiguration() -> PerformRouteConfig {
let config = ServiceMakeableConfiguration<PersonType, (String) -> PersonType>({ _ in})
// 設置工廠方法,讓調(diào)用者使用
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> {
// 使用工廠方法創(chuàng)建的 destination
return config.makedDestination
}
return nil
}
}

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

}

避免接口污染

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

protocol EditorViewModuleInput {
// 工廠方法,聲明了參數(shù) note,返回 EditorViewInput 類型的 destination
func makeDestinationWith(_ note: Note) -> EditorViewInput?
}
class EditorViewRouter: ZIKViewRouter<EditorViewInput, ViewRouteConfig> {

// 重寫 defaultRouteConfiguration,使用自定義 configuration
override class func defaultRouteConfiguration() -> ViewRouteConfig {
let config = ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput>({ _ in})
// 設置工廠方法,讓調(diào)用者使用
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 傳遞給數(shù)據(jù)管理者,view 不接觸 model
interactor.note = note
return view
}

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

就可以用EditorViewModuleInput獲取模塊了:

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

依賴查找

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

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

代碼如下:

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

循環(huán)依賴

使用依賴注入時,有些特殊情況需要處理,例如循環(huán)依賴的無限遞歸問題。
循環(huán)依賴是指兩個對象互相依賴。

在 router 內(nèi)部動態(tài)注入依賴時,如果注入的依賴同時依賴于被注入的對象,則必須在 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!
}
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 內(nèi)部再去尋找 parent 依賴,導致循環(huán)
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 內(nèi)部再去尋找 child 依賴,導致循環(huán)
parent.child = destination
})
destination.parent = parent
}
}

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

模塊適配器

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

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

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

required protocol 和 provided protocol

你可以為同一個 router 注冊多個 protocol。

根據(jù)依賴關系,接口可以分為required protocol和provided protocol。模塊本身提供的接口是provided protocol,模塊的調(diào)用者需要使用的接口是required protocol。

required protocol是provided protocol的子集,調(diào)用者只需要聲明自己用到的那些接口,不必引入整個provided protocol,這樣可以讓模塊間的耦合進一步減少。

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

那么如何實施Required Interface和Provided Interface?從架構(gòu)分層上看,所有的模塊都是依附于一個更上層的宿主 app 環(huán)境存在的,應該由使用這些模塊的宿主 app 在一個 adapter 里進行接口適配,從而使得調(diào)用者可以繼續(xù)在內(nèi)部使用required protocol,adapter 負責把required protocol和修改后的provided protocol進行適配。整個過程模塊都無感知。

這時候,調(diào)用者中定義的required protocol就相當于是在聲明自己所依賴的外部模塊。

為provided模塊添加required protocol

模塊適配的工作全部由模塊的使用和裝配者 App Context 完成,最少時只需要兩行代碼。

例如,某個模塊需要展示一個登陸界面,而且這個登陸界面可以顯示一段自定義的提示語。

調(diào)用者模塊示例:

// 調(diào)用者中聲明的依賴接口,表明自身依賴一個登陸界面
protocol RequiredLoginViewInput {
var message: String? { get set } //顯示在登陸界面上的自定義提示語
}

// 調(diào)用者中調(diào)用 login 模塊
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
destination.message = "請登錄"
})

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

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

適配的代碼由宿主 app 實現(xiàn),讓登陸界面支持?

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

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

// 如果可以獲取到 router 類,可以直接為 router 添加 RequiredLoginViewInput
LoginViewRouter.register(RoutableView())
// 如果不能得到對應模塊的 router,可以用 adapter 進行轉(zhuǎn)發(fā)
ZIKViewRouteAdapter.register(adapter: RoutableView(), forAdaptee: RoutableView())

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

調(diào)用者模塊示例:

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

接口適配

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

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

適配時需要進行接口轉(zhuǎn)發(fā),讓登陸界面支持 RequiredLoginViewInput:

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

用中介者轉(zhuǎn)發(fā)接口

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

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

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

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

相同方法有不同參數(shù)類型時,可以用一個新的 router 代替真正的 router,在新的 router 里插入一個中介者,負責轉(zhuǎn)發(fā)接口:

class ReqiredLoginViewRouter: ProvidedLoginViewRouter {

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

對于普通OC類,proxy 可以用 NSProxy 來實現(xiàn)。對于 UIKit 中的那些復雜的 UI 類,或者 Swift 類,可以用子類,然后在子類中重寫方法,進行模塊適配。

聲明式依賴

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

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

模塊提供默認的依賴配置

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

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

#if USE_DEFAULT_DEPENDENCY

import ProvidedLoginModule

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

extension ProvidedLoginViewController: RequiredLoginViewInput {

}

#endif

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

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) USE_DEFAULT_DEPENDENCY=1

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

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

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

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

模塊化

區(qū)分了required protocol和provided protocol后,就可以實現(xiàn)真正的模塊化。在調(diào)用者聲明了所需要的required protocol后,被調(diào)用模塊就可以隨時被替換成另一個相同功能的模塊。

參考 demo 中的ZIKLoginModule示例模塊,登錄模塊依賴于一個彈窗模塊,而這個彈窗模塊在ZIKRouterDemo和ZIKRouterDemo-macOS中是不同的,而在切換彈窗模塊時,登錄模塊中的代碼不需要做任何改變。

使用 adapter 的規(guī)范

一般來說,并不需要立即把所有的 protocol 都分離為required protocol和provided protocol。調(diào)用模塊和目的模塊可以暫時共用 protocol,或者只是簡單地改個名字,讓required protocol作為provided protocol的子集,在第一次需要替換模塊的時候再用 category、extension、proxy、subclass 等技術進行接口適配。

接口適配也不能濫用,因為成本比較高,而且并非所有的接口都能適配,例如同步接口和異步接口就難以適配。

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

? 如果依賴的是提供特定功能的模塊,沒有通用性,直接引用類即可

? 如果是依賴某些簡單的通用模塊(例如日志模塊),可以在模塊的接口上把依賴交給外部來設置,例如 block 的形式

? 大部分需要解耦的模塊都是需要重用的業(yè)務模塊,如果你的模塊不需要重用,并且也不需要分工開發(fā),直接引用對應類即可

? 大部分情況下建議共用 protocol,或者讓required protocol作為provided protocol的子集,接口名保持一致

? 只有在你的業(yè)務模塊的確允許使用者使用不同的依賴模塊時,才進行多個接口間的適配。例如需要跨平臺的模塊,例如登錄界面模塊允許不同的 app 使用不同的登陸 service 模塊

通過required protocol和provided protocol,我們就實現(xiàn)了模塊間的完全解耦。

模塊間通信

模塊間通信有多種方式,解耦程度也各有不同。這里只討論接口交互的方式。

控制流 input 和 output

模塊的對外接口可以分為 input 和 output。兩者的區(qū)別主要是控制流的主動權歸屬不同。

Input 是由外部主動調(diào)用的接口,控制流的發(fā)起者在外部,例如外部調(diào)用 view 的 UI 修改接口。

Output 是模塊內(nèi)部主動調(diào)用外部實現(xiàn)的接口,控制流的發(fā)起者在內(nèi)部,需要外部實現(xiàn) output 所要求的方法。例如輸出 UI 事件、事件回調(diào)、獲取外部的 dataSource。iOS 中常用的 delegate 模式,也是一種 output。

設置 input 和 output

模塊設計好 input 和 output,然后在模塊創(chuàng)建的時候,設置好模塊之間的 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 架構(gòu),此時一個 view controller 使用了 child view controller,那多個模塊的 view model 和 interactor 之間如何交互?子模塊由誰初始化、由誰管理?

有些方案是直接在父 view model 里創(chuàng)建和使用子 view model,但是這樣就導致了 view 的實現(xiàn)方式影響了view model 的實現(xiàn),如果父 view 里替換使用了另一個子 view,那父 view model 里的代碼也需要修改。

子模塊的來源

子模塊的來源有:

? 父 view 引用了一個封裝好的子 view 控件,連帶著引入了子 view 的整個 MVVM 或者 VIPER 模塊

? View model 或者 interactor 里使用了一個 Service

通信方式

子 view 可能是一個 UIView,也可能是一個 Child UIViewController。因此子 view 有可能需要向外部請求數(shù)據(jù),也可能獨立完成所有任務,不需要依賴父模塊。

如果子 view 可以獨立,那在子模塊里不會出現(xiàn)和父模塊交互的邏輯,只有把一些事件通過 output 傳遞出去的接口。這時只需要把子 view 的 input 接口封裝在父 view 的 input 接口里即可,父 view model / presenter / interactor 是不知道父 view 提供的這幾個接口是通過子 view 實現(xiàn)的。

如果父模塊需要調(diào)用子模塊的業(yè)務接口,或接收子模塊的數(shù)據(jù)或業(yè)務事件,并且不想影響 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
// 此時子模塊是把這些內(nèi)部組件作為業(yè)務 input 開放給了外部
self.viewModel.textService = destination.viewModel
destination.viewModel.output = self.viewModel
}

addChildViewController(textViewController)
view.addSubview(textViewController.view)
textViewController.didMove(toParentViewController: self)
}
}

子模塊:

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

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

Output 的適配

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

例如這樣一對 required-provided protocol:

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

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

由于 output 的實現(xiàn)者不是固定的,因此無法讓所有的 output 類都同時適配RequiredEditorViewOutput和ProvidedEditorViewOutput。此時建議直接使用對應的 protocol,不使用 required-provided 模式。

如果你仍然想要使用 required-provided 模式,那就需要用工廠模式來傳遞 output ,在內(nèi)部用 proxy 進行適配。

實際模塊的 router:

protocol ProvidedEditorViewModuleInput {
var makeDestinationWith(_ output: ProvidedEditorViewOutput?) -> ProvidedEditorViewInput? { get set }
}

class ProvidedEditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {

override class func registerRoutableDestination() {
register(RoutableViewModule<ProvidedEditorViewModuleInput>())
}

override class func defaultRouteConfiguration() -> ViewRouteConfig {
let config = ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?) -> ProvidedViewInput?>({ _ in})
config.makeDestinationWith = { [unowned config] output in
// 設置 output
let viewModel = EditorViewModel(output: output)
config.makedDestination = EditorViewController(viewModel: viewModel)
return config.makedDestination
}
return config
}

override func destination(with configuration: ViewRouteConfig) -> EditorViewController? {
if let config = configuration as? ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?) {
return config.makedDestination
}
return nil
}
}

適配代碼:

protocol 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 轉(zhuǎn)為 ProvidedEditorViewOutput
let providedOutput = EditorOutputProxy(forwarding: requiredOutput)
return makeDestinationWith(providedOutput)
}
return config
}
}

class EditorOutputProxy: ProvidedEditorViewOutput {
let forwarding: RequiredEditorViewOutput
// 實現(xiàn) ProvidedEditorViewOutput,轉(zhuǎn)發(fā)給 forwarding
}

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

功能擴展

總結(jié)完使用接口進行模塊解耦和依賴管理的方法,我們可以進一步對 router 進行擴展了。上面使用 makeDestination 創(chuàng)建模塊是最基本的功能,使用 router 子類后,我們可以進行許多有用的功能擴展,這里給出一些示范。

自動注冊

編寫 router 代碼時,需要注冊 router 和 protocol 。在 OC 中可以在 +load 方法中注冊,但是 Swift 里已經(jīng)不能使用 +load 方法,而且分散在 +load 中的注冊代碼也不好管理。BeeHive 中通過宏定義和__attribute((used, section("__DATA,""BeehiveServices"""))),把注冊信息添加到了 mach-O 中的自定義區(qū)域,然后在啟動時讀取并自動注冊,可惜這種方式在 Swift 中也無法使用了。

我們可以把注冊代碼寫在 router 的+registerRoutableDestination方法里,然后逐個調(diào)用每個 router 類的+registerRoutableDestination方法即可。還可以更進一步,用 runtime 技術遍歷 mach-O 中的__DATA,__objc_classlist區(qū)域的類列表,獲取所有的 router 類,自動調(diào)用所有的+registerRoutableDestination方法。

把注冊代碼統(tǒng)一管理之后,如果不想使用自動注冊,也能隨時切換為手動注冊。

// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter {

override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView())
}
}
復制代碼Objective-C Sample
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {[self registerView:[EditorViewController class]];[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}@end

封裝界面跳轉(zhuǎn)

iOS 中模塊間耦合的原因之一,就是界面跳轉(zhuǎn)的邏輯是通過 UIViewController 進行的,跳轉(zhuǎn)功能被限制在了 view controller 上,導致數(shù)據(jù)流常常都繞不開 view 層。要想更好地管理跳轉(zhuǎn)邏輯,就需要進行封裝。

封裝界面跳轉(zhuǎn)可以屏蔽 UIKit 的細節(jié),此時界面跳轉(zhuǎn)的代碼就可以放在非 view 層(例如 presenter、view model、interactor、service),并且能夠跨平臺,也能輕易地通過配置切換跳轉(zhuǎn)方式。

如果是普通的模塊,就用ZIKServiceRouter,而如果是界面模塊,例如 UIViewController 和 UIView,就可以用ZIKViewRouter,在其中封裝了界面跳轉(zhuǎn)功能。

封裝界面跳轉(zhuǎn)后,使用方式如下:

class TestViewController: UIViewController {

//直接跳轉(zhuǎn)到 editor 界面
func showEditor() {
Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}

//跳轉(zhuǎn)到 editor 界面,跳轉(zhuǎn)前用 protocol 配置界面
func prepareAndShowEditor() {
Router.perform(
to: RoutableView<EditorViewProtocol>(),
path: .push(from: self),
preparation: { destination in
// 跳轉(zhuǎn)前進行配置
// destination 自動推斷為 EditorViewProtocol
})
}
}

可以用 ViewRoutePath 一鍵切換不同的跳轉(zhuǎn)方式:

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)
}

而且在界面跳轉(zhuǎn)后,還可以根據(jù)跳轉(zhuǎn)時的跳轉(zhuǎn)方式,一鍵回退界面,無需再手動區(qū)分 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 執(zhí)行 pop 操作,移除界面
func removeEditor() {
guard let router = router, router.canRemove else {
return
}
router.removeRoute()
router = nil
}
}

自定義跳轉(zhuǎn)

有些界面的跳轉(zhuǎn)方式很特殊,例如 tabbar 上的界面,需要通過切換 tabbar item 來進行。也有的界面有自定義的跳轉(zhuǎn)動畫,此時可以在 router 子類中重寫對應方法,進行自定義跳轉(zhuǎn)。

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()
// 自定義跳轉(zhuǎn)
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)
// 移除自定義跳轉(zhuǎn)
CustomAnimator.dismiss(destination) {
self.endRemoveRouteWithSuccess(onDestination: destination, fromSource: source)
}
}

override class func supportedRouteTypes() -> ZIKViewRouteTypeMask {
return [.custom, .viewControllerDefault]
}
}

支持 storyboard

很多項目使用了 storyboard,在進行模塊化時,肯定不能要求所有使用 storyboard 的模塊都改為使用代碼。因此我們可以 hook 一些 storyboard 相關的方法,例如-prepareSegue:sender:,在其中調(diào)用prepareDestination:configuring:即可。

URL 路由

雖然之前列出了 URL 路由的許多缺點,但是如果你的模塊需要從 h5 界面調(diào)用,例如電商 app 需要實現(xiàn)跨平臺的動態(tài)路由規(guī)則,那么 URL 路由就是最佳的方案。

但是我們并不想為了實現(xiàn) URL 路由,使用另一套框架再重新封裝一次模塊。只需要在 router 上擴展 URL 路由的功能,即可同時用接口和 URL 管理模塊。

你可以給 router 注冊 url:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ViewRouteConfig> {
override class func registerRoutableDestination() {
// 注冊 url
registerURLPattern("app://editor/:title")
}
}

之后就可以用相應的 url 獲取 router:

ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: 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
}

每個 router 子類還能各自對 url 進行進一步處理,例如處理 url 中的參數(shù)、通過 url 執(zhí)行對應方法、執(zhí)行路由后發(fā)送返回值給調(diào)用者等。

每個項目對 URL 路由的需求都不一樣,基于 ZIKRouter 強大的可擴展性,你也可以按照項目需求實現(xiàn)自己的 URL 路由規(guī)則。

用 router 對象代替 router 子類

除了創(chuàng)建 router 子類,也可以使用通用的 router 實例對象,在每個對象的 block 屬性中提供和 router 子類一樣的功能,因此不必擔心類過多的問題。原理就和用泛型 configuration 代替 configuration 子類一樣。

ZIKViewRoute 對象通過 block 屬性實現(xiàn)子類重寫的效果,代碼可以用鏈式調(diào)用:

ZIKViewRoute
.make(withDestination: EditorViewController.self, makeDestination: ({ (config, router) -> EditorViewController? inreturn EditorViewController()
}))
.prepareDestination({ (destination, config, router) in
}).didFinishPrepareDestination({ (destination, config, router) in
})
.register(RoutableView())

簡化 router 實現(xiàn)

基于 ZIKViewRoute 對象實現(xiàn)的 router,可以進一步簡化 router 的實現(xiàn)代碼。

如果你的類很簡單,并不需要用到 router 子類,直接一行代碼注冊類即可:

ZIKAnyViewRouter.register(RoutableView(), forMakingView: EditorViewController.self)

或者用 block 自定義創(chuàng)建對象的方式:

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

或者指定用 C 函數(shù)創(chuàng)建對象:

function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
return EditorViewController()
}

ZIKAnyViewRouter.register(RoutableView(),
forMakingView: EditorViewController.self, making: makeEditorViewController)

事件處理

有時候模塊需要處理一些系統(tǒng)事件或者 app 的自定義事件,此時可以讓 router 子類實現(xiàn),再進行遍歷分發(fā)。

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)
}
}
}

}

單元測試

借助于使用接口管理依賴的方案,我們在對模塊進行單元測試時,可以自由配置 mock 依賴,而且無需 hook 模塊內(nèi)部的代碼。

例如這樣一個依賴于網(wǎng)絡模塊的登陸模塊:

// 登錄模塊
class LoginService {

func login(account: String, password: String, completion: (Result) -> Void) {
// 內(nèi)部使用 RequiredNetServiceInput 進行網(wǎng)絡訪問
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() {}
}

在編寫單元測試時,不需要引入真實的網(wǎng)絡模塊,可以提供一個自定義的 mock 網(wǎng)絡模塊:

class MockNetService: RequiredNetServiceInput {
func POST(request: Request, completion: (Result) {
completion(.success)
}
}
// 注冊 mock 依賴
ZIKAnyServiceRouter.register(RoutableService(),
forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? inreturn MockNetService()
}

對于那些沒有接口交互的外部依賴,例如只是簡單的跳轉(zhuǎn)到對應界面,則只需注冊一個空白的 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)}})
}

}

使用接口管理依賴,可以更容易 mock,剝除外部依賴對測試的影響,讓單元測試更穩(wěn)定。

接口版本管理

使用接口管理模塊時,還有一個問題需要注意。接口是會隨著模塊更新而變化的,這個接口已經(jīng)被很多外部使用了,要如何減少接口變化產(chǎn)生的影響?

此時需要區(qū)分新接口和舊接口,區(qū)分版本,推出新接口的同時,保留舊接口,并將舊接口標記為廢棄。這樣使用者就可以暫時使用舊接口,漸進式地修改代碼。

這部分可以參考 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));

接口已經(jīng)無效:

// Swift
@available(iOS, unavailable)
// Objective-C
NS_UNAVAILABLE

最終形態(tài)

最后,一個 router 的最終形態(tài)就是下面這樣:

// 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 中的參數(shù)
}

// 子類重寫,創(chuàng)建模塊
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}

// 配置模塊,注入靜態(tài)依賴
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依賴
destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
// 其他配置
// 處理來自 url 的參數(shù)
if let title = configuration.userInfo["title"] as? String {
destination.title = title
} else {
destination.title = "默認標題"
}
}

// 事件處理
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}

基于接口進行解耦的優(yōu)勢

我們可以看到基于接口管理模塊的優(yōu)勢:

? 依賴編譯檢查,實現(xiàn)嚴格的類型安全

? 依賴編譯檢查,減少重構(gòu)時的成本

? 通過接口明確聲明模塊所需的依賴,允許外部進行依賴注入

? 保持動態(tài)特性的同時,進行路由檢查,避免使用不存在的路由模塊

? 利用接口,區(qū)分 required protocol 和 provided protocol,進行明確的模塊適配,實現(xiàn)徹底解耦

回過頭看之前的 8 個解耦指標,ZIKRouter 已經(jīng)完全滿足。而 router 提供的多種模塊管理方式(makeDestination、prepareDestination、依賴注入、頁面跳轉(zhuǎn)、storyboard 支持),能夠覆蓋大多數(shù)現(xiàn)有的場景,從而實現(xiàn)漸進式的模塊化,減輕重構(gòu)現(xiàn)有代碼的成本。

參考

[1]https://juejin.im/post/59cb629c5188253cb5016322?
[2]https://github.com/Zuikyo/ZIKRouter?
[3]https://stackoverflow.com/questions/42662028/are-performselector-and-respondstoselector-banned-by-app-store?
[4]https://github.com/Zuikyo/ZIKRouter/blob/acb923bcdd09c65672977b5a20f7c527e459ead5/ZIKRouter/Utilities/ZIKRouterRuntimeDebug.h#L41


推薦閱讀??聊聊AppDelegate解耦??iOS 原生 App 是怎么 deselectRow 的??動手制作一個簡易的iOS動態(tài)執(zhí)行器??iOS 流量監(jiān)控分析

總結(jié)

以上是生活随笔為你收集整理的swift 组件化_打造完备的iOS组件化方案:如何面向接口进行模块解耦?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 日韩精品视频免费 | 三级理论电影 | 国产爆乳无码一区二区麻豆 | 精品人妻少妇AV无码专区 | 偷拍视频一区 | 免费久久精品视频 | 黄色在线免费观看视频 | 中文字幕第11页 | 一区二区片 | 一区二区三区四区影院 | 精品久久久久久久久久久久久 | www.日韩欧美 | 欧美老女人性生活 | 久久久精品人妻一区二区三区 | 亚洲色成人www永久在线观看 | 黄色视屏免费 | 狠狠躁狠狠躁视频专区 | 国产成人免费在线观看 | 成人av网站在线播放 | 91毛片网站 | 国产91精品在线观看 | 自拍视频一区 | 国产美女激情 | 久久国产在线观看 | 久久国产露脸精品国产 | 精品国产乱码久久久久久蜜臀网站 | 一区二区三区四区欧美 | 99国产精品国产免费观看 | 女王脚交玉足榨精调教 | 男人的天堂毛片 | 香港日本韩国三级网站 | 亚洲日本va中文字幕 | 特级精品毛片免费观看 | 麻豆极品 | 少妇高潮一区二区三区99小说 | 日本少妇毛茸茸 | 国产美女免费网站 | 在线免费福利 | 日韩av在线中文字幕 | 中国浓毛少妇毛茸茸 | 国产极品美女高潮无套嗷嗷叫酒店 | 精品一区二区免费视频 | 成人欧美一区二区三区黑人孕妇 | 日韩一级片av | 午夜男人天堂 | 国产精品视频播放 | 久久精品视频偷拍 | 久久久精品99 | 无码国产69精品久久久久网站 | 午夜中文字幕 | 寂寞人妻瑜伽被教练日 | 日韩网红少妇无码视频香港 | 日本在线视频www | 国产精品毛片视频 | 涩涩av| 欧美绿帽交换xxx | 国产伦精品一区二区三区四区免费 | 亚洲羞羞 | 久草成人网| av网站在线免费看 | 91视频观看 | 超碰在线网站 | 精品久久久中文字幕 | 我要看一级片 | 国产成人精品一区 | 国产在线视频第一页 | 国产精品久久久久久亚洲调教 | 欧美一级一区 | 亚洲一区美女 | 老狼影院伦理片 | 超碰在线观看免费 | 欧美日韩亚洲精品内裤 | 凹凸福利视频 | 国产喷水吹潮视频www | 色xxxx| 午夜影院久久久 | 91久久精品国产91久久 | 久久久国际精品 | 精品黑人一区二区三区久久 | 在线观看亚洲区 | 亚洲黄色免费在线观看 | 久久婷婷综合国产 | 182av| 久久久99久久 | 少妇激情一区二区三区视频 | 成人污在线观看 | 日韩视频中文字幕在线观看 | 性少妇bbw张开 | 免费人成网 | 久久精品国产99久久不卡 | 午夜精品一区二区三区在线观看 | 欧美在线aa | 在线免费小电影 | 婷婷av一区二区三区 | 在线日韩三级 | 成人天堂噜噜噜 | 人妻丰满熟妇aⅴ无码 | 福利色导航 | 美女被日网站 |