If you don't want to depend same protocol in module and module's user, use adapter to make totally decouple. Then even modules depends on each other, they can build independently.
A router can register with multi protocols. The protocol provided by module itself is the provided protocol
. The protocol used inside the module's user is the required protocol
。
See Required Interface
and Provided Interface
in component diagramscomponent diagrams:
Read VIPER architecture to get more details about implementing Required Interface
and Provided Interface
.
App Context is responsible for adapting interfaces. The module's user uses Required Interface
, and the adapter forwards Required Interface
to Provided Interface
.
Those required protocol
in a module are actually its dependencies.
Add required protocol
for module with category and extension in app context.
For example, a module A needs to show a login view, and the login view can display a custom tip.
Module A:
protocol ModuleARequiredLoginViewInput {
var message: String? { get set } //Message displayed on login view
}
//Show login view in module A
Router.perform(
to: RoutableView<ModuleARequiredLoginViewInput>(),
path: .presentModally(from: self)
configuring { (config, _) in
config.prepareDestination = { destination in
destination.message = "Please login to read this note"
}
})
Objective-C Sample
@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@end
//Show login view in module A
[ZIKRouterToView(ModuleARequiredLoginViewInput)
performPath:ZIKViewRoutePath.presentModallyFrom(self)
configuring:^(ZIKViewRouteConfiguration *config) {
config.prepareDestination = ^(id<ModuleARequiredLoginViewInput> destination) {
destination.message = @"Please login to read this note";
};
}];
ZIKViewAdapter
and ZIKServiceAdapter
are responsible for registering protocols for other router.
Make login view support ModuleARequiredLoginViewInput
:
//Login Module Provided Interface
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
}
//Write in app context, make LoginViewRouter support ModuleARequiredLoginViewInput
class LoginViewAdapter: ZIKViewRouteAdapter {
override class func registerRoutableDestination() {
//If you can get the router, you can just register ModuleARequiredLoginViewInput to it
LoginViewRouter.register(RoutableView<ModuleARequiredLoginViewInput>())
//If you don't know the router, you can use adapter
register(adapter: RoutableView<ModuleARequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput>())
}
}
extension LoginViewController: ModuleARequiredLoginViewInput {
var message: String? {
get {
return notifyString
}
set {
notifyString = newValue
}
}
}
Objective-C Sample
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
//LoginViewAdapter.h
@interface LoginViewAdapter : ZIKViewRouteAdapter
@end
//LoginViewAdapter.m
@implementation LoginViewAdapter
+ (void)registerRoutableDestination {
//If you can get the router, you can just register ModuleARequiredLoginViewInput to it
[LoginViewRouter registerViewProtocol:ZIKRoutable(ModuleARequiredLoginViewInput)];
//If you don't know the router, you can use adapter
[self registerDestinationAdapter:ZIKRoutable(ModuleARequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
}
@end
//Make LoginViewController support ModuleARequiredLoginViewInput
@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
If you can't add required protocol
for module, for example, the delegate type in protocol is different:
protocol ModuleARequiredLoginViewDelegate {
func didFinishLogin() -> Void
}
protocol ModuleARequiredLoginViewInput {
var message: String? { get set }
var delegate: ModuleARequiredLoginViewDelegate { get set }
}
Objective-C Sample
@protocol ModuleARequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end
@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<ModuleARequiredLoginViewDelegate> delegate;
@end
Delegate is different in provided module:
protocol ProvidedLoginViewDelegate {
func didLogin() -> Void
}
protocol ProvidedLoginViewInput {
var notifyString: String? { get set }
var delegate: ProvidedLoginViewDelegate { get set }
}
Objective-C Sample
@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<ProvidedLoginViewDelegate> delegate;
@end
In this situation, you can create a new router to forward the real router, and return a proxy for the real destination:
class ModuleAReqiredLoginViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(/*proxy class*/)
register(RoutableView<ModuleARequiredLoginViewInput>())
}
override func destination(with configuration: ZIKViewRouteConfiguration) -> ModuleARequiredLoginViewInput? {
//Get real destination with ProvidedLoginViewInput's router
let realDestination: ProvidedLoginViewInput = LoginViewRouter.makeDestination()
//Proxy is responsible for forwarding ModuleARequiredLoginViewInput to ProvidedLoginViewInput
let proxy: ModuleARequiredLoginViewInput = ProxyForDestination(realDestination)
return proxy
}
}
Objective-C Sample
@implementation ModuleARequiredLoginViewRouter
+ (void)registerRoutableDestination {
//Register ModuleARequiredLoginViewInput with ModuleARequiredLoginViewRouter
[self registerView:/* proxy class*/];
[self registerViewProtocol:ZIKRoutable(ModuleARequiredLoginViewInput)];
}
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
//Get real destination with ProvidedLoginViewDelegate's router
id<ProvidedLoginViewInput> realDestination = [LoginViewRouter makeDestination];
//Proxy is responsible for forwarding ModuleARequiredLoginViewInput to ProvidedLoginViewInput
id<ModuleARequiredLoginViewInput> proxy = ProxyForDestination(realDestination);
return proxy;
}
@end
For simple objc classes, you can use NSProxy to create a proxy. For those complex classes such as UIViewController in UIKit, you can subclass the UIViewController, and override methods to adapt interface.
Separating required protocol
and provided protocol
makes your code truly modular. The caller declares its required protocol
, and the provided module can easily be replaced by another module with the same required protocol
.
Read the ZIKLoginModule
module in demo. The login module depends on an alert module, and the alert module is different in ZIKRouterDemo
and ZIKRouterDemo-macOS
. You can change the provided module without changing anything in the login module.
You don't have to always separate required protocol
and provided protocol
. It's OK to use the same protocol in module and its user. Or you can just make a copy and change the protocol name, letting required protocol
to be subset of provided protocol
. You only need to do adapting when changing to another provided module.
But adapting with category, extension, proxy and subclass will write much more code, you should not abuse the adapting.
There're several suggestions for decouple modules:
- Frameworks for unique functions can be directly used
- Some simple dependencies can be declared in module's interface and let the caller to inject them, such as logging function
- Most of modules that needed to decouple is a reusable business module. If the module is not necessary to be reusable, it can just use other modules directly
- Only do adapting when your module really allow multiple modules to provide the required protocol. Such as login view module allows different service module in different app.