Skip to content

Latest commit

 

History

History
241 lines (194 loc) · 9.03 KB

ModuleAdapter.md

File metadata and controls

241 lines (194 loc) · 9.03 KB

Module Adapter

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.

Provided protocol and Required protocol

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:

component 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 Interface for module

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

Forward Interface with Proxy

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.

Modularization

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.

When should you use an adapter?

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.