如果你不想让模块的调用者和模块都使用同一个 protocol,可以用模块适配彻底把两个模块解耦。此时即便模块间有相互依赖的情况,也可以让每个模块各自单独编译。
你可以为同一个 router 注册多个 protocol。模块本身提供的接口是provided protocol
,模块的调用者使用的接口是required protocol
。
在 UML 的组件图中,就很明确地表现出了这两者的概念。下图中的半圆就是Required Interface
,框外的圆圈就是Provided Interface
:
那么如何实施Required Interface
和Provided Interface
?在我的这篇文章iOS VIPER架构实践(二):VIPER详解与实现里有详细讲解过,应该由 App Context 在一个 adapter 里进行接口适配,从而使得调用者可以继续在内部使用Required Interface
,adapter 负责把Required Interface
和修改后的Provided Interface
进行适配。
这时候,调用者中的required protocol
就相当于是在声明自己所依赖的模块。
用 category、extension、proxy 类为模块添加required protocol
,工作全部由模块的使用和装配者 App Context 完成。
例如,某个界面A需要展示一个登陆界面,而且这个登陆界面可以显示一段自定义的提示语。
调用者模块示例:
protocol ModuleARequiredLoginViewInput {
var message: String? { get set } //显示在登陆界面上的自定义提示语
}
//Module A中调用Login模块
Router.perform(
to RoutableView<ModuleARequiredLoginViewInput>(),
path: .presentModally(from: self)
configuring { (config, _) in
config.prepareDestination = { destination in
destination.message = "请登录查看笔记详情"
}
})
Objective-C示例
@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
//Module A 中调用 Login 模块
[ZIKRouterToView(ModuleARequiredLoginViewInput)
performPath:ZIKViewRoutePath.presentModallyFrom(self)
configuring:^(ZIKViewRouteConfiguration *config) {
//配置目的界面
config.prepareDestination = ^(id<ModuleARequiredLoginViewInput> destination) {
destination.message = @"请登录查看笔记详情";
};
}];
ZIKViewAdapter
和ZIKServiceAdapter
专门负责为其他 router 添加 protocol。
在宿主 App Context 中让登陆模块支持ModuleARequiredLoginViewInput
:
//登陆界面提供的接口
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
}
//由App Context 实现,让登陆界面支持 ModuleARequiredLoginViewInput
class LoginViewAdapter: ZIKViewRouteAdapter {
override class func registerRoutableDestination() {
//如果可以获取到 router 类,可以直接为 router 添加 ModuleARequiredLoginViewInput
LoginViewRouter.register(RoutableView<ModuleARequiredLoginViewInput>())
//如果不能得到对应模块的 router,可以注册 adapter
register(adapter: RoutableView<ModuleARequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
}
}
extension LoginViewController: ModuleARequiredLoginViewInput {
var message: String? {
get {
return notifyString
}
set {
notifyString = newValue
}
}
}
Objective-C示例
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
//LoginViewAdapter.h,ZIKViewRouteAdapter 的子类
@interface LoginViewAdapter : ZIKViewRouteAdapter
@end
//LoginViewAdapter.m
@implementation LoginViewAdapter
+ (void)registerRoutableDestination {
//如果可以获取到 router 类,可以直接为 router 添加 ModuleARequiredLoginViewInput
[LoginViewRouter registerViewProtocol:ZIKRoutable(ModuleARequiredLoginViewInput)];
//如果不能得到对应模块的 router,可以注册 adapter
[self registerDestinationAdapter:ZIKRoutable(ModuleARequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
}
@end
//用Objective-C的 category、Swift 的 extension 进行接口适配
@interface LoginViewController (ModuleAAdapter) <ModuleARequiredLoginViewInput>
@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 ModuleARequiredLoginViewDelegate {
func didFinishLogin() -> Void
}
protocol ModuleARequiredLoginViewInput {
var message: String? { get set }
var delegate: ModuleARequiredLoginViewDelegate { get set }
}
Objective-C示例
@protocol ModuleARequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end
@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<ModuleARequiredLoginViewDelegate> delegate;
@end
而模块里的 delegate 接口不一样:
protocol ProvidedLoginViewDelegate {
func didLogin() -> Void
}
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
var delegate: ProvidedLoginViewDelegate { get set }
}
Objective-C示例
@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<ProvidedLoginViewDelegate> delegate;
@end
相同方法有不同参数类型时,可以用一个新的 router 代替真正的 router,在新的 router 里插入一个中介者,负责转发接口:
class ModuleAReqiredLoginViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(/* proxy 类*/);
register(RoutableView<ModuleARequiredLoginViewInput>())
}
override func destination(with configuration: ZIKViewRouteConfiguration) -> ModuleARequiredLoginViewInput? {
let realDestination: ProvidedLoginViewInput = LoginViewRouter.makeDestination()
//proxy 负责把 ModuleARequiredLoginViewInput 转发为 ProvidedLoginViewInput
let proxy: ModuleARequiredLoginViewInput = ProxyForDestination(realDestination)
return proxy
}
}
Objective-C示例
@implementation ModuleARequiredLoginViewRouter
+ (void)registerRoutableDestination {
//注册 ModuleARequiredLoginViewInput,和新的ModuleARequiredLoginViewRouter 配对,而不是目的模块中的 LoginViewRouter
[self registerView:/* proxy 类*/];
[self registerViewProtocol:ZIKRoutable(ModuleARequiredLoginViewInput)];
}
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
//用 LoginViewRouter 获取真正的 destination
id<ProvidedLoginViewInput> realDestination = [LoginViewRouter makeDestination];
//proxy 负责把 ModuleARequiredLoginViewInput 转发为 ProvidedLoginViewInput
id<ModuleARequiredLoginViewInput> proxy = ProxyForDestination(realDestination);
return mediator;
}
@end
对于普通类,proxy 可以用 NSProxy 来实现。对于 UIKit 中的那些复杂的 UI 类,可以用子类,然后在子类中重写方法,进行模块适配。
区分了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 等技术进行接口适配。
接口适配也不能滥用,因为成本比较高。对于模块间耦合的处理,有这么几条建议:
- 如果是特定功能模块间的互相依赖,直接引用类即可
- 如果是依赖某些简单的通用模块(例如日志模块),可以在模块的接口上把依赖交给外部来设置
- 大部分需要解耦的模块都是需要重用的业务模块,如果你的模块不需要重用,直接引用对应类即可
- 只有在你的业务模块的确允许使用者使用不同的依赖模块时,才进行多个接口间的适配。例如需要跨平台的模块,例如登录模块允许不同的 app 使用不同的登陆 service 模块
通过required protocol
和provided protocol
,就可以实现模块间的完全解耦。