关于组件化的探讨已经有很多了,在以前的文章iOS VIPER架构实践(三):面向接口的路由设计中,综合比较了各类方案后,我倾向于使用面向接口的方式进行组件化。html
这是一篇从代码层面讲解模块解耦的文章,会全方位地展现如何实践面向接口的思想,尽可能全面地探讨在模块管理和解耦的过程当中,须要考虑到的各类问题,而且给出实际的解决方案,以及对应的模块管理开源工具:ZIKRouter。你也能够根据本文的内容改造本身现有的方案,即便你的项目不进行组件化,也能够参考本文进行代码解耦。ios
文章主要内容:git
将模块单独抽离、分层,并制定模块间通讯的方式,从而实现解耦,以及适应团队开发。github
主要有4个缘由:objective-c
当项目愈来愈大的时候,各个模块之间若是是直接互相引用,就会产生许多耦合,致使接口滥用,当某天须要进行修改时,就会牵一发而动全身,难以维护。数据库
问题主要体如今:编程
因此须要减小模块之间的耦合,用更规范的方式进行模块间交互。这就是组件化,也能够叫作模块化。swift
组件化也不是必须的,有些状况下并不须要组件化:设计模式
组件化也是有必定成本的,你须要花时间设计接口,分离代码,因此并非全部的模块都须要组件化。api
不过,当你发现这几个迹象时,就须要考虑组件化了:
决定了要开始组件化之路后,就须要思考咱们的目标了。一个组件化方案须要达到怎样的效果呢?我在这里给出8个理想状况下的指标:
前4条用于衡量一个模块是否真正解耦,后4条用于衡量在项目实践中的易用程度。最后一条必须支持 Swift,是由于 Swift 是一个必然的趋势,若是你的方案不支持 Swift,说明这个方案在未来的某个时刻一定要改进改变,而到时候全部基于这个方案实现的模块都会受到影响。
基于这8个指标,咱们就能在必定程度上对咱们的方案作出衡量了。
如今主要有3种组件化方案:URL 路由、target-action、protocol 匹配。
接下来咱们就比较一下这几种组件化方案,看看它们各有什么优缺点。这部分在以前的文章中已经探讨过,这里再从新比较一次,补充一些细节。必需要先说明的是,没有一个完美的方案能知足全部场景下的需求,须要根据每一个项目的需求选择最适合的方案。
目前 iOS 上绝大部分的路由工具都是基于 URL 匹配的,或者是根据命名约定,用 runtime 方法进行动态调用。
这些动态化的方案的优势是实现简单,缺点是须要维护字符串表,或者依赖于命名约定,没法在编译时暴露出全部问题,须要在运行时才能发现错误。
代码示例:
// 注册某个URL
[URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {
UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];
return editorViewController;
}];
复制代码
// 调用路由
[URLRouter openURL:@"app://editor/?debug=true" completion:^(NSDictionary *info) {
}];
复制代码
URL router 的优势:
URL router 的缺点:
若是用上面的8个指标来衡量,URL 路由只能知足"支持模块单独编译"、"支持 OC 和 Swift"两条。它的解耦程度很是通常。
全部基于字符串的解耦方案其实均可以说是伪解耦,它们只是放弃了编译依赖,可是当代码变化以后,即使可以编译运行,逻辑仍然是错误的。
例如修改了模块定义时的 URL:
// 注册某个URL
[URLRouter registerURL:@"app://editorView" handler:^(NSDictionary *userInfo) {
...
}];
复制代码
那么调用者的 URL 也必须修改,代码仍然是有耦合的,只不过此时编译器没法检查而已。这会致使维护更加困难,一旦 URL 中的参数有了增减,或者决定替换为另外一个模块,参数命名有了变化,几乎没有高效的方式来重构代码。可使用宏定义来管理字符串,不过这要求全部模块都使用同一个头文件,而且也没法解决参数类型和数量变化的问题。
URL 路由适合用来作远程模块的网络协议交互,而在管理本地模块时,最大的甚至是惟一的优点,就是适合常常跨多端运营活动的 app,由于能够由运营人员统一管理多平台的路由规则。
改进 URL 路由的方式,就是避免使用字符串,经过接口管理模块。
参数能够经过 protocol 直接传递,可以利用编译器检查参数类型,而且在 ZIKRouter 中,能经过路由声明和编译检查,保证所使用的模块必定存在。在为模块建立路由时,也无需修改模块的代码。
可是必需要认可的是,尽管 URL 路由缺点多多,但它在跨平台路由管理上的确是最适合的方案。所以 ZIKRouter 也对 URL 路由作出了支持,在用 protocol 管理的同时,能够经过字符串匹配 router,也能和其余 URL router 框架对接。
有一些模块管理工具基于 Objective-C 的 runtime、category 特性动态获取模块。例如经过NSClassFromString
获取类并建立实例,经过performSelector:
NSInvocation
动态调用方法。
例如基于 target-action 模式的设计,大体是利用 category 为路由工具添加新接口,在接口中经过字符串获取对应的类,再用 runtime 建立实例,动态调用实例的方法。
示例代码:
// 模块管理者,提供了动态调用 target-action 的基本功能
@interface Mediator : NSObject
+ (instancetype)sharedInstance;
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
@end
复制代码
// 在 category 中定义新接口
@interface Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController;
@end
@implementation Mediator (ModuleActions)
- (UIViewController *)Mediator_editorViewController {
// 使用字符串硬编码,经过 runtime 动态建立 Target_Editor,并调用 Action_viewController:
UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}];
return viewController;
}
@end
// 调用者经过 Mediator 的接口调用模块
UIViewController *editor = [[Mediator sharedInstance] Mediator_editorViewController];
复制代码
// 模块提供者提供 target-action 的调用方式
@interface Target_Editor : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
@implementation Target_Editor
- (UIViewController *)Action_viewController:(NSDictionary *)params {
// 参数经过字典传递,没法保证类型安全
EditorViewController *viewController = [[EditorViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
复制代码
优势:
缺点:
字典传参时没法保证参数的数量和类型,只能依赖调用约定,就和字符串传参同样,一旦某一方作出修改,另外一方也必须修改。
相比于 URL 路由,target-action 经过 category 的接口把字符串管理的问题缩小到了 mediator 内部,不过并无彻底消除,并且在其余方面仍然有不少改进空间。上面的8个指标中其实只能知足第2个"支持模块单独编译",另外在和接口相关的第三、五、6点上,比 URL 路由要有改善。
Target-Action 方案最大的优势就是整个方案实现轻量,而且也必定程度上明确了模块的接口。只是这些接口都须要经过 Target-Action 封装一次,而且每一个模块都要建立一个 target 类,既然如此,直接用 protocol 进行接口管理会更加简单。
ZIKRouter 避免使用 runtime 获取和调用模块,所以能够适配 OC 和 swift。同时,基于 protocol 匹配的方式,避免引入字符串硬编码,可以更好地管理模块,也避免了字典传参。
有一些模块管理工具或者依赖注入工具,也实现了基于接口的管理方式。实现思路是将 protocol 和对应的类进行字典匹配,以后就能够用 protocol 获取 class,再动态建立实例。
BeeHive 示例代码:
// 注册模块 (protocol-class 匹配)
[[BeeHive shareInstance] registerService:@protocol(EditorViewProtocol) service:[EditorViewController class]];
复制代码
// 获取模块 (用 runtime 建立 EditorViewController 实例)
id<EditorViewProtocol> editor = [[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)];
复制代码
优势:
缺点:
相比直接 protocol-class 匹配的方式,protocol-block 的方式更加易用。例如 Swinject。
Swinject 示例代码:
let container = Container()
// 注册模块
container.register(EditorViewProtocol.self) { _ in
return EditorViewController()
}
// 获取模块
let editor = container.resolve(EditorViewProtocol.self)!
复制代码
BeeHive 这种方式和 ZIKRouter 的思路相似,可是全部的模块在注册后,都是由 BeeHive 单例来建立,使用场景十分有限,例如不支持纯 Swift 类型,不支持使用自定义初始化方法以及额外的依赖注入。
ZIKRouter 进行了进一步的改进,并非直接对 protocol 和 class 进行匹配,而是将 protocol 和 router 子类或者 router 对象进行匹配,在 router 子类中再提供建立模块的实例的方式。这时,模块的建立职责就从 BeeHive 单例上转到了每一个单独的 router 上,从集约型变成了离散型,扩展性进一步提高。
变成 protocol-router 匹配后,代码将会变成这样:
一个 router 父类提供基础的方法:
class ZIKViewRouter: NSObject {
...
// 获取模块
public class func makeDestination -> Any? {
let router = self.init(with: ViewRouteConfig())
return router.destination(with: router.configuration)
}
// 让子类重写
public func destination(with configuration: ViewRouteConfig) -> Any? {
return nil
}
}
复制代码
@interface ZIKViewRouter: NSObject
@end
@implementation ZIKViewRouter
...
// 获取模块
+ (id)makeDestination {
ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
return [router destinationWithConfiguration:router.configuration];
}
// 让子类重写
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return nil;
}
@end
复制代码
每一个模块各自编写本身的 router 子类:
// editor 模块的 router
class EditorViewRouter: ZIKViewRouter {
// 子类重写,建立模块
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}
}
复制代码
// editor 模块的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
// 子类重写,建立模块
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
@end
复制代码
把 protocol 和 router 类进行注册绑定:
EditorViewRouter.register(RoutableView<EditorViewProtocol>())
复制代码
// 注册 protocol 和 router
[EditorViewRouter registerViewProtocol:@protocol(EditorViewProtocol)];
复制代码
而后就能够用 protocol 获取 router 类,再进一步获取模块:
// 获取模块的 router 类
let routerClass = Router.to(RoutableView<EditorViewProtocol>())
// 获取 EditorViewProtocol 模块
let destination = routerClass?.makeDestination()
复制代码
// 获取模块的 router 类
Class routerClass = ZIKViewRouter.toView(@protocol(EditorViewProtocol));
// 获取 EditorViewProtocol 模块
id<EditorViewProtocol> destination = [routerClass makeDestination];
复制代码
加了一层 router 中间层以后,解耦能力一会儿就加强了:
大部分组件化方案都会带来一个问题,就是减弱甚至抛弃编译检查,由于模块已经变得高度动态化了。
当调用一个模块时,怎么能保证这个模块必定存在?直接引用类时,若是类不存在,编译器会给出引用错误,可是动态组件就没法在静态时检查了。
例如 URL 地址变化了,可是代码中的某些 URL 没有及时更新;使用 protocol 获取模块时,protocol 并无注册对应的模块。这些问题都只能在运行时才能发现。
那么有没有一种方式,可让模块既高度解耦,又能在编译时保证调用的模块必定存在呢?
答案是 YES。
ZIKRouter 最特别的功能,就是可以保证所使用的 protocol 必定存在,在编译阶段就能防止使用不存在的模块。这个功能可让你更安全、更简单地管理所使用的路由接口,没必要再用其余复杂的方式进行检查和维护。
当使用了错误的 protocol 时,会产生编译错误。
Swift 中使用未声明的 protocol:
Objective-C 中使用未声明的 protocol:
这个特性经过两个机制来实现:
下面就一步步讲解,怎么在保持动态解耦特性的同时,实现一套完备的静态类型检查的机制。
怎么才能声明一个 protocol 是能够用于路由的呢?
要实现第一个机制,关键就是要为 protocol 添加特殊的属性或者类型,使用时,若是 protocol 不符合特定类型,就产生编译错误。
原生 Xcode 并不支持这样的静态检查,这时候就要考验咱们的创造力了。
在 Objective-C 中,能够要求 protocol 必须继承自某个特定的父 protocol,而且经过宏定义 + protocol 限定,对 protocol 的父 protocol 继承链进行静态检查。
例如 ZIKRouter 中获取 router 类的方法是这样的:
@protocol ZIKViewRoutable
@end
@interface ZIKViewRouter()
@property (nonatomic, class, readonly) ZIKViewRouterType *(^toView)(Protocol<ZIKViewRoutable> *viewProtocol);
@end
复制代码
toView
用类属性的方式提供,以方便链式调用,这个 block 接收一个Protocol<ZIKViewRoutable> *
类型的 protocol,返回对应的 router 类。
Protocol<ZIKViewRoutable> *
表示这个 protocol 必须继承自ZIKViewRoutable
。普通 protocol 的类型是Protocol *
,因此若是传入@protocol(EditorViewProtocol)
就会产生编译警告。
而若是用宏定义再给 protocol 变量加上一个 protocol 限定,进行一次类型转换,就能够利用编译器检查 protocol 的继承链:
// 声明时继承自 ZIKViewRoutable
@protocol EditorViewProtocol <ZIKViewRoutable>
@end
复制代码
// 宏定义,为 protocol 变量添加 protocol 限定
#define ZIKRoutable(RoutableProtocol) (Protocol<RoutableProtocol>*)@protocol(RoutableProtocol)
复制代码
// 用 protocol 获取 router
ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))
复制代码
ZIKRoutable(EditorViewProtocol)
展开后是(Protocol<EditorViewProtocol> *)@protocol(EditorViewProtocol)
,类型为Protocol<EditorViewProtocol> *
。在 Objective-C 中Protocol<EditorViewProtocol> *
是Protocol<ZIKViewRoutable> *
的子类型,编译器将不会有警告。
可是当传入的 protocol 没有继承自ZIKViewRoutable
时,例如ZIKRoutable(UndeclaredProtocol)
的类型是Protocol<UndeclaredProtocol> *
,编译器在检查 protocol 的继承链时,因为UndeclaredProtocol
没有继承自ZIKViewRoutable
,所以Protocol<UndeclaredProtocol> *
不是Protocol<ZIKViewRoutable> *
的子类型,编译器会给出类型错误的警告。在Build Settings
中能够把incompatible pointer types
警告变成编译错误。
最后,把ZIKViewRouter.toView(ZIKRoutable(EditorViewProtocol))
用宏定义简化一下,变成ZIKViewRouterToView(EditorViewProtocol)
,就能在获取 router 的时候方便地静态检查 protocol 的类型了。
Swift 中不支持宏定义,也不能随意进行类型转换,所以须要换一种方式来进行编译检查。
能够用 struct 的泛型传递 protocol,而后用条件扩展为特定泛型的 struct 添加初始化方法,从而让没有声明过的泛型类型不能直接建立 struct。
例如:
// 用 RoutableView 的泛型来传递 protocol
struct RoutableView<Protocol> {
// 禁止默认的初始化方法
@available(*, unavailable, message: "Protocol is not declared as routable")
public init() { }
}
复制代码
// 泛型为 EditorViewProtocol 的扩展
extension RoutableView where Protocol == EditorViewProtocol {
// 容许初始化
init() { }
}
复制代码
// 泛型为 EditorViewProtocol 时能够初始化
RoutableView<EditorViewProtocol>()
// 没有声明过的泛型没法初始化,会产生编译错误
RoutableView<UndeclaredProtocol>()
复制代码
此时 Xcode 还能够给出自动补全,列出全部声明过的 protocol:
经过路由声明,咱们作到了在编译时对所使用的 protocol 作出限制。下一步就是保证声明过的 protocol 一定有对应的模块,相似于程序在 link 阶段,会检查头文件中声明过的类一定有对应的实现。
这一步是没法直接在编译阶段实现的,不过能够参考 iOS 在启动时检查动态库的方式,咱们能够在启动阶段实现这个功能。
在 app 以 DEBUG 模式启动时,咱们能够遍历全部继承自 ZIKViewRoutable 的 protocol,在注册表中检查是否有对应的 router,若是没有,就给出断言错误。
另外,还可让 router 同时注册建立模块时用到类:
EditorViewRouter.registerView(EditorViewController.self)
复制代码
// 注册 protocol 和 router
[EditorViewRouter registerView:[EditorViewController class]];
复制代码
从而进一步检查 router 中的 class 是否遵照对应的 protocol。这时整个类型检查过程就完整了。
可是 Swift 中的 protocol 是静态类型,并不能经过 OC runtime 直接遍历。是否是就没法动态检查了呢?其实只要发挥创造力,同样能作到。
Swift 的泛型名会在符号名中体现出来。例如上面声明的 init 方法:
// MyApp 中,泛型为 EditorViewProtocol 的扩展
extension RoutableView where Protocol == EditorViewProtocol {
// 容许初始化
init() { }
}
复制代码
在还原符号后就是(extension in MyApp):ZRouter.RoutableView<A where A == MyApp.EditorViewProtocol>.init() -> ZRouter.RoutableView<MyApp.EditorViewProtocol>
。
此时咱们能够遍历 app 的符号表,来查找 RoutableView 的全部扩展,从而提取出全部声明过的 protocol 类型,再去检查是否有对应的 router。
可是若是要进一步检查 router 中的 class 是否遵照 router 中的 protocol,就会遇到问题了。在 Swift 中怎么检查某个任意的 class 遵照某个 Swift protocol ?
Swift 中没有直接提供class_conformsToProtocol
这样的函数,不过咱们能够经过 Swift Runtime 提供的标准函数和 Swift ABI 中定义的内存结构,完成一样的功能。
这部分的实现能够参考代码:_swift_typeIsTargetType。以后我会写几篇文章详细讲解 Swift ABI 的底层内容。
路由检查这部分只在 DEBUG 模式下进行,所以能够放开折腾。
还有最后一个问题,在 BeeHive 中使用[[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)]
获取模块时,返回值是一个id
类型,使用者须要手动指定返回变量的类型,在 Swift 中更是须要手动类型转换,而这一步是可能出错的,而且编译器没法检查。要实现最完备的类型检查,就不能忽视这个问题。
有没有一种方式能让返回值的类型和 protocol 的类型对应呢?OC 中的泛型在这时候就发挥做用了。
能够在 router 上声明模块的泛型:
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *> : NSObject
@end
复制代码
这里使用了两个泛型参数 Destination
和 RouteConfig
,分别表示此 router 所管理的模块类型和路由 config 的类型。__covariant
则表示这个泛型支持协变,也就是子类型能够和父类型同样使用。
声明了泛型参数后,咱们能够在方法中的参数声明中使用泛型:
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *> : NSObject
- (nullable Destination)makeDestination;
- (nullable Destination)destinationWithConfiguration:(RouteConfig)configuration;
@end
复制代码
此时在获取 router 时,就能够把 protocol 的类型做为 router 的泛型参数:
#define ZIKRouterToView(ViewProtocol) [ZIKViewRouter<id<ViewProtocol>,ZIKViewRouteConfiguration *> toView](ZIKRoutable(ViewProtocol))
复制代码
使用ZIKRouterToView(EditorViewProtocol)
获取的 router 类型就是ZIKViewRouter<id<EditorViewProtocol>,ZIKViewRouteConfiguration *>
。在这个 router 上调用makeDestination
时,返回值的类型就是id<EditorViewProtocol>
,从而实现了完整的类型传递。
而在 Swift 中,直接用函数泛型就能实现:
class Router {
static func to<Protocol>(_ routableView: RoutableView<Protocol>) -> ViewRouter<Protocol, ViewRouteConfig>?
}
复制代码
使用Router.to(RoutableView<EditorViewProtocol>())
时,得到的 router 类型就是ViewRouter<EditorViewProtocol, ViewRouteConfig>?
,在调用makeDestination
时,返回值类型就是EditorViewProtocol
,无需手动类型转换。
若是你使用协议组合,还能同时指明多个类型:
typealias EditorViewProtocol = UIViewController & EditorViewInput
复制代码
而且在 router 子类中重写对应方法时,也能用泛型进一步确保类型正确:
class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ZIKViewRouteConfiguration> {
override func destination(with configuration: ZIKViewRouteConfiguration) -> EditorViewProtocol? {
// 函数重写时,参数类型会和泛型一致,实现时能确保返回值的类型是正确的
return EditorViewController()
}
}
复制代码
如今咱们完成了一套完备的类型检查机制,并且这套检查同时支持 OC 和 Swift。
至此,一个基于接口的、类型安全的模块管理工具就完成了。使用 makeDestination
建立模块只是最基本的功能,咱们能够在父类 router 中进行许多有用的功能扩展,例如依赖注入、界面跳转、接口适配,来更好地进行面向接口的开发。
那么在面向接口编程时,咱们还须要哪些功能呢?在扩展以前,咱们先来讨论一下如何使用接口进行模块解耦,首先从理论层面梳理,再把理论转化为工具。
不一样模块对解耦的要求是不一样的。模块从层级上能够从低到高分类:
首先明确一下什么才是解耦,梳理这个问题可以帮助咱们明确目标。
解耦的目的基本上就是两个:提升代码的可维护性、模块重用。指导思想就是面向对象的设计原则。
解耦也有不一样的程度,从低到高,差很少能够分为3层:
第一层解耦,是为了减小不一样代码间的依赖关系,让代码更容易维护。例如把类替换为 protocol,隔绝模块的私有接口,把依赖关系最小化。
解耦的整个过程,就是梳理和管理依赖的过程。所以模块的内聚性越高越好,外部依赖越少越好,这样维护起来才更简单。
若是模块不须要重用,那在这一层基本上就够了。
第二层解耦,是把代码单独抽离,作到了模块重用,能够交给不一样的成员维护,对模块间通讯提出了更高的要求。模块须要在接口中声明外部依赖,去除对特定类型的耦合。
此时影响最大的地方就是模块间通讯的方式,有时候即使是可以单独编译了,也不意味着解耦。例如 URL 路由,只是放弃了编译检查,耦合关系仍是存在于 URL 字符串中,一方的 URL 改变,其余方的代码逻辑就会出错,因此逻辑上仍然是耦合的。所以全部基于某种隐式调用约定的方案(例如字符串匹配),都只是解除编译检查,而不是真正的解耦。
有人说使用 protocol 进行模块间通讯,会致使模块和 protocol 耦合。这个观点是错误的。 protocol 偏偏是把模块的依赖明确地提取出来,是一种更高效的方法。不然彻底用隐式约定来进行通讯,没有编译器的辅助,一旦模块的接口名、参数类型、参数数量须要更新,将会很是难以维护。
并且,经过设计模式,是能够解除对特定 protocol 的依赖的,下文将会对此进行讲解。
第三层解耦,模块间作到了真正的解耦,只要两个模块提供了相同的功能,就能够无缝替换,而且调用方无需任何修改。被替换的模块只须要提供相同功能的接口,经过适配器对接便可,没有其余任何限制,不存在任何其余的隐式调用约定。
通常有这种解耦要求的,都是那些跨项目的通用模块,而项目内专有的业务模块则没有这么高的要求。不过那些跨多端的模块和远程模块没法作到这样的解耦,由于跨多端时没有统一的定义接口的方式,所以只能经过隐式约定或者网络协议定义接口,例如 URL 路由。
总的来讲,解耦的过程就是职责分离、依赖管理(依赖声明和注入)、模块通讯 这三大部分。
要作到模块重用,模块须要尽可能减小外部依赖,而且把依赖提取出来,体现到模块的接口上,让调用者主动注入。同时,把模块的各类事件也提取出来,让调用者进行处理。
这样一来,模块就只须要负责自身的逻辑,不须要关心调用者如何使用模块。那些每一个应用各自专有的应用层逻辑也就从模块中分离出来了。
所以,要想作好模块解耦,管理好依赖是很是重要的。而 protocol 接口就是管理依赖的最高效的方式。
依赖,就是模块中用到的外部数据和外部模块。接下来讨论如何使用 protocol 管理依赖,而且演示如何用 router 实现。
先来复习一下依赖注入的概念。依赖注入和依赖查找是实现控制反转思想的具体方式。
控制反转是将对象依赖的获取从主动变为被动,从对象内部直接引用并获取依赖,变为由外部向对象提供对象所要求的依赖,把不属于本身的职责移交出去,从而让对象和其依赖解耦。此时控制流的主动权从内部转移到了外部,所以称为控制反转。
依赖注入就是指外部向对象传入依赖。
一个类 A 在接口中体现出内部须要用到的一些依赖(例如内部须要用到类B的实例),从而让使用者从外部注入这些依赖,而不是在类内部直接引用依赖并建立类 B。依赖能够用 protocol 的方式声明,这样就可使类 A 和所使用的依赖类 B 进行解耦。
那么如何用 router 进行依赖注入呢?
模块建立了实例后,常常还须要进行一些配置。模块管理工具应该从设计上提供配置功能。
最简单的方式,就是在destinationWithConfiguration:
中建立 destination 时进行配置。可是咱们还能够更进一步,把 destination 的建立和配置分离开。分离以后,router 就能够单独提供配置功能,去配置那些不是由 router 建立的 destination,例如 storyboard 中建立的 view、各类接口回调中返回的实例对象。这样就能够覆盖更多现存的使用场景,减小代码修改。
能够在 router 子类中的prepareDestination:configuration:
中进行模块配置,也就是依赖注入,而模块的调用者无需关心这部分依赖是如何配置的:
// router 父类
class ZIKViewRouter<Destination, RouteConfig>: NSObject {
...
public class func makeDestination -> Destination? {
let router = self.init(with: ViewRouteConfig())
let destination = router.destination(with: router.configuration)
if let destination = destination {
// router 父类中调用模块配置方法
router.prepareDestination(destination, configuration: router.configuration)
}
return destination
}
// 模块建立,让子类重写
public func destination(with configuration: ViewRouteConfig) -> Destination? {
return nil
}
// 模块配置,让子类重写
func prepareDestination(_ destination: Destination, configuration: RouteConfig) {
}
}
// editor 模块的 router
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override func destination(with configuration: ViewRouteConfig) -> EditorViewController? {
let destination = EditorViewController()
return destination
}
// 配置模块,注入静态依赖
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依赖
destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
// 其余配置
destination.title = "默认标题"
}
}
复制代码
// router 父类
@interface ZIKViewRouter<__covariant Destination, __covariant RouteConfig: ZIKViewRouteConfiguration *>: NSObject
@end
@implementation ZIKViewRouter
...
+ (id)makeDestination {
ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
id destination = [router destinationWithConfiguration:router.configuration];
if (destination) {
// router 父类中调用模块配置方法
[router prepareDestination:destination configuration:router.configuration];
}
return destination;
}
// 模块建立,让子类重写
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return nil;
}
// 模块配置,让子类重写
- (void)prepareDestination:(id)destination configuration:(ZIKViewRouteConfiguration *)configuration {
}
@end
// editor 模块的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
// 配置模块,注入静态依赖
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// 注入 service 依赖
destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
// 其余配置
destination.title = @"默认标题";
}
@end
复制代码
此时调用者中若是有某些对象不是建立自 router的,就能够直接用对应的 router 进行配置,执行依赖注入:
var destination: EditorViewProtocol = ...
Router.to(RoutableView<EditorViewProtocol>())?.prepare(destination: destination, configuring: { (config, _) in
})
复制代码
id<EditorViewProtocol> destination = ...
[ZIKRouterToView(EditorViewProtocol) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
}];
复制代码
独立的配置功能在某些场景下是很是有用的,尤为是在重构现有代码的时候。有一些系统接口的设计就是在接口中返回对象,可是这些对象是由系统自动建立的,而不是经过 router 建立的,所以须要经过 router 对其进行配置,例如 storyboard 中建立的 view controller。此时将 view controller 模块化后,依然能够保持现有代码,只须要调用一句prepareDestination:configuration:
配置便可,模块化的过程当中就能让代码的修改最小化。
当依赖是可选的,并非建立对象所必需的,能够用属性注入和方法注入。
属性注入是指外部设置对象的属性。方法注入是指外部调用对象的方法,从而传入依赖。
protocol PersonType {
var wife: Person? { get set } // 可选的属性依赖
func addChild(_ child: Person) -> Void // 可选的方法注入
}
protocol Child {
var parent: Person { get }
}
class Person: PersonType {
var wife: Person? = nil
var childs: Set<Child> = []
func addChild(_ child: Child) {
childs.insert(child)
}
}
复制代码
@protocol PersonType: ZIKServiceRoutable
@property (nonatomic, strong, nullable) Person *wife; // 可选的属性依赖
- (void)addChild:(Person *)child; // 可选的方法注入
@end
@protocol Child
@property (nonatomic, strong) Person *parent;
@end
@interface Person: NSObject <PersonType>
@property (nonatomic, strong, nullable) Person *wife;
@property (nonatomic, strong) NSSet<id<Child>> childs;
@end
复制代码
在 router 里,能够注入一些默认的依赖:
class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {
...
override func destination(with configuration: PerformRouteConfig) -> Person? {
let person = Person()
return person
}
// 配置模块,注入静态依赖
override func prepareDestination(_ destination: Person, configuration: PerformRouteConfig) {
if destination.wife != nil {
return
}
//设置默认值
let wife: Person = ...
person.wife = wife
}
}
复制代码
@interface PersonRouter: ZIKServiceRouter<Person *, ZIKPerformRouteConfiguration *>
@end
@implementation PersonRouter
- (nullable Person *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
Person *person = [Person new];
return person;
}
// 配置模块,注入静态依赖
- (void)prepareDestination:(Person *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
if (destination.wife != nil) {
return;
}
Person *wife = ...
destination.wife = wife;
}
@end
复制代码
在执行路由操做的同时,调用者也能够用PersonType
动态地注入依赖,也就是向模块传参。
configuration 就是用来进行各类功能扩展的。Router 能够在 configuration 上提供prepareDestination
,让调用者设置,就能让调用者配置 destination。
let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), configuring: { (config, _) in
// 获取模块的同时进行配置
config.prepareDestination = { destination in
destination.wife = wife
destination.addChild(child)
}
})
复制代码
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType)
makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration *config) {
// 获取模块的同时进行配置
config.prepareDestination = ^(id<PersonType> destination) {
destination.wife = wife;
[destination addChild:child];
};
}];
复制代码
封装一下就能变成更简单的接口:
let wife: Person = ...
let child: Child = ...
let person = Router.makeDestination(to: RoutableService<PersonType>(), preparation: { destination in
destination.wife = wife
destination.addChild(child)
})
复制代码
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType)
makeDestinationWithPreparation:^(id<PersonType> destination) {
destination.wife = wife;
[destination addChild:child];
}];
复制代码
有一些参数是在 destination 类建立前就须要传入的必需参数,例如初始化方法中的参数,就是必需依赖。
class Person: PersonType {
let name: String
// 初始化方法,须要必需参数
init(name: String) {
self.name = name
}
}
复制代码
@interface Person: NSObject <PersonType>
@property (nonatomic, strong) NSString *name;
// 初始化方法,须要必需参数
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
@end
复制代码
这些必需参数有时候是由调用者提供的。在 URL 路由中,这种"必需"特性就没法体现出来,而用接口的方式就能简单地实现。
传递必需依赖须要用工厂模式,在工厂方法上声明必需参数和模块接口。
protocol PersonTypeFactory {
// 工厂方法,声明了必需参数 name,返回 PersonType 类型的 destination
func makeDestinationWith(_ name: String) -> PersonType?
}
复制代码
@protocol PersonTypeFactory: ZIKServiceModuleRoutable
// 工厂方法,声明了必需参数 name,返回 PersonType 类型的 destination
- (id<PersonType>)makeDestinationWith:(NSString *)name;
@end
复制代码
那么如何用 router 传递必需参数呢?
Router 的 configuration 能够用来进行自定义参数扩展。能够把必需参数保存到 configuration 上,或者更直接点,由 configuration 来提供工厂方法,而后使用工厂方法的 protocol 来获取模块:
// 通用 configuration,能够提供自定义工厂方法
class PersonModuleConfiguration: PerformRouteConfig, PersonTypeFactory {
// 工厂方法
public func makeDestinationWith(_ name: String) -> PersonType? {
self.makedDestination = Person(name: name)
return self.makedDestination
}
// 由工厂方法建立的 destination,提供给 router
public var makedDestination: Destination?
}
复制代码
// 通用 configuration,能够提供自定义工厂方法
@interface PersonModuleConfiguration: ZIKPerformRouteConfiguration<PersonTypeFactory>
// 由工厂方法建立的 destination,提供给 router
@property (nonatomic, strong, nullable) id<PersonTypeFactory> makedDestination;
@end
@implementation PersonModuleConfiguration
// 工厂方法
-(id<PersonTypeFactory>)makeDestinationWith:(NSString *)name {
self.makedDestination = [[Person alloc] initWithName:name];
return self.makedDestination;
}
@end
复制代码
在 router 中使用自定义 configuration:
class PersonRouter: ZIKServiceRouter<Person, PersonModuleConfiguration> {
// 重写 defaultRouteConfiguration,使用自定义 configuration
override class func defaultRouteConfiguration() -> PersonModuleConfiguration {
return PersonModuleConfiguration()
}
override func destination(with configuration: PersonModuleConfiguration) -> Person? {
// 使用工厂方法建立的 destination
return config.makedDestination
}
}
复制代码
@interface PersonRouter: ZIKServiceRouter<id<PersonType>, PersonModuleConfiguration *>
@end
@implementation PersonRouter
// 重写 defaultRouteConfiguration,使用自定义 configuration
+ (PersonModuleConfiguration *)defaultRouteConfiguration {
return [PersonModuleConfiguration new];
}
- (nullable id<PersonType>)destinationWithConfiguration:(PersonModuleConfiguration *)configuration {
// 使用工厂方法建立的 destination
return configuration.makedDestination;
}
@end
复制代码
而后把PersonTypeFactory
协议和 router 进行注册:
PersonRouter.register(RoutableServiceModule<PersonTypeFactory>())
复制代码
[PersonRouter registerModuleProtocol:ZIKRoutable(PersonTypeFactory)];
复制代码
就能够用PersonTypeFactory
获取模块了:
let name: String = ...
Router.makeDestination(to: RoutableServiceModule<PersonTypeFactory>(), configuring: { (config, _) in
// config 遵照 PersonTypeFactory
config.makeDestinationWith(name)
})
复制代码
NSString *name = ...
ZIKRouterToServiceModule(PersonTypeFactory) makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration<PersonTypeFactory> *config) {
// config 遵照 PersonTypeFactory
[config makeDestinationWith:name];
}]
复制代码
若是你不须要在 configuration 上保存其余自定义参数,也不想建立过多的 configuration 子类,能够用一个通用的泛型类来实现子类重写的效果。
泛型能够自定义参数类型,此时能够直接把工厂方法用 block 保存在 configuration 的属性上。
// 通用 configuration,能够提供自定义工厂方法
class ServiceMakeableConfiguration<Destination, Constructor>: PerformRouteConfig {
public var makeDestinationWith: Constructor
public var makedDestination: Destination?
}
复制代码
@interface ZIKServiceMakeableConfiguration<__covariant Destination>: ZIKPerformRouteConfiguration
@property (nonatomic, copy) Destination(^makeDestinationWith)();
@property (nonatomic, strong, nullable) Destination makedDestination;
@end
复制代码
在 router 中使用自定义 configuration:
class PersonRouter: ZIKServiceRouter<Person, PerformRouteConfig> {
// 重写 defaultRouteConfiguration,使用自定义 configuration
override class func defaultRouteConfiguration() -> PerformRouteConfig {
let config = ServiceMakeableConfiguration<PersonType, (String) -> PersonType>({ _ in})
// 设置工厂方法,让调用者使用
config.makeDestinationWith = { [unowned config] name in
config.makedDestination = Person(name: name)
return config.makedDestination
}
return config
}
override func destination(with configuration: PerformRouteConfig) -> Person? {
if let config = configuration as? ServiceMakeableConfiguration<PersonType, (String) -> PersonType> {
// 使用工厂方法建立的 destination
return config.makedDestination
}
return nil
}
}
// 让对应泛型的 configuration 遵照 PersonTypeFactory
extension ServiceMakeableConfiguration: PersonTypeFactory where Destination == PersonType, Constructor == (String) -> PersonType {
}
复制代码
@interface PersonRouter: ZIKServiceRouter<id<PersonType>, ZIKServiceMakeableConfiguration *>
@end
@implementation PersonRouter
// 重写 defaultRouteConfiguration,使用自定义 configuration
+ (ZIKServiceMakeableConfiguration *)defaultRouteConfiguration {
ZIKServiceMakeableConfiguration *config = [ZIKServiceMakeableConfiguration new];
__weak typeof(config) weakConfig = config;
// 设置工厂方法,让调用者使用
config.makeDestinationWith = id ^(NSString *name) {
weakConfig.makedDestination = [[Person alloc] initWithName:name];
return weakConfig.makedDestination;
};
return config;
}
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
// 使用工厂方法建立的 destination
return configuration.makedDestination;
}
@end
复制代码
除了必需依赖,还有一些参数是不属于 destination 类的,而是属于模块内其余组件的,也不能经过 destination 的接口来传递。例如 MVVM 和 VIPER 架构中,model 参数不能传给 view,而是应该交给 view model 或者 interactor。此时可使用相同的模式。
protocol EditorViewModuleInput {
// 工厂方法,声明了参数 note,返回 EditorViewInput 类型的 destination
func makeDestinationWith(_ note: Note) -> EditorViewInput?
}
复制代码
@protocol EditorViewModuleInput: ZIKViewModuleRoutable
// 工厂方法,声明了参数 note,返回 EditorViewInput 类型的 destination
- (id<EditorViewInput>)makeDestinationWith:(Note *)note;
@end
复制代码
class EditorViewRouter: ZIKViewRouter<EditorViewInput, ViewRouteConfig> {
// 重写 defaultRouteConfiguration,使用自定义 configuration
override class func defaultRouteConfiguration() -> ViewRouteConfig {
let config = ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput>({ _ in})
// 设置工厂方法,让调用者使用
config.makeDestinationWith = { [unowned config] note in
config.makedDestination = self.makeDestinationWith(note: note)
return config.makedDestination
}
return config
}
class func makeDestinationWith(note: Note) -> EditorViewInput {
let view = EditorViewController()
let presenter = EditorViewPresenter(view)
let interactor = EditorInteractor(Presenter)
// 把 model 传递给数据管理者,view 不接触 model
interactor.note = note
return view
}
override func destination(with configuration: ViewRouteConfig) -> EditorViewInput? {
if let config = configuration as? ViewMakeableConfiguration<EditorViewInput, (Note) -> EditorViewInput> {
// 使用工厂方法建立的 destination
return config.makedDestination
}
return nil
}
}
复制代码
@interface EditorViewRouter: ZIKViewRouter<id<EditorViewInput>, ZIKViewMakeableConfiguration *>
@end
@implementation PersonRouter
// 重写 defaultRouteConfiguration,使用自定义 configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
__weak typeof(config) weakConfig = config;
// 设置工厂方法,让调用者使用
config.makeDestinationWith = id ^(Note *note) {
weakConfig.makedDestination = [self makeDestinationWith:note];
return weakConfig.makedDestination;
};
return config;
}
+ (id<EditorViewInput>)makeDestinationWith:(Note *)note {
EditorViewController *view = [[EditorViewController alloc] init];
EditorViewPresenter *presenter = [[EditorViewPresenter alloc] initWithView:view];
EditorInteractor *interactor = [[EditorInteractor alloc] initWithPresenter:presenter];
// 把 model 传递给数据管理者,view 不接触 model
interactor.note = note;
return view;
}
- (nullable id<EditorViewInput>)destinationWithConfiguration:(ZIKViewMakeableConfiguration *)configuration {
// 使用工厂方法建立的 destination
return configuration.makedDestination;
}
@end
复制代码
就能够用EditorViewModuleInput
获取模块了:
let note: Note = ...
Router.makeDestination(to: RoutableViewModule<EditorViewModuleInput>(), configuring: { (config, _) in
// config 遵照 EditorViewModuleInput
config.makeDestinationWith(note)
})
复制代码
Note *note = ...
ZIKRouterToViewModule(EditorViewModuleInput) makeDestinationWithConfiguring:^(ZIKViewRouteConfiguration<EditorViewModuleInput> *config) {
// config 遵照 EditorViewModuleInput
config.makeDestinationWith(note);
}]
复制代码
当模块的必需依赖不少时,若是把依赖都放在初始化接口中,就会出现一个很是长的方法。
除了让模块把依赖声明在接口中,模块内部也能够用模块管理工具动态查找依赖,例如用 router 查找 protocol 对应的模块。若是要使用这种模式,那么全部模块都须要统一使用相同的模块管理工具。
代码以下:
class EditorViewController: UIViewController {
lazy var storageService: EditorStorageServiceInput {
return Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())!
}
}
复制代码
@interface EditorViewController : UIViewController()
@property (nonatomic, strong) id<EditorStorageServiceInput> storageService;
@end
@implementation EditorViewController
- (id<EditorStorageServiceInput>)storageService {
if (!_storageService) {
_storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
}
return _storageService;
}
@end
复制代码
使用依赖注入时,有些特殊状况须要处理,例如循环依赖的无限递归问题。
循环依赖是指两个对象互相依赖。
在 router 内部动态注入依赖时,若是注入的依赖同时依赖于被注入的对象,则必须在 protocol 中声明。
protocol Parent {
// Parent 依赖 Child
var child: Child { get set }
}
protocol Child {
// Child 依赖 Parent
var parent: Parent { get set }
}
class ParentObject: Parent {
var child: Child!
}
class ChildObject: Child {
var parent: Parent!
}
复制代码
@protocol Parent <ZIKServiceRoutable>
// Parent 依赖 Child
@property (nonatomic, strong) id<Child> child;
@end
@protocol Child <ZIKServiceRoutable>
// Child 依赖 Parent
@property (nonatomic, strong) id<Parent> parent;
@end
@interface ParentObject: NSObject<Parent>
@end
@interface ParentObject: NSObject<Child>
@end
复制代码
class ParentRouter: ZIKServiceRouter<ParentObject, PerformRouteConfig> {
override func destination(with configuration: PerformRouteConfig) -> ParentObject? {
return ParentObject()
}
override func prepareDestination(_ destination: ParentObject, configuration: PerformRouteConfig) {
guard destination.child == nil else {
return
}
// 只有在外部没有设置 child 时,才去主动寻找依赖
let child = Router.makeDestination(to RoutableService<Child>(), preparation { child in
// 设置 child 的依赖,防止 child 内部再去寻找 parent 依赖,致使循环
child.parent = destination
})
destination.child = child
}
}
class ChildRouter: ZIKServiceRouter<ChildObject, PerformRouteConfig> {
override func destination(with configuration: PerformRouteConfig) -> ChildObject? {
return ChildObject()
}
override func prepareDestination(_ destination: ChildObject, configuration: PerformRouteConfig) {
guard destination.parent == nil else {
return
}
// 只有在外部没有设置 parent 时,才去主动寻找依赖
let parent = Router.makeDestination(to RoutableService<Parent>(), preparation { parent in
// 设置 parent 的依赖,防止 parent 内部再去寻找 child 依赖,致使循环
parent.child = destination
})
destination.parent = parent
}
}
复制代码
@interface ParentRouter: ZIKServiceRouter<ParentObject *, ZIKPerformRouteConfiguration *>
@end
@implementation ParentRouter
- (ParentObject *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
return [ParentObject new];
}
- (void)prepareDestination:(ParentObject *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
if (destination.child) {
return;
}
// 只有在外部没有设置 child 时,才去主动寻找依赖
destination.child = [ZIKRouterToService(Child) makeDestinationWithPreparation:^(id<Child> child) {
// 设置 child 的依赖,防止 child 内部再去寻找 parent 依赖,致使循环
child.parent = destination;
}];
}
@end
@interface ChildRouter: ZIKServiceRouter<ChildObject *, ZIKPerformRouteConfiguration *>
@end
@implementation ChildRouter
- (ChildObject *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
return [ChildObject new];
}
- (void)prepareDestination:(ChildObject *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
if (destination.parent) {
return;
}
// 只有在外部没有设置 parent 时,才去主动寻找依赖
destination.parent = [ZIKRouterToService(Parent) makeDestinationWithPreparation:^(id<Parent> parent) {
// 设置 parent 的依赖,防止 parent 内部再去寻找 child 依赖,致使循环
parent.child = destination;
}];
}
@end
复制代码
这样就能避免循环依赖致使的无限递归问题。
当使用 protocol 管理模块时,protocol 一定会出如今多个模块中。那么此时如何让每一个模块单独编译呢?
一个方式是把 protocol 在每一个用到的模块里复制一份,并且无需修改 protocol 名,Xcode 不会报错。
另外一个方式是使用适配器模式,可让不一样模块使用各自不一样的 protocol 和同一个模块交互。
你能够为同一个 router 注册多个 protocol。
根据依赖关系,接口能够分为required protocol
和provided protocol
。模块自己提供的接口是provided protocol
,模块的调用者须要使用的接口是required protocol
。
required protocol
是provided protocol
的子集,调用者只须要声明本身用到的那些接口,没必要引入整个provided protocol
,这样可让模块间的耦合进一步减小。
在 UML 的组件图中,就很明确地表现出了这二者的概念。下图中的半圆就是Required Interface
,框外的圆圈就是Provided Interface
:
那么如何实施Required Interface
和Provided Interface
?从架构分层上看,全部的模块都是依附于一个更上层的宿主 app 环境存在的,应该由使用这些模块的宿主 app 在一个 adapter 里进行接口适配,从而使得调用者能够继续在内部使用required protocol
,adapter 负责把required protocol
和修改后的provided protocol
进行适配。整个过程模块都无感知。
这时候,调用者中定义的required protocol
就至关因而在声明本身所依赖的外部模块。
provided
模块添加required protocol
模块适配的工做所有由模块的使用和装配者 App Context 完成,最少时只须要两行代码。
例如,某个模块须要展现一个登录界面,并且这个登录界面能够显示一段自定义的提示语。
调用者模块示例:
// 调用者中声明的依赖接口,代表自身依赖一个登录界面
protocol RequiredLoginViewInput {
var message: String? { get set } //显示在登录界面上的自定义提示语
}
// 调用者中调用 login 模块
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
destination.message = "请登陆"
})
复制代码
// 调用者中声明的依赖接口,代表自身依赖一个登录界面
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
// 调用者中调用 login 模块
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
destination.message = @"请登陆";
}];
复制代码
实际登录界面提供的接口则是ProvidedLoginViewInput
:
// 实际登录界面提供的接口
protocol ProvidedLoginViewInput {
var message: String? { get set }
}
复制代码
// 实际登录界面提供的接口
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
复制代码
适配的代码由宿主 app 实现,让登录界面支持 RequiredLoginViewInput
:
// 让模块支持 required protocol,只须要添加一个 protocol 扩展便可
extension LoginViewController: RequiredLoginViewInput {
}
复制代码
而且让登录界面的 router 也支持 RequiredLoginViewInput
:
// 若是能够获取到 router 类,能够直接为 router 添加 RequiredLoginViewInput
LoginViewRouter.register(RoutableView<RequiredLoginViewInput>())
// 若是不能获得对应模块的 router,能够用 adapter 进行转发
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
复制代码
适配以后,RequiredLoginViewInput
就能和ProvidedLoginViewInput
同样使用,获取到同一个模块了:
调用者模块示例:
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
destination.message = "请登陆"
})
// ProvidedLoginViewInput 和 RequiredLoginViewInput 能获取到同一个 router
Router.makeDestination(to: RoutableView<ProvidedLoginViewInput>(), preparation: {
destination.message = "请登陆"
})
复制代码
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
destination.message = @"请登陆";
}];
// ProvidedLoginViewInput 和 RequiredLoginViewInput 能获取到同一个 router
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<ProvidedLoginViewInput> destination) {
destination.message = @"请登陆";
}];
复制代码
有时候ProvidedLoginViewInput
和RequiredLoginViewInput
的接口名可能会稍有不一样,此时须要用 category、extension、子类、proxy 类等方式进行接口适配。
protocol ProvidedLoginViewInput {
var notifyString: String? { get set } // 接口名不一样
}
复制代码
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString; // 接口名不一样
@end
复制代码
适配时须要进行接口转发,让登录界面支持 RequiredLoginViewInput
:
extension LoginViewController: RequiredLoginViewInput {
var message: String? {
get {
return notifyString
}
set {
notifyString = newValue
}
}
}
复制代码
@interface LoginViewController (ModuleAAdapter) <RequiredLoginViewInput>
@property (nonatomic, copy) NSString *message;
@end
@implementation LoginViewController (ModuleAAdapter)
- (void)setMessage:(NSString *)message {
self.notifyString = message;
}
- (NSString *)message {
return self.notifyString;
}
@end
复制代码
若是不能直接为模块添加required protocol
,好比 protocol 里的一些 delegate 须要兼容:
protocol RequiredLoginViewDelegate {
func didFinishLogin() -> Void
}
protocol RequiredLoginViewInput {
var message: String? { get set }
var delegate: RequiredLoginViewDelegate { get set }
}
复制代码
@protocol RequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<RequiredLoginViewDelegate> delegate;
@end
复制代码
而模块里的 delegate 接口不同:
protocol ProvidedLoginViewDelegate {
func didLogin() -> Void
}
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
var delegate: ProvidedLoginViewDelegate { get set }
}
复制代码
@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<ProvidedLoginViewDelegate> delegate;
@end
复制代码
相同方法有不一样参数类型时,能够用一个新的 router 代替真正的 router,在新的 router 里插入一个中介者,负责转发接口:
class ReqiredLoginViewRouter: ProvidedLoginViewRouter {
override func destination(with configuration: ZIKViewRouteConfiguration) -> RequiredLoginViewInput? {
let realDestination: ProvidedLoginViewInput = super.destination(with configuration)
// proxy 负责把 RequiredLoginViewInput 转发为 ProvidedLoginViewInput
let proxy: RequiredLoginViewInput = ProxyForDestination(realDestination)
return proxy
}
}
复制代码
@interface ReqiredLoginViewRouter : ProvidedLoginViewRouter
@end
@implementation RequiredLoginViewRouter
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
id<ProvidedLoginViewInput> realDestination = [super destinationWithConfiguration:configuration];
// proxy 负责把 RequiredLoginViewInput 转发为 ProvidedLoginViewInput
id<RequiredLoginViewInput> proxy = ProxyForDestination(realDestination);
return mediator;
}
@end
复制代码
对于普通OC类,proxy 能够用 NSProxy 来实现。对于 UIKit 中的那些复杂的 UI 类,或者 Swift 类,能够用子类,而后在子类中重写方法,进行模块适配。
利用以前的静态路由检查机制,模块只须要声明 required 接口,就能保证对应的模块一定存在。
模块无需在本身的接口里声明依赖,若是模块须要新增依赖,只须要建立新的 required 接口便可,无需修改接口自己。这样也能避免依赖变更致使的接口变化,减小接口维护的成本。
每次引入模块,宿主 app 都须要写一份适配代码,虽然大多数状况下只有两行,可是咱们想尽可能减小宿主 app 的维护职责。
此时,可让模块提供一份默认的依赖,用宏定义包裹,绕过编译检查。
#if USE_DEFAULT_DEPENDENCY
import ProvidedLoginModule
public func registerDefaultDependency() {
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
}
extension ProvidedLoginViewController: RequiredLoginViewInput {
}
#endif
复制代码
#if USE_DEFAULT_DEPENDENCY
@import ProvidedLoginModule;
static inline void registerDefaultDependency() {
[ZIKViewRouteAdapter registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
}
// 宏定义,默认的适配代码
#define ADAPT_DEFAULT_DEPENDENCY \
@interface ProvidedLoginViewController (Adapter) <RequiredLoginViewInput> \
@end \
@implementation ProvidedLoginViewController (Adapter) \
@end \
#endif
复制代码
若是宿主 app 要使用默认依赖,就在.xcconfig
里设置Preprocessor Macros
,开启宏定义:
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) USE_DEFAULT_DEPENDENCY=1
复制代码
若是是 Swift 模块,须要在模块的 target 里设置Active Compilation Conditions
,添加编译宏USE_DEFAULT_DEPENDENCY
。
宿主 app 直接调用默认的适配代码便可,不用再负责维护:
public func registerAdapters() {
// 注册默认的依赖
registerDefaultDependency()
...
}
复制代码
void registerAdapters() {
// 注册默认的依赖
registerDefaultDependency();
...
}
// 使用默认的适配代码
ADAPT_DEFAULT_DEPENDENCY
复制代码
若是宿主 app 须要替换使用另外一个 provided 模块,能够关闭宏定义,再写一份另外的适配代码,便可替换依赖。
区分了required protocol
和provided protocol
后,就能够实现真正的模块化。在调用者声明了所须要的required protocol
后,被调用模块就能够随时被替换成另外一个相同功能的模块。
参考 demo 中的ZIKLoginModule
示例模块,登陆模块依赖于一个弹窗模块,而这个弹窗模块在ZIKRouterDemo
和ZIKRouterDemo-macOS
中是不一样的,而在切换弹窗模块时,登陆模块中的代码不须要作任何改变。
通常来讲,并不须要当即把全部的 protocol 都分离为required protocol
和provided protocol
。调用模块和目的模块能够暂时共用 protocol,或者只是简单地改个名字,让required protocol
做为provided protocol
的子集,在第一次须要替换模块的时候再用 category、extension、proxy、subclass 等技术进行接口适配。
接口适配也不能滥用,由于成本比较高,并且并不是全部的接口都能适配,例如同步接口和异步接口就难以适配。
对于模块间耦合的处理,有这么几条建议:
required protocol
做为provided protocol
的子集,接口名保持一致经过required protocol
和provided protocol
,咱们就实现了模块间的彻底解耦。
模块间通讯有多种方式,解耦程度也各有不一样。这里只讨论接口交互的方式。
模块的对外接口能够分为 input 和 output。二者的区别主要是控制流的主动权归属不一样。
Input 是由外部主动调用的接口,控制流的发起者在外部,例如外部调用 view 的 UI 修改接口。
Output 是模块内部主动调用外部实现的接口,控制流的发起者在内部,须要外部实现 output 所要求的方法。例如输出 UI 事件、事件回调、获取外部的 dataSource。iOS 中经常使用的 delegate 模式,也是一种 output。
模块设计好 input 和 output,而后在模块建立的时候,设置好模块之间的 input 和 output 关系,便可配置好模块间通讯,同时充分解耦。
class NoteListViewController: UIViewController, EditorViewOutput {
func showEditor() {
let destination = Router.makeDestination(to: RoutableView<EditorViewInput>(), preparation: { [weak self] destination in
destination.output = self
})
present(destination, animated: true)
}
}
protocol EditorViewInput {
weak var output: EditorViewOutput? { get set }
}
复制代码
大部分方案都没有讨论子模块存在的状况。若是使用了 MVVM 或者 VIPER 架构,此时一个 view controller 使用了 child view controller,那多个模块的 view model 和 interactor 之间如何交互?子模块由谁初始化、由谁管理?
有些方案是直接在父 view model 里建立和使用子 view model,可是这样就致使了 view 的实现方式影响了view model 的实现,若是父 view 里替换使用了另外一个子 view,那父 view model 里的代码也须要修改。
子模块的来源有:
子 view 多是一个 UIView,也多是一个 Child UIViewController。所以子 view 有可能须要向外部请求数据,也可能独立完成全部任务,不须要依赖父模块。
若是子 view 能够独立,那在子模块里不会出现和父模块交互的逻辑,只有把一些事件经过 output 传递出去的接口。这时只须要把子 view 的 input 接口封装在父 view 的 input 接口里便可,父 view model / presenter / interactor 是不知道父 view 提供的这几个接口是经过子 view 实现的。
若是父模块须要调用子模块的业务接口,或接收子模块的数据或业务事件,而且不想影响 view 的接口,能够把子 view model / presenter / interactor 做为父 view model / presenter / interactor 的一个 service,在引入子模块时,注入到父 view model / presenter / interactor,从而绕过 view 层。这样子模块和父模块就能经过 service 的形式进行通讯了,而这时,父模块也不知道这个 service 是来自子模块里的。
在这样的设计下,子模块和父模块是不知道彼此的存在的,只是经过接口进行交互。好处是父 view 若是想要更换为另外一个相同功能的子 view 控件,就只须要在父 view 里修改,不会影响其余的 view model / presenter / interactor。
父模块:
class EditorViewController: UIViewController {
var viewModel: EditorViewModel!
func addTextView() {
let textViewController = Router.makeDestination(to: RoutableView<TextViewInput>()) { (destination) in
// 设置模块间交互
// 本来父 view 是没法接触到子模块的 view model / presenter / interactor
// 此时子模块是把这些内部组件做为业务 input 开放给了外部
self.viewModel.textService = destination.viewModel
destination.viewModel.output = self.viewModel
}
addChildViewController(textViewController)
view.addSubview(textViewController.view)
textViewController.didMove(toParentViewController: self)
}
}
复制代码
@interface EditorViewController: UIViewController
@property (nonatomic, strong) id<EditorViewModel> viewModel;
@end
@implementation EditorViewController
- (void)addTextView {
UIViewController *textViewController = [ZIKRouterToView(TextViewInput) makeDestinationWithPreparation:^(id<TextViewInput> destination) {
// 设置模块间交互
// 本来父 view 是没法接触到子模块的 view model / presenter / interactor
// 此时子模块是把这些内部组件做为业务 input 开放给了外部
self.viewModel.textService = destination.viewModel;
destination.viewModel.output = self.viewModel;
}];
[self addChildViewController:textViewController];
[self.view addSubview: textViewController.view];
[textViewController didMoveToParentViewController: self];
}
@end
复制代码
子模块:
protocol TextViewInput {
weak var output: TextViewModuleOutput? { get set }
var viewModel: TextViewModel { get }
}
class TextViewController: UIViewController, TextViewInput {
weak var output: TextViewModuleOutput?
var viewModel: TextViewModel!
}
复制代码
@protocol TextViewInput <ZIKViewRoutable>
@property (nonatomic, weak) id<TextViewModuleOutput> output;
@property (nonatomic, strong) id<TextViewModel> viewModel;
@end
@interface TextViewController: UIViewController <TextViewInput>
@property (nonatomic, weak) id<TextViewModuleOutput> output;
@property (nonatomic, strong) id<TextViewModel> viewModel;
@end
复制代码
在使用 output 时,模块适配会带来必定麻烦。
例如这样一对 required-provided protocol:
protocol RequiredEditorViewInput {
weak var output: RequiredEditorViewOutput? { get set }
}
protocol ProvidedEditorViewInput {
weak var output: ProvidedEditorViewOutput? { get set }
}
复制代码
@protocol RequiredEditorViewInput <NSObject>
@property (nonatomic, weak) id<RequiredEditorViewOutput> output;
@end
@protocol ProvidedEditorViewInput <NSObject>
@property (nonatomic, weak) id<ProvidedEditorViewOutput> output;
@end
复制代码
因为 output 的实现者不是固定的,所以没法让全部的 output 类都同时适配RequiredEditorViewOutput
和ProvidedEditorViewOutput
。此时建议直接使用对应的 protocol,不使用 required-provided 模式。
若是你仍然想要使用 required-provided 模式,那就须要用工厂模式来传递 output ,在内部用 proxy 进行适配。
实际模块的 router:
protocol ProvidedEditorViewModuleInput {
var makeDestinationWith(_ output: ProvidedEditorViewOutput?) -> ProvidedEditorViewInput? { get set }
}
class ProvidedEditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
register(RoutableViewModule<ProvidedEditorViewModuleInput>())
}
override class func defaultRouteConfiguration() -> ViewRouteConfig {
let config = ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?) -> ProvidedViewInput?>({ _ in})
config.makeDestinationWith = { [unowned config] output in
// 设置 output
let viewModel = EditorViewModel(output: output)
config.makedDestination = EditorViewController(viewModel: viewModel)
return config.makedDestination
}
return config
}
override func destination(with configuration: ViewRouteConfig) -> EditorViewController? {
if let config = configuration as? ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?) {
return config.makedDestination
}
return nil
}
}
复制代码
@protocol ProvidedEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic, readonly) id<ProvidedEditorViewInput> (makeDestinationWith)(id<ProvidedEditorViewOutput> output);
@end
@interface ProvidedEditorViewRouter: ZIKViewRouter
@end
@implementation ProvidedEditorViewRouter
+ (void)registerRoutableDestination {
[self registerModuleProtocol:ZIKRoutable(ProvidedEditorViewModuleInput)];
}
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
__weak typeof(config) weakConfig = config;
config.makeDestinationWith = id ^(id<ProvidedEditorViewOutput> output) {
// 设置 output
EditorViewModel *viewModel = [[EditorViewModel alloc] initWithOutput:output];
weakConfig.makedDestination = [[EditorViewController alloc] initWithViewModel:viewModel];
return weakConfig.makedDestination;
};
return config;
}
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
return configuration.makedDestination;
}
@end
复制代码
适配代码:
protocol RequiredEditorViewModuleInput {
var makeDestinationWith(_ output: RequiredEditorViewOutput?) -> RequiredEditorViewInput? { get set }
}
// 用于适配的 required router
class RequiredEditorViewRouter: ProvidedEditorViewRouter {
override class func registerRoutableDestination() {
register(RoutableViewModule<RequiredEditorViewModuleInput>())
}
// 兼容 configuration
override class func defaultRouteConfiguration() -> PerformRouteConfig {
let config = super.defaultRouteConfiguration()
let makeDestinationWith = config.makeDestinationWith
config.makeDestinationWith = { requiredOutput in
// proxy 负责把 RequiredEditorViewOutput 转为 ProvidedEditorViewOutput
let providedOutput = EditorOutputProxy(forwarding: requiredOutput)
return makeDestinationWith(providedOutput)
}
return config
}
}
class EditorOutputProxy: ProvidedEditorViewOutput {
let forwarding: RequiredEditorViewOutput
// 实现 ProvidedEditorViewOutput,转发给 forwarding
}
复制代码
@protocol RequiredEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic, readonly) id<RequiredEditorViewInput> (makeDestinationWith)(id<RequiredEditorViewOutput> output);
@end
// 用于适配的 required router
@interface RequiredEditorViewRouter: ProvidedEditorViewRouter
@end
@implementation RequiredEditorViewRouter
+ (void)registerRoutableDestination {
[self registerModuleProtocol:ZIKRoutable(RequiredEditorViewModuleInput)];
}
// 兼容 configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
ZIKViewMakeableConfiguration *config = [super defaultRouteConfiguration];
id<ProvidedEditorViewInput>(^makeDestinationWith)(id<ProvidedEditorViewOutput>) = config.makeDestinationWith;
config.makeDestinationWith = id ^(id<RequiredEditorViewOutput> requiredOutput) {
// proxy 负责把 RequiredEditorViewOutput 转为 ProvidedEditorViewOutput
EditorOutputProxy *providedOutput = [[EditorOutputProxy alloc] initWithForwarding: requiredOutput];
return makeDestinationWith(providedOutput);
};
return config;
}
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
return configuration.makedDestination;
}
@end
// 实现 ProvidedEditorViewOutput,转发给 forwarding
@interface EditorOutputProxy: NSProxy <ProvidedEditorViewOutput>
@property (nonatomic, strong) id forwarding;
@end
@implementation EditorOutputProxy
- (instancetype)initWithForwarding:(id)forwarding {
if (self = [super init]) {
_forwarding = forwarding;
}
return self;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self.forwarding respondsToSelector:aSelector];
}
- (BOOL)conformsToProtocol:(Protocol *)protocol {
return [self.forwarding conformsToProtocol:protocol];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.forwarding;
}
@end
复制代码
能够看到,output 的适配有些繁琐。所以除非你的模块是通用模块,有实际的解耦需求,不然直接使用 provided protocol 便可。
总结完使用接口进行模块解耦和依赖管理的方法,咱们能够进一步对 router 进行扩展了。上面使用 makeDestination
建立模块是最基本的功能,使用 router 子类后,咱们能够进行许多有用的功能扩展,这里给出一些示范。
编写 router 代码时,须要注册 router 和 protocol 。在 OC 中能够在 +load 方法中注册,可是 Swift 里已经不能使用 +load 方法,并且分散在 +load 中的注册代码也很差管理。BeeHive 中经过宏定义和__attribute((used, section("__DATA,""BeehiveServices""")))
,把注册信息添加到了 mach-O 中的自定义区域,而后在启动时读取并自动注册,惋惜这种方式在 Swift 中也没法使用了。
咱们能够把注册代码写在 router 的+registerRoutableDestination
方法里,而后逐个调用每一个 router 类的+registerRoutableDestination
方法便可。还能够更进一步,用 runtime 技术遍历 mach-O 中的__DATA,__objc_classlist
区域的类列表,获取全部的 router 类,自动调用全部的+registerRoutableDestination
方法。
把注册代码统一管理以后,若是不想使用自动注册,也能随时切换为手动注册。
// editor 模块的 router
class EditorViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView<EditorViewProtocol>())
}
}
复制代码
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}
@end
复制代码
iOS 中模块间耦合的缘由之一,就是界面跳转的逻辑是经过 UIViewController 进行的,跳转功能被限制在了 view controller 上,致使数据流经常都绕不开 view 层。要想更好地管理跳转逻辑,就须要进行封装。
封装界面跳转能够屏蔽 UIKit 的细节,此时界面跳转的代码就能够放在非 view 层(例如 presenter、view model、interactor、service),而且可以跨平台,也能轻易地经过配置切换跳转方式。
若是是普通的模块,就用ZIKServiceRouter
,而若是是界面模块,例如 UIViewController
和 UIView
,就能够用ZIKViewRouter
,在其中封装了界面跳转功能。
封装界面跳转后,使用方式以下:
class TestViewController: UIViewController {
//直接跳转到 editor 界面
func showEditor() {
Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}
//跳转到 editor 界面,跳转前用 protocol 配置界面
func prepareAndShowEditor() {
Router.perform(
to: RoutableView<EditorViewProtocol>(),
path: .push(from: self),
preparation: { destination in
// 跳转前进行配置
// destination 自动推断为 EditorViewProtocol
})
}
}
复制代码
@implementation TestViewController
- (void)showEditor {
//直接跳转到 editor 界面
[ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
- (void)prepareAndShowEditor {
//跳转到 editor 界面,跳转前用 protocol 配置界面
[ZIKRouterToView(EditorViewProtocol)
performPath:ZIKViewRoutePath.pushFrom(self)
preparation:^(id<EditorViewProtocol> destination) {
// 跳转前进行配置
// destination 自动推断为 EditorViewProtocol
}];
}
@end
复制代码
能够用 ViewRoutePath
一键切换不一样的跳转方式:
enum ViewRoutePath {
case push(from: UIViewController)
case presentModally(from: UIViewController)
case presentAsPopover(from: UIViewController, configure: ZIKViewRoutePopoverConfigure)
case performSegue(from: UIViewController, identifier: String, sender: Any?)
case show(from: UIViewController)
case showDetail(from: UIViewController)
case addAsChildViewController(from: UIViewController, addingChildViewHandler: (UIViewController, @escaping () -> Void) -> Void)
case addAsSubview(from: UIView)
case custom(from: ZIKViewRouteSource?)
case makeDestination
case extensible(path: ZIKViewRoutePath)
}
复制代码
并且在界面跳转后,还能够根据跳转时的跳转方式,一键回退界面,无需再手动区分 dismiss、pop 等各类状况:
class TestViewController: UIViewController {
var router: DestinationViewRouter<EditorViewProtocol>?
func showEditor() {
// 持有 router
router = Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}
// Router 会对 editor view controller 执行 pop 操做,移除界面
func removeEditor() {
guard let router = router, router.canRemove else {
return
}
router.removeRoute()
router = nil
}
}
复制代码
@interface TestViewController()
@property (nonatomic, strong) ZIKDestinationViewRouter(id<EditorViewProtocol>) *router;
@end
@implementation TestViewController
- (void)showEditor {
// 持有 router
self.router = [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
// Router 会对 editor view controller 执行 pop 操做,移除界面
- (void)removeEditor {
if (![self.router canRemove]) {
return;
}
[self.router removeRoute];
self.router = nil;
}
@end
复制代码
有些界面的跳转方式很特殊,例如 tabbar 上的界面,须要经过切换 tabbar item 来进行。也有的界面有自定义的跳转动画,此时能够在 router 子类中重写对应方法,进行自定义跳转。
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override func destination(with configuration: ViewRouteConfig) -> Any? {
return EditorViewController()
}
override func canPerformCustomRoute() -> Bool {
return true
}
override func performCustomRoute(onDestination destination: EditorViewController, fromSource source: Any?, configuration: ViewRouteConfig) {
beginPerformRoute()
// 自定义跳转
CustomAnimator.transition(from: source, to: destination) {
self.endPerformRouteWithSuccess()
}
}
override func canRemoveCustomRoute() -> Bool {
return true
}
override func removeCustomRoute(onDestination destination: EditorViewController, fromSource source: Any?, removeConfiguration: ViewRemoveConfig, configuration: ViewRouteConfig) {
beginRemoveRoute(fromSource: source)
// 移除自定义跳转
CustomAnimator.dismiss(destination) {
self.endRemoveRouteWithSuccess(onDestination: destination, fromSource: source)
}
}
override class func supportedRouteTypes() -> ZIKViewRouteTypeMask {
return [.custom, .viewControllerDefault]
}
}
复制代码
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return [[EditorViewController alloc] init];
}
- (BOOL)canPerformCustomRoute {
return YES;
}
- (void)performCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source configuration:(ZIKViewRouteConfiguration *)configuration {
[self beginPerformRoute];
// 自定义跳转
[CustomAnimator transitionFrom:source to:destination completion:^{
[self endPerformRouteWithSuccess];
}];
}
- (BOOL)canRemoveCustomRoute {
return YES;
}
- (void)removeCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source removeConfiguration:(ZIKViewRemoveConfiguration *)removeConfiguration configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
[self beginRemoveRouteFromSource:source];
// 移除自定义跳转
[CustomAnimator dismiss:destination completion:^{
[self endRemoveRouteWithSuccessOnDestination:destination fromSource:source];
}];
}
+ (ZIKViewRouteTypeMask)supportedRouteTypes {
return ZIKViewRouteTypeMaskCustom|ZIKViewRouteTypeMaskViewControllerDefault;
}
@end
复制代码
不少项目使用了 storyboard,在进行模块化时,确定不能要求全部使用 storyboard 的模块都改成使用代码。所以咱们能够 hook 一些 storyboard 相关的方法,例如-prepareSegue:sender:
,在其中调用prepareDestination:configuring:
便可。
虽然以前列出了 URL 路由的许多缺点,可是若是你的模块须要从 h5 界面调用,例如电商 app 须要实现跨平台的动态路由规则,那么 URL 路由就是最佳的方案。
可是咱们并不想为了实现 URL 路由,使用另外一套框架再从新封装一次模块。只须要在 router 上扩展 URL 路由的功能,便可同时用接口和 URL 管理模块。
你能够给 router 注册 url:
class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ViewRouteConfig> {
override class func registerRoutableDestination() {
// 注册 url
registerURLPattern("app://editor/:title")
}
}
复制代码
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
// 注册 url
[self registerURLPattern:@"app://editor/:title"];
}
@end
复制代码
以后就能够用相应的 url 获取 router:
ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
复制代码
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
复制代码
以及处理 URL Scheme:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let urlString = url.absoluteString
if let _ = ZIKAnyViewRouter.performURL(urlString, fromSource: self.rootViewController) {
return true
} else if let _ = ZIKAnyServiceRouter.performURL(urlString) {
return true
}
return false
}
复制代码
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
return YES;
} else if ([ZIKAnyServiceRouter performURL:urlString]) {
return YES;
}
return NO;
}
复制代码
每一个 router 子类还能各自对 url 进行进一步处理,例如处理 url 中的参数、经过 url 执行对应方法、执行路由后发送返回值给调用者等。
每一个项目对 URL 路由的需求都不同,基于 ZIKRouter 强大的可扩展性,你也能够按照项目需求实现本身的 URL 路由规则。
除了建立 router 子类,也可使用通用的 router 实例对象,在每一个对象的 block 属性中提供和 router 子类同样的功能,所以没必要担忧类过多的问题。原理就和用泛型 configuration 代替 configuration 子类同样。
ZIKViewRoute 对象经过 block 属性实现子类重写的效果,代码能够用链式调用:
ZIKViewRoute<EditorViewController, ViewRouteConfig>
.make(withDestination: EditorViewController.self, makeDestination: ({ (config, router) -> EditorViewController? in
return EditorViewController()
}))
.prepareDestination({ (destination, config, router) in
}).didFinishPrepareDestination({ (destination, config, router) in
})
.register(RoutableView<EditorViewProtocol>())
复制代码
[ZIKDestinationViewRoute(id<EditorViewProtocol>)
makeRouteWithDestination:[ZIKInfoViewController class]
makeDestination:^id<EditorViewProtocol> _Nullable(ZIKViewRouteConfig *config, ZIKRouter *router) {
return [[EditorViewController alloc] init];
}]
.prepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.didFinishPrepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));
复制代码
基于 ZIKViewRoute 对象实现的 router,能够进一步简化 router 的实现代码。
若是你的类很简单,并不须要用到 router 子类,直接一行代码注册类便可:
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), forMakingView: EditorViewController.self)
复制代码
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];
复制代码
或者用 block 自定义建立对象的方式:
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),
forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
return EditorViewController()
}
复制代码
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
return [[EditorViewController alloc] init];
}];
复制代码
或者指定用 C 函数建立对象:
function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
return EditorViewController()
}
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),
forMakingView: EditorViewController.self, making: makeEditorViewController)
复制代码
id<EditorViewController> makeEditorViewController(ZIKViewRouteConfiguration *config) {
return [[EditorViewController alloc] init];
}
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
factory:makeEditorViewController];
复制代码
有时候模块须要处理一些系统事件或者 app 的自定义事件,此时可让 router 子类实现,再进行遍历分发。
class SomeServiceRouter: ZIKServiceRouter {
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
复制代码
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
Router.enumerateAllViewRouters { (routerType) in
if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
}
}
Router.enumerateAllServiceRouters { (routerType) in
if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
}
}
}
}
复制代码
@interface SomeServiceRouter : ZIKServiceRouter
@end
@implementation SomeServiceRouter
+ (void)applicationDidEnterBackground:(UIApplication *)application {
// handle applicationDidEnterBackground event
}
@end
复制代码
@interface AppDelegate ()
@end
@implementation AppDelegate
- (void)applicationDidEnterBackground:(UIApplication *)application {
[ZIKAnyViewRouter enumerateAllViewRouters:^(Class routerClass) {
if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
[routerClass applicationDidEnterBackground:application];
}
}];
[ZIKAnyServiceRouter enumerateAllServiceRouters:^(Class routerClass) {
if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
[routerClass applicationDidEnterBackground:application];
}
}];
}
@end
复制代码
借助于使用接口管理依赖的方案,咱们在对模块进行单元测试时,能够自由配置 mock 依赖,并且无需 hook 模块内部的代码。
例如这样一个依赖于网络模块的登录模块:
// 登陆模块
class LoginService {
func login(account: String, password: String, completion: (Result<LoginError>) -> Void) {
// 内部使用 RequiredNetServiceInput 进行网络访问
let netService = Router.makeDestination(to: RoutableService<RequiredNetServiceInput
>())
let request = makeLoginRequest(account: account, password: password)
netService?.POST(request: request, completion: completion)
}
}
// 声明依赖
extension RoutableService where Protocol == RequiredNetServiceInput {
init() {}
}
复制代码
// 登陆模块
@interface LoginService : NSObject
@end
@implementation LoginService
- (void)loginWithAccount:(NSString *)account password:(NSString *)password completion:(void(^)(Result *result))completion {
// 内部使用 RequiredNetServiceInput 进行网络访问
id<RequiredNetServiceInput> netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination];
Request *request = makeLoginRequest(account, password);
[netService POSTRequest:request completion: completion];
}
@end
// 声明依赖
@protocol RequiredNetServiceInput <ZIKServiceRoutable>
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion;
@end
复制代码
在编写单元测试时,不须要引入真实的网络模块,能够提供一个自定义的 mock 网络模块:
class MockNetService: RequiredNetServiceInput {
func POST(request: Request, completion: (Result<NetError>) {
completion(.success)
}
}
复制代码
// 注册 mock 依赖
ZIKAnyServiceRouter.register(RoutableService<RequiredNetServiceInput>(),
forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? in
return MockNetService()
}
复制代码
@interface MockNetService : NSObject <RequiredNetServiceInput>
@end
@implementation MockNetService
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
completion([Result success]);
}
@end
复制代码
// 注册 mock 依赖
[ZIKServiceRouter registerServiceProtocol:ZIKRoutable(EditorViewInput) forMakingService:[MockNetService class]];
复制代码
对于那些没有接口交互的外部依赖,例如只是简单的跳转到对应界面,则只需注册一个空白的 proxy。
单元测试代码:
class LoginServiceTests: XCTestCase {
func testLoginSuccess() {
let expectation = expectation(description: "end login")
let loginService = LoginService()
loginService.login(account: "account", password: "pwd") { result in
expectation.fulfill()
}
waitForExpectations(timeout: 5, handler: { if let error = $0 {print(error)}})
}
}
复制代码
@interface LoginServiceTests : XCTestCase
@end
@implementation LoginServiceTests
- (void)testLoginSuccess {
XCTestExpectation *expectation = [self expectationWithDescription:@"end login"];
[[LoginService new] loginWithAccount:@"" password:@"" completion:^(Result *result) {
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
!error? : NSLog(@"%@", error);
}];
}
@end
复制代码
使用接口管理依赖,能够更容易 mock,剥除外部依赖对测试的影响,让单元测试更稳定。
使用接口管理模块时,还有一个问题须要注意。接口是会随着模块更新而变化的,这个接口已经被不少外部使用了,要如何减小接口变化产生的影响?
此时须要区分新接口和旧接口,区分版本,推出新接口的同时,保留旧接口,并将旧接口标记为废弃。这样使用者就能够暂时使用旧接口,渐进式地修改代码。
这部分能够参考 Swift 和 OC 中的版本管理宏。
接口废弃,能够暂时使用,建议尽快使用新接口代替:
// Swift
@available(iOS, deprecated: 8.0, message: "Use new interface instead")
复制代码
// Objective-C
API_DEPRECATED_WITH_REPLACEMENT("performPath:configuring:", ios(7.0, 7.0));
复制代码
接口已经无效:
// Swift
@available(iOS, unavailable)
复制代码
// Objective-C
NS_UNAVAILABLE
复制代码
最后,一个 router 的最终形态就是下面这样:
// editor 模块的 router
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView<EditorViewProtocol>())
registerURLPattern("app://editor/:title")
}
override func processUserInfo(_ userInfo: [AnyHashable : Any] = [:], from url: URL) {
let title = userInfo["title"]
// 处理 url 中的参数
}
// 子类重写,建立模块
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}
// 配置模块,注入静态依赖
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依赖
destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
// 其余配置
// 处理来自 url 的参数
if let title = configuration.userInfo["title"] as? String {
destination.title = title
} else {
destination.title = "默认标题"
}
}
// 事件处理
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
复制代码
// editor 模块的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
[self registerURLPattern:@"app://editor/:title"];
}
- (void)processUserInfo:(NSDictionary *)userInfo fromURL:(NSURL *)url {
NSString *title = userInfo[@"title"];
// 处理 url 中的参数
}
// 子类重写,建立模块
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
// 配置模块,注入静态依赖
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// 注入 service 依赖
destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
// 其余配置
// 处理来自 url 的参数
NSString *title = configuration.userInfo[@"title"];
if (title) {
destination.title = title;
} else {
destination.title = @"默认标题";
}
}
// 事件处理
+ (void)applicationDidEnterBackground:(UIApplication *)application {
// handle applicationDidEnterBackground event
}
@end
复制代码
咱们能够看到基于接口管理模块的优点:
回过头看以前的 8 个解耦指标,ZIKRouter 已经彻底知足。而 router 提供的多种模块管理方式(makeDestination、prepareDestination、依赖注入、页面跳转、storyboard 支持),可以覆盖大多数现有的场景,从而实现渐进式的模块化,减轻重构现有代码的成本。