There have been a lot of discussions about componentization. In the previous article iOS VIPER Architecture Practice (iii) : Interface Oriented Routing Design, AFTER comprehensive comparison of various schemes, I tend to use the interface oriented approach to componentization.

This is an article explaining module decoupling from the code level. It will show how to practice the idea of interface in an all-round way. It will try to comprehensively discuss various problems that need to be considered in the process of module management and decoupling, and give practical solutions as well as the corresponding open source module management tool: ZIKRouter. You can also use this article to adapt your existing solutions, even if your project is not componentized, for code decoupling.

Main contents:

  • How do you measure the degree of module decoupling
  • Compare the advantages and disadvantages of different schemes
  • Check static routes at compile time to avoid using modules that do not exist
  • How to decouple modules, including module reuse, module adaptation, inter-module communication and sub-module interaction
  • Module interface and dependency management
  • Switch logic of the management interface

directory

  • What is componentization
  • Why componentize
  • Whether your project needs to be componentized
  • Eight indicators of the componentization scheme
  • Scheme comparison
    • URL routing
    • Target – the Action plan
    • Protocol based matching scheme
  • Protocol-router Matching scheme
  • The risk of being dynamic
  • Static Route Check
  • The module of decoupling
    • Module classification
    • What is decoupling
    • Module reuse
  • Dependency management
    • Dependency injection
    • Separate module creation and configuration
    • Optional dependencies: property injection and method injection
    • Required dependency: Factory method
    • Avoid interface contamination
    • Rely on to find
    • Circular dependencies
  • Module adapter
    • Required Protocol and Provided Protocol
  • Intermodule communication
    • Control streams INPUT and output
    • Set input and Output
    • The child module
    • The Output of the adapter
  • Function extension
    • Automatic registration
    • Packaging interface jump
    • Custom jump
    • Support the storyboard
    • URL routing
    • Replace router subclasses with router objects
    • Simplified Router Implementation
    • The event processing
  • Unit testing
  • Interface Version Management
  • Final shape
  • Advantages of interface-based decoupling

What is componentization

Separate modules, layer them, and define how they communicate to each other to decouple and adapt to team development.

Why componentization

There are four main reasons:

  • Decoupling between modules
  • Module reuse
  • Improve team collaboration and development efficiency
  • Unit testing

As the project grows larger and larger, direct references between modules will lead to a lot of coupling, resulting in interface abuse, which will affect the whole system and be difficult to maintain when modifications are needed someday.

The main problems are as follows:

  • Changing the functionality of one module requires modifying the code of many other modules because this module is referenced by other modules
  • The external interface of the module is not clear, and the external will even call the private interface that should not be exposed, which will take a lot of time to modify
  • The modified modules cover a wide range, which can easily affect the development of other team members and cause code conflicts
  • When a module needs to be pulled out for reuse elsewhere, you find that coupling makes it impossible to pull out separately
  • Coupling between modules leads to confusion of interfaces and dependencies, making it difficult to write unit tests

So you need to reduce the coupling between modules and interact with them in a more formal way. This is called componentization, or modularization.

Whether your project needs to be componentized

Componentization is also not necessary, and in some cases it is not necessary:

  • Small project, simple interaction between modules, less coupling
  • A module is not referenced by multiple external modules, just a single small module
  • Modules do not need to be reused, and code is rarely modified
  • Small team size
  • No unit tests need to be written

Componentization also comes at a cost, as you spend time designing interfaces and separating code, so not all modules need to be componentized.

But consider componentization when you see these signs:

  • Module logic is complex and multiple modules reference each other frequently
  • As the project grows larger, it becomes more and more difficult to change the code
  • As teams grow, submitted code often conflicts with other members
  • Project compilation takes a lot of time
  • Unit tests of modules often fail due to changes made to other modules

Eight indicators of the componentization scheme

Once we’ve decided to start down the componentization path, we need to think about our goals. What does a componentization scheme need to achieve? Here ARE eight ideal metrics:

  1. There is no direct coupling between modules, and changes made within one module do not affect the other
  2. Modules can be compiled separately
  3. Data transfer between modules can be carried out clearly
  4. A module can be replaced at any time by another module that provides the same functionality
  5. The external interface of the module is easy to find and maintain
  6. When a module’s interface changes, external code that uses this module can be efficiently refactored
  7. Modularize existing projects with as little modification and code as possible
  8. Support for Objective-C and Swift, and mixing

The first four are used to measure whether a module is truly decoupled, and the last four are used to measure ease of use in project practice. The last one must support Swift, because Swift is an inevitable trend, and if your solution does not support Swift, it will have to change at some point in the future, and all modules implemented based on this solution will be affected.

Based on these 8 indicators, we can measure our plan to a certain extent.

Scheme comparison

There are three main componentization schemes: URL routing, target-Action, and Protocol matching.

Let’s take a look at each of these componentization options and see what their strengths and weaknesses are. This part has been covered in previous articles, so here’s a recomparison to fill in some details. It must be noted at the outset that there is no perfect solution that can meet the needs of all scenarios, and the most suitable solution needs to be selected according to the needs of each project.

URL routing

At present, most routing tools on iOS are based on URL matching, or based on naming conventions, using runtime method to dynamically call.

The advantage of these dynamic schemes is that they are simple to implement, while the disadvantage is that they need to maintain string tables, or rely on naming conventions, and cannot expose all problems at compile time, requiring errors to be discovered at run time.

Code examples:

// Register a URL
[URLRouter registerURL:@"app://editor" handler:^(NSDictionary *userInfo) {
    UIViewController *editorViewController = [[EditorViewController alloc] initWithParam:userInfo];
    return editorViewController;
}];
Copy the code
[URLRouter openURL:@"app:// Editor /? Debug =true" completion:^(NSDictionary *info) {}];Copy the code

Advantages of URL Router:

  • Highly dynamic, suitable for apps that often carry out operational activities, such as e-commerce
  • Routing rules of multiple platforms can be centrally managed
  • Easy to adapt to URL schemes

Disadvantages of URL Router:

  • There are limited ways to pass arguments, and no compiler can do parameter type checking, so all arguments have to be converted from strings
  • Applies only to interface modules, not to generic modules
  • You cannot use designated Initializer to declare required parameters
  • In order for the View Controller to support urls, we need to add initialization methods to it, so we need to make changes to the module
  • Do not support the storyboard
  • The interface provided by the module cannot be explicitly declared. You can only rely on the interface documentation and cannot ensure correct modifications during refactoring
  • Relies on hard coding of strings and is difficult to manage
  • There is no guarantee that the module being used exists
  • The decoupling ability is limited. The “registration”, “implementation” and “use” of URL must use the same character rules. Once any party makes modification, the code of other parties will become invalid and it is difficult to reconstruct

The problem of string decoupling

If measured by the above 8 indicators, URL routing can only meet the requirements of “support module compilation alone” and “support OC and Swift”. The degree of decoupling is very modest.

All string-based decoupling schemes are pseudo-decoupling in that they simply abandon compilation dependencies, but when the code changes, even if it compiles and runs, the logic is still wrong.

For example, modify the URL of the module definition:

// Register a URL [URLRouter registerURL:@"app://editorView" Handler :^(NSDictionary *userInfo) {...}];Copy the code

The caller’s URL must also be changed, and the code is still coupled, but the compiler can’t check it. This makes maintenance more difficult, and once the parameters in the URL are added or subtracted, or the decision is made to replace them with another module, the parameter names are changed, and there are few efficient ways to refactor the code. You can use macro definitions to manage strings, but this requires all modules to use the same header file and does not solve the problem of variable parameter types and numbers.

URL routing is suitable for network protocol interaction of remote modules, while in the management of local modules, the biggest or even the only advantage is suitable for apps that often operate across multiple terminals, because operators can uniformly manage the routing rules of multiple platforms.

On behalf of the framework

  • routable-ios
  • JLRoutes
  • MGJRouter
  • HHRouter

Improvements: Avoid string management

The way to improve URL routing is to avoid using strings through the interface management module.

Parameters can be passed directly through protocol, parameter types can be checked by the compiler, and in ZIKRouter, the module used can be guaranteed to exist through route declarations and compilation checks. There is also no need to modify the module’s code when creating a route for the module.

However, it must be acknowledged that URL routing is the most suitable solution for cross-platform routing management, despite its drawbacks. Therefore, ZIKRouter also supports URL routing. In addition to protocol management, the ZIKRouter can match the router with strings and interconnect with other URL router frameworks.

Target – the Action plan

There are some module management tools that dynamically acquire modules based on the Objective-C Runtime and category features. For example, get the class via NSClassFromString and create the instance, and call the method dynamically via performSelector: NSInvocation.

For example, the design based on target-Action mode roughly adds a new interface for the routing tool by category, obtains the corresponding class through the string in the interface, creates an instance with runtime, and dynamically calls the method of the instance.

Sample code:

@interface Mediator: NSObject + (instanceType)sharedInstance; - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params; @endCopy the code
// Define a new interface @interface Mediator (ModuleActions) - (UIViewController *)Mediator_editorViewController; @end@implementation Mediator (ModuleActions) - (UIViewController *)Mediator_editorViewController {// Hardcode with string, Create the Target_Editor dynamically from the Runtime and call Action_viewController: UIViewController *viewController = [self performTarget:@"Editor" action:@"viewController" params:@{@"key":@"value"}]; return viewController; } @mediatorShareDInstance = [[Mediator_editorViewController];} @mediatorShareDInstance = [Mediator_editorViewController];Copy the code
// The module provider provides the way target-action is invoked @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; } @endCopy the code

Advantages:

  • Category allows you to explicitly declare interfaces for compilation checks
  • The implementation is lightweight

Disadvantages:

  • It is necessary to add every interface in Mediator and target, and the code is cumbersome when modularizing
  • It still hardcodes strings in categories, internally uses dictionaries to pass parameters, and to some extent has the same problems as URL routing
  • There is no guarantee that the module being used exists, and after the target module is modified, the user will only find the error at run time
  • Too dependent on runtime features to apply to pure Swift. Arguments of pure Swift type cannot be used when extending Mediator in Swift
  • Too many Target classes may be created
  • Use the Runtime interface to call any method of any class. Be careful not to be audited by Apple. Are selector and response to selector banned by the App Store?

Dictionary parameter transfer problem

Dictionaries cannot guarantee the number and type of arguments. They rely on the calling convention. Just like strings, if one side makes changes, the other side must too.

In contrast to URL routing, target-Action Narrows the string management problem inside Mediator through the category interface, but it is not completely eliminated and there is still room for improvement in other areas. In fact, the above 8 indicators can only meet the second “support module compilation alone”, in addition to the interface related to the third, fifth and sixth points, it is better than URL routing.

On behalf of the framework

CTMediator

Improvement: Avoid dictionary parameter passing

The biggest advantage of target-action scheme is the lightweight implementation of the whole scheme, and also to a certain extent clear module interface. However, these interfaces need to be wrapped once with target-Action and each module needs to create a Target class, so it is much easier to manage interfaces directly with Protocol.

ZIKRouter avoids the use of Runtime fetching and calling modules, and therefore ADAPTS to OC and SWIFT. At the same time, the protocol matching method avoids the introduction of string hard coding, which can better manage the module and avoid dictionary parameter passing.

Protocol based matching scheme

Some module management tools or dependency injection tools also implement interface-based management. Protocol can then be used to retrieve classes and dynamically create instances.

BeeHive example code:

// Register module (protocol-class matching) [[BeeHive shareInstance] registerService:@protocol(EditorViewProtocol) service:[EditorViewController class]];Copy the code
Id <EditorViewProtocol> Editor = [[BeeHive shareInstance] createService:@protocol(EditorViewProtocol)];Copy the code

Advantages:

  • Using interface call, the type safety of parameter passing is realized
  • Directly use the protocol interface of the module without repeated encapsulation

Disadvantages:

  • All objects are created by the framework in limited ways, such as calling custom initialization methods without supporting external passed arguments
  • Create objects with OC Runtime, Swift is not supported
  • Only protocol and class matching is performed. More complex creation methods and dependency injection are not supported
  • It is not guaranteed that the protocol used has a corresponding module, nor can it be directly determined whether a protocol can be used to obtain modules

Protocol-block is easier to use than direct protocol-class matching. Such as Swinject.

Sample code for Swinject:

let container = Container(a)// Register module
container.register(EditorViewProtocol.self) { _ in
    return EditorViewController()}// Get the module
let editor = container.resolve(EditorViewProtocol.self)!
Copy the code

On behalf of the framework

BeeHive

Swinject

Improvement: Discrete management

BeeHive is similar to ZIKRouter, but all modules are created by BeeHive singleton after registration. The usage scenarios are very limited. For example, the pure Swift type is not supported, and custom initialization methods and additional dependency injection are not supported.

ZIKRouter takes this one step further. Instead of matching protocol and class directly, ZIKRouter matches protocol and router subclasses or Router objects, providing a way to create instances of modules in router subclasses. At this point, the responsibility for creating modules is shifted from BeeHive singletons to individual routers, from intensive to discrete, further improving scalability.

Protocol-router Matching scheme

With a protocol-router match, the code looks like this:

A Router parent class provides the basic methods:

class ZIKViewRouter: NSObject {...// Get the module
    public class func makeDestination -> Any? {
        let router = self.init(with: ViewRouteConfig())
        return router.destination(with: router.configuration) 
    }
  
    // Let subclasses override
    public func destination(with configuration: ViewRouteConfig) -> Any? {
        return nil}}Copy the code
Objective-C Sample
@interface ZIKViewRouter: NSObject
@end
  
@implementation ZIKViewRouter.// Get the module
+ (id)makeDestination {
    ZIKViewRouter *router = [self alloc] initWithConfiguration:[ZIKViewRouteConfiguration new]];
    return [router destinationWithConfiguration:router.configuration];
}

// Let subclasses override
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    return nil;
}
@end
Copy the code

Each module writes its own Router subclass:

// Router for the editor module
class EditorViewRouter: ZIKViewRouter {
    // Subclass override to create a module
    override func destination(with configuration: ViewRouteConfig) -> Any? {
        let destination = EditorViewController(a)return destination
    }
}
Copy the code
Objective-C Sample
// Router for the editor module
@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

// Subclass override to create a module- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { EditorViewController  *destination = [[EditorViewController alloc] init];return destination;
}

@end
Copy the code

Register the protocol and router classes together:

EditorViewRouter.register(RoutableView<EditorViewProtocol> ())Copy the code
Objective-C Sample
// Register the protocol and router
[EditorViewRouter registerViewProtocol:@protocol(EditorViewProtocol)];
Copy the code

We can then use protocol to get the Router class, and further get the module:

// Get the router class of the module
let routerClass = Router.to(RoutableView<EditorViewProtocol> ())// Get the EditorViewProtocol module
letdestination = routerClass? .makeDestination()Copy the code
Objective-C Sample
// Get the router class of the module
Class routerClass = ZIKViewRouter.toView(@protocol(EditorViewProtocol));
// Get the EditorViewProtocol module
id<EditorViewProtocol> destination = [routerClass makeDestination];
Copy the code

With the addition of a router intermediate layer, decoupling is suddenly enhanced:

  • You can add many generic extension interfaces to the Router, such as module creation, dependency injection, interface jump, interface removal, and even URL routing support
  • More detailed dependency injection and customization can be performed in each Router subclass
  • You can customize the way objects are created, such as custom initialization methods, factory methods, and can directly carry the existing creation code during refactoring without adding or modifying interfaces to the original class, reducing the amount of work in the modularization process
  • Multiple protocols can be matched to the same module
  • You can enable the module to perform interface adaptation, allowing external devices to add a new protocol to the router after adaptation to solve the compilation dependency problem
  • The returned object simply conforms to protocol and is no longer bound to a single class. Therefore, you can return different objects according to the conditions, for example, for different system versions, return different controls, so that the external focus on the interface

The risk of being dynamic

The problem with most componentization scenarios is that compilation checks are weakened or even discarded because modules have become highly dynamic.

When calling a module, how can I guarantee that the module exists? When referring to a class directly, the compiler gives a reference error if the class does not exist, but dynamic components cannot be checked statically.

For example, the URL address changes, but some urls in the code are not updated in time; When you retrieve a module using protocol, protocol does not register the corresponding module. These problems can only be found at run time.

So is there a way to have modules that are highly decoupled while still ensuring that the calling module exists at compile time?

The answer is YES.

Static Route Check

One of the most unique features of ZIKRouter is its ability to ensure that the protocol used exists, preventing the use of non-existent modules at compile time. This feature makes it easier and safer to manage the routing interfaces you use without having to check and maintain them in other complicated ways.

A compilation error occurs when the wrong protocol is used.

Undeclared protocol used in Swift:

Using an undeclared protocol in Objective-C:

This feature is implemented through two mechanisms:

  • Only protocols that have been declared routable can be used for routing; otherwise, a compilation error will occur
  • A routable protocol must have a corresponding module

Here’s how to implement a complete static type checking mechanism while maintaining dynamic decoupling.

Routing statement

How do I declare a protocol to be routable?

The key to implementing the first mechanism is to add a special attribute or type to the protocol, which generates a compilation error if the protocol does not conform to a specific type.

Native Xcode does not support such static checking, and this is where our creativity is tested.

Objective-c: Protocol inheritance chain

In Objective-C, a protocol can be required to inherit from a particular parent protocol, and the parent protocol inheritance chain of a protocol can be statically checked by the macro definition + protocol qualification.

For example, ZIKRouter gets the Router class like this:

@protocol ZIKViewRoutable
@end

@interface ZIKViewRouter()
@property (nonatomic, class, readonly) ZIKViewRouterType *(^toView)(Protocol<ZIKViewRoutable> *viewProtocol);
@end
Copy the code

ToView is provided as a class attribute to facilitate chained calls. This block receives a Protocol

* and returns the corresponding Router class.

Protocol

* indicates that the Protocol must inherit from ZIKViewRoutable. The normal protocol is of type protocol *, so if you pass @protocol(EditorViewProtocol) you get a compile warning.

If the protocol variable is qualified by protocol with a macro definition, and a cast is performed, the compiler can check the protocol inheritance chain:

ZIKViewRoutable @protocol EditorViewProtocol <ZIKViewRoutable> @endCopy the code
// macro definition, #define ZIKRoutable(RoutableProtocol) (Protocol<RoutableProtocol>*)@protocol(RoutableProtocol)Copy the code
Router zikViewrouter.toView (ZIKRoutable(EditorViewProtocol))Copy the code

ZIKRoutable(EditorViewProtocol) expands to (Protocol

*)@protocol(EditorViewProtocol), Type: Protocol

*. Protocol

* is a subtype of Protocol

* in Objective-C, and the compiler will not warn.



However, if the incoming protocol does not inherit from ZIKViewRoutable, for example, ZIKRoutable(UndeclaredProtocol) is of type protocol

*, UndeclaredProtocol does not inherit from ZIKViewRoutable. Therefore Protocol

* is not a subtype of Protocol

*, and the compiler will warn of type errors. The Incompatible Pointer types warning can be turned into a compilation error in Build Settings.


Finally, zikviewrouter.toview (ZIKRoutable(EditorViewProtocol)) simplifies to ZIKViewRouterToView(EditorViewProtocol) with a macro definition, It is easy to statically check the protocol type when obtaining the router.

Swift: Conditional extension

Swift does not support macro definitions and does not allow arbitrary type conversions, so you need a different approach to compile checking.

You can pass protocol as a generic type of a struct, and then use conditional extensions to add initialization methods to structs of a particular generic type, so that undeclared generic types cannot create structs directly.

Such as:

// Use the RoutableView generics to pass protocol
struct RoutableView<Protocol> {
    // Disable default initialization methods
    @available(*, unavailable, message: "Protocol is not declared as routable")
    public init() {}}Copy the code
// Generics are extensions of EditorViewProtocol
extension RoutableView where Protocol= =EditorViewProtocol {
    // allow initialization
    init() {}}Copy the code
// This can be initialized when the generics are EditorViewProtocol
RoutableView<EditorViewProtocol> ()// Undeclared generics cannot be initialized, resulting in a compilation error
RoutableView<UndeclaredProtocol> ()Copy the code

Xcode can also provide auto-completion by listing all declared protocols:

Routing check

With route declarations, we limit the protocol used at compile time. The next step is to ensure that the declared protocol must have a corresponding module, similar to the application in the link phase, which checks that the declared class in the header file must have a corresponding implementation.

This step is not directly implemented at compile time, but it can be implemented at startup by looking at the way iOS checks dynamic libraries at startup.

Objective – C: protocol traversal

When the app starts in DEBUG mode, we can iterate over all protocols inherited from ZIKViewRoutable, check in the registry to see if there is a corresponding router, and if not, give an assertion error.

Alternatively, you can have the Router register classes used to create modules:

EditorViewRouter.registerView(EditorViewController.self)
Copy the code
Objective-C Sample
// Register the protocol and router
[EditorViewRouter registerView:[EditorViewController class]].Copy the code

To further check whether the class in the router complies with the corresponding protocol. The whole type checking process is complete.

Swift: symbol traversal

However, protocol in Swift is static and cannot be traversed directly through the OC Runtime. Is it impossible to check dynamically? You can do it with creativity.

Swift’s generic name is reflected in the symbol name. For example, the init method declared above:

// In MyApp, generics are extensions of EditorViewProtocol
extension RoutableView where Protocol= =EditorViewProtocol {
    // allow initialization
    init() {}}Copy the code

ZRouter.RoutableView.init() -> ZRouter. RoutableView < MyApp. EditorViewProtocol >.

At this point, we can traverse the symbol table of the app to find all extensions of the RoutableView, so as to extract all declared Protocol types, and then check whether there is a corresponding Router.

Swift, the Runtime and ABI

However, if you further check that the class in the Router complies with the Protocol in the Router, you run into problems. How do I check that an arbitrary class complies with a Swift Protocol in Swift?

Swift does not provide a function such as class_conformsToProtocol directly, but we can do the same with the standard function provided by Swift Runtime and the memory structure defined in the Swift ABI.

The code for this section is _swift_typeIsTargetType. I’ll write several articles later that go into detail about the underlying Swift ABI.

The route checking part is only done in DEBUG mode, so you can let it go.

Automatically infer the return value type

[[BeeHive shareInstance] createService: @Protocol (EditorViewProtocol)]] returns an ID. The user needs to manually specify the type of the variable to be returned, and in Swift, manual type conversion is required, which can be error-prone and cannot be checked by the compiler. To achieve the most complete type checking, this problem cannot be ignored.

Is there a way to match the type of the return value to the type of the protocol? Generics in OC come into play here.

Module generics can be declared on the Router:

@interface ZIKViewRouter<__covariant Destination.__covariant RouteConfig: ZIKViewRouteConfiguration* > :NSObject
@end
Copy the code

Two generic parameters, Destination and RouteConfig, represent the module type managed by the router and the RouteConfig type, respectively. __covariant means that the generic supports covariant, meaning that the child type can be used in the same way as the parent type.

After declaring generic parameters, we can use generics in method parameter declarations:

@interface ZIKViewRouter<__covariant Destination.__covariant RouteConfig: ZIKViewRouteConfiguration* > :NSObject

- (nullable Destination)makeDestination;

- (nullable Destination)destinationWithConfiguration:(RouteConfig)configuration;

@end
Copy the code

[router] [protocol] [router] [protocol] [router] [protocol] [router]

#define ZIKRouterToView(ViewProtocol) [ZIKViewRouter<id<ViewProtocol>,ZIKViewRouteConfiguration *> toView](ZIKRoutable(ViewProtocol))
Copy the code

Using ZIKRouterToView (EditorViewProtocol) access to the router type is ZIKViewRouter < id < EditorViewProtocol >, ZIKViewRouteConfiguration * >. When makeDestination is called on this router, the return value is of type ID

, thus achieving complete type passing.

In Swift, function generics are used directly:

class Router {
    
    static func to<Protocol>(_ routableView: RoutableView<Protocol>) -> ViewRouter<Protocol.ViewRouteConfig>?
    
    }
Copy the code

Using router. to(RoutableView

()), the Router type obtained is ViewRouter

? When makeDestination is called, the return value type is EditorViewProtocol without manual type conversion.
,>

If you use protocol combinations, you can also specify multiple types at the same time:

typealias EditorViewProtocol = UIViewController & EditorViewInput
Copy the code

And when overriding the corresponding method in the Router subclass, generics can be used to further ensure that the type is correct:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol.ZIKViewRouteConfiguration> {
    
    override func destination(with configuration: ZIKViewRouteConfiguration) -> EditorViewProtocol? {
        // When the function is overwritten, the parameter types are the same as those of the generic type, and the implementation ensures that the return value is of the correct type
        return EditorViewController()}}Copy the code

We now have a complete type checking mechanism that supports both OC and Swift.

At this point, an interface-based, type-safe module management tool is complete. Creating a module using makeDestination is only the most basic feature. There are many useful features that can be extended to the parent Router, such as dependency injection, interface hopping, and interface adaptation, for better interface oriented development.

The module of decoupling

So what else do we need when programming to interfaces? Before extending, let’s discuss how to use interfaces for module decoupling, first from a theoretical perspective, and then to turn the theory into a tool.

Module classification

Different modules have different requirements for decoupling. Modules can be hierarchically classified from low to high:

  • The bottom functional module has a single function and certain versatility, such as various functional components (log, database). The main purpose of the underlying module is reuse
  • Common business modules in the middle tier that can be common across different projects. It references various underlying modules and communicates with other business modules
  • Special functional modules in the middle layer, which provide unique functions and are not generic, may reference some low-level modules, such as performance monitoring modules. Such modules can be directly referenced by other modules without much consideration for decoupling between modules
  • Upper-level proprietary business modules that belong to businesses unique to a project. It references various low-level modules and communicates with other business modules. The difference with the middle layer is that the decoupling requirements of the upper layer are not as high as those of the middle layer

What is decoupling

First, clarify what decoupling is. Sorting through the problem can help clarify our goals.

The purpose of decoupling is basically two: to improve code maintainability and module reuse. The guiding principle is object – oriented design principle.

There are also different degrees of decoupling, from low to high, which can be roughly divided into three levels:

  1. Modules interact using abstract interfaces, without direct type coupling, and changes within one module do not affect the other (single responsibility, dependency inversion)
  2. Modules are reusable and can be compiled separately (interface isolation, dependency inversion, inversion of control)
  3. A module can be replaced at any time by another module that provides the same functionality (open closed principle, dependency inversion, inversion of control)

Layer 1: Abstract interfaces and extract dependencies

The first level of decoupling is to reduce dependencies between different code and make it easier to maintain. For example, replace the class with protocol, isolating the module’s private interface and minimizing dependencies.

The whole process of decoupling is the process of sorting out and managing dependencies. Therefore, modules should be as cohesive as possible, with as few external dependencies as possible, which makes maintenance easier.

If the module doesn’t need to be reused, it’s basically enough at this layer.

Layer 2: module reuse, management module communication

The second layer of decoupling is to separate the code, so that modules can be reused and can be maintained by different members, which puts forward higher requirements for communication between modules. Modules need to declare external dependencies in their interfaces, removing coupling to a particular type.

At this point, the biggest impact is the way modules communicate, sometimes even if it can be compiled separately, it does not mean decoupling. For example, in URL routing, the coupling relationship still exists in the URL string, and the URL of one side changes, the code logic of the other side breaks, so it is still logically coupled. So all schemes based on some sort of implicit invocation convention, such as string matching, are simply de-compile-checking, not true decoupling.

Some say that using protocol for module to module communication leads to module and protocol coupling. This view is wrong. Protocol is a more efficient way to explicitly extract module dependencies. Otherwise, it will be very difficult to maintain the interface name, parameter type and parameter number of the module once the interface name, parameter type and parameter number need to be updated.

Furthermore, it is possible to remove dependencies on a particular protocol through design patterns, as explained below.

Layer 3: Remove implicit conventions

The third layer of decoupling is true decoupling between modules, as long as the two modules provide the same functionality, they can be seamlessly replaced without any modification by the caller. The module to be replaced only needs to provide the interface with the same function, through the adapter docking, without any other restrictions, there is no other implicit invocation convention.

This decoupling requirement is generally applied to modules that are common across projects, but not to business modules that are proprietary within projects. However, modules and remote modules that span multiple endpoints cannot achieve such decoupling, because there is no unified way to define interfaces across multiple endpoints, so interfaces can only be defined by implicit conventions or network protocols, such as URL routing.

In general, the decoupling process consists of responsibility separation, dependency management (dependency declaration and injection), and module communication.

Module reuse

To achieve module reuse, modules need to reduce external dependencies as far as possible, and extract the dependencies, reflected in the module interface, let the caller active injection. At the same time, various events of the module are also extracted for the caller to process.

In this way, the module is only responsible for its own logic and does not care how the caller uses the module. The application-layer logic that is unique to each application is separated from the modules.

Therefore, to do module decoupling well, managing dependencies is very important. The Protocol interface is the most efficient way to manage dependencies.

Dependency management

Dependencies are external data and external modules used in a module. Next we discuss how to manage dependencies using Protocol and demonstrate how to implement it using Router.

Dependency injection

Let’s review the concept of dependency injection. Dependency injection and dependency lookup are concrete ways to implement the idea of inversion of control.

Inversion of control is to change the acquisition of object dependencies from active to passive, directly reference and obtain the dependencies from the object, to provide the dependencies required by the object from the outside, and transfer the responsibility that does not belong to oneself, so as to decouple the object from its dependencies. At this point, the initiative of the control flow is transferred from the inside to the outside, so it is called inversion of control.

Dependency injection is the passing of dependencies from the outside into an object.

A class A represents internally needed dependencies in its interface (such as internally needed instances of class B), allowing the consumer to inject these dependencies externally rather than directly referencing the dependencies and creating class B internally. Dependencies can be declared as protocol, which decouples class A from the dependency class B used.

Separate module creation and configuration

So how do you use router for dependency injection?

Once a module has created an instance, it often needs to do some configuration. Module management tools should provide configuration capabilities by design.

The most simple way, is in the destinationWithConfiguration: create a destination configured. But we can go a step further and separate the creation and configuration of destinations. After separation, the Router can separately provide configuration capabilities to configure destinations that are not created by the Router, such as views created in storyboards and instance objects returned in various interface callbacks. This can cover more existing usage scenarios and reduce code changes.

Prepare Destination

Can be found in the router in the subclass prepareDestination: configuration: in the module configuration, namely dependency injection, and the module of the caller do not need to care about this part is dependent on how to configure:

/ / the router parent class
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 {
            // Call the module configuration method in the router parent class
            router.prepareDestination(destination, configuration: router.configuration)
        }
        return destination
    }
  
    // create a module and let subclasses override it
    public func destination(with configuration: ViewRouteConfig) -> Destination? {
        return nil
    }
    // Module configuration, let subclasses override
    func prepareDestination(_ destination: Destination, configuration: RouteConfig){}}// Router for the editor module
class EditorViewRouter: ZIKViewRouter<EditorViewController.ViewRouteConfig> {
    
    override func destination(with configuration: ViewRouteConfig) -> EditorViewController? {
        let destination = EditorViewController(a)return destination
    }
    // Configure the module to inject static dependencies
    override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
        // Inject a service dependency
        destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput> ())// Other configurations
        destination.title = "Default title"}}Copy the code
Objective-C Sample
/ / the router parent class
@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) {
        // Call the module configuration method in the router parent class
        [router prepareDestination:destination configuration:router.configuration];
    }
    return destination;
}

// create a module and let subclasses override it
- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    return nil;
}
// Module configuration, let subclasses override
- (void)prepareDestination:(id)destination configuration:(ZIKViewRouteConfiguration *)configuration {
    
}
@end

// Router for the editor module
@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { EditorViewController  *destination = [[EditorViewController alloc] init];return destination;
}
// Configure the module to inject static dependencies
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
    // Inject a service dependency
    destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
    // Other configurations
    destination.title = @" Default title";
}

@end
Copy the code

In this case, if some objects in the caller are not created from the router, they can be directly configured with the corresponding router to perform dependency injection:

var destination: EditorViewProtocol=...Router.to(RoutableView<EditorViewProtocol> ())? .prepare(destination: destination, configuring: { (config,_) in
    
})
Copy the code
Objective-C Sample
id<EditorViewProtocol> destination = ...
[ZIKRouterToView(EditorViewProtocol) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
    
}];
Copy the code

Separate configuration capabilities can be useful in some scenarios, especially when refactoring existing code. Some system interfaces are designed to return objects in the interface, but these objects are automatically created by the system, not by the Router, and therefore need to be configured by the Router, such as the View Controller created in storyboard. At this time will be after the view controller modular, can still maintain the existing code, just call a prepareDestination: configuration, configuration, modular process can minimize code revision.

Optional dependencies: property injection and method injection

When dependencies are optional and not required for object creation, property injection and method injection can be used.

Property injection refers to setting the properties of an object externally. Method injection refers to the external invocation of an object’s methods, passing in dependencies.

protocol PersonType {
    var wife: Person? { get set } // Optional attribute dependencies
    func addChild(_ child: Person) -> Void // Optional method injection
}
protocol Child {
    var parent: Person { get}}class Person: PersonType {
    var wife: Person? = nil
    var childs: Set<Child> = []
    func addChild(_ child: Child) {
        childs.insert(child)
    }
}
Copy the code
Objective – C example
@protocol PersonType: ZIKServiceRoutable
@property (nonatomic.strong.nullable) Person *wife; // Optional attribute dependencies
- (void)addChild:(Person *)child; // Optional method injection
@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
Copy the code

In the Router, you can inject some default dependencies:

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

    // Configure the module to inject static dependencies
    override func prepareDestination(_ destination: Person, configuration: PerformRouteConfig) {
        ifdestination.wife ! =nil {
            return
        }
        // Set the default value
        let wife: Person=... person.wife = wife } }Copy the code
Objective – C example
@interface PersonRouter: ZIKServiceRouter<Person *, ZIKPerformRouteConfiguration* >
@end
@implementation PersonRouter

- (nullable Person *)destinationWithConfiguration:(ZIKPerformRouteConfiguration *)configuration {
    Person *person = [Person new];
    return person;
}
// Configure the module to inject static dependencies
- (void)prepareDestination:(Person *)destination configuration:(ZIKPerformRouteConfiguration *)configuration {
    if(destination.wife ! =nil) {
        return;
    }
    Person *wife = ...
    destination.wife = wife;
}

@end
Copy the code

Parameter transfer between modules

At the same time as the routing operation, the caller can also inject a dependency dynamically with PersonType, that is, pass a parameter to the module.

Configuration is used to extend various functions. The Router can provide a prepareDestination on the Configuration and let the caller set it so that the caller can configure the destination.

let wife: Person=...let child: Child=...let person = Router.makeDestination(to: RoutableService<PersonType>(), configuring: { (config, _) in
    // Get the module and configure it
    config.prepareDestination = { destination in
        destination.wife = wife
        destination.addChild(child)
    }
})
Copy the code
Objective – C example
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType) 
         makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration *config) {
    // Get the module and configure it
    config.prepareDestination = ^(id<PersonType> destination) {
        destination.wife = wife;
        [destination addChild:child];
    };
}];
Copy the code

It can be encapsulated into a simpler interface:

let wife: Person=...let child: Child=...let person = Router.makeDestination(to: RoutableService<PersonType>(), preparation: { destination in
            destination.wife = wife
            destination.addChild(child)
        })
Copy the code
Objective – C example
Person *wife = ...
Child *child = ...
Person *person = [ZIKRouterToService(PersonType) 
         makeDestinationWithPreparation:^(id<PersonType> destination) {
            destination.wife = wife;
            [destination addChild:child];
        }];
Copy the code

Required dependency: Factory method

Some parameters are required before the Destination class is created, such as parameters in the initialization method, which are required dependencies.

class Person: PersonType {
    let name: String
    // Initialize the method with the required parameters
    init(name: String) {
        self.name = name
    }
}
Copy the code
Objective – C example
@interface Person: NSObject <PersonType>
@property (nonatomic.strong) NSString *name;
// Initialize the method with the required parameters
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;
@end
Copy the code

These required parameters are sometimes provided by the caller. This “required” feature is not present in URL routing and can be easily implemented as an interface.

Passing required dependencies requires a factory pattern, declaring required parameters and module interfaces on factory methods.

protocol PersonTypeFactory {
  // Factory method that declares the required parameter name and returns destination of type PersonType
    func makeDestinationWith(_ name: String) -> PersonType?
}
Copy the code
Objective – C example
@protocol PersonTypeFactory: ZIKServiceModuleRoutable
// Factory method that declares the required parameter name and returns destination of type PersonType
- (id<PersonType>)makeDestinationWith:(NSString *)name;
@end
Copy the code

So how do you pass the required parameters with router?

The Configuration of the Router can be used for custom parameter extension. You can save the required parameters to Configuration, or more directly, configuration provides the factory method, and then use the protocol of the factory method to get the module:

// Generic Configuration, which provides custom factory methods
class PersonModuleConfiguration: PerformRouteConfig.PersonTypeFactory {
    // Factory method
    public func makeDestinationWith(_ name: String) -> PersonType? {
        self.makedDestination = Person(name: name)
        return self.makedDestination
    }
    // The destination created by the factory method is provided to the router
    public var makedDestination: Destination?
}
Copy the code
Objective – C example
// Generic Configuration, which provides custom factory methods
@interface PersonModuleConfiguration: ZIKPerformRouteConfiguration<PersonTypeFactory>
// The destination created by the factory method is provided to the router
@property (nonatomic.strong.nullable) id<PersonTypeFactory> makedDestination;
@end
  
@implementation PersonModuleConfiguration
// Factory method- (id<PersonTypeFactory>)makeDestinationWith:(NSString *)name {
    self.makedDestination = [[Person alloc] initWithName:name];
    return self.makedDestination;
}
@end
Copy the code

Using a custom configuration on the Router:

class PersonRouter: ZIKServiceRouter<Person.PersonModuleConfiguration> {
    / / rewrite defaultRouteConfiguration, using a custom configuration
    override class func defaultRouteConfiguration() - >PersonModuleConfiguration {
        return PersonModuleConfiguration()}override func destination(with configuration: PersonModuleConfiguration) -> Person? {
        // Destination created using the factory method
        return config.makedDestination
    }
}
Copy the code
Objective – C example
@interface PersonRouter: ZIKServiceRouter<id<PersonType>, PersonModuleConfiguration* >
@end
@implementation PersonRouter
  
/ / rewrite defaultRouteConfiguration, using a custom configuration
+ (PersonModuleConfiguration *)defaultRouteConfiguration {
    return [PersonModuleConfiguration new];
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(PersonModuleConfiguration *)configuration {
    // Destination created using the factory method
    return configuration.makedDestination;
}

@end
Copy the code

Then register the PersonTypeFactory protocol with the router:

PersonRouter.register(RoutableServiceModule<PersonTypeFactory> ())Copy the code
Objective – C example
[PersonRouter registerModuleProtocol:ZIKRoutable(PersonTypeFactory)];
Copy the code

To get the module, use PersonTypeFactory:

let name: String=...Router.makeDestination(to: RoutableServiceModule<PersonTypeFactory>(), configuring: { (config, _) in
    // config complies with PersonTypeFactory
    config.makeDestinationWith(name)
})
Copy the code
Objective – C example
NSString *name = ...
ZIKRouterToServiceModule(PersonTypeFactory) makeDestinationWithConfiguring:^(ZIKPerformRouteConfiguration<PersonTypeFactory> *config) {
    // config complies with PersonTypeFactory
    [config makeDestinationWith:name];
}]
Copy the code

Replace the Configuration subclass with generics

If you don’t need to save other custom parameters on Configuration and don’t want to create too many Configuration subclasses, you can use a generic generic class to achieve the effect of subclass rewriting.

Generics allow custom parameter types, in which case factory methods can be stored directly as blocks on the configuration property.

// Generic Configuration, which provides custom factory methods
class ServiceMakeableConfiguration<Destination.Constructor> :PerformRouteConfig {    
    public var makeDestinationWith: Constructor
    public var makedDestination: Destination?
}
Copy the code
Objective – C example
@interface ZIKServiceMakeableConfiguration<__covariant Destination> :ZIKPerformRouteConfiguration
@property (nonatomic.copy) Destination(^makeDestinationWith)();
@property (nonatomic.strong.nullable) Destination makedDestination;
@end
Copy the code

Using a custom configuration on the Router:

class PersonRouter: ZIKServiceRouter<Person.PerformRouteConfig> {
    
    / / rewrite defaultRouteConfiguration, using a custom configuration
    override class func defaultRouteConfiguration() - >PerformRouteConfig {
        let config = ServiceMakeableConfiguration<PersonType, (String) - >PersonType> ({_ in})
        // Set up the factory method for the caller to use
        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 created using the factory method
            return config.makedDestination
        }
        return nil}}// Make the configuration corresponding to the generic conform to the PersonTypeFactory
extension ServiceMakeableConfiguration: PersonTypeFactory where Destination= =PersonType.Constructor= = (String) - >PersonType {}Copy the code
Objective – C example
@interface PersonRouter: ZIKServiceRouter<id<PersonType>, ZIKServiceMakeableConfiguration* >
@end
@implementation PersonRouter

/ / rewrite defaultRouteConfiguration, using a custom configuration
+ (ZIKServiceMakeableConfiguration *)defaultRouteConfiguration {
    ZIKServiceMakeableConfiguration *config = [ZIKServiceMakeableConfiguration new];
    __weak typeof(config) weakConfig = config;
    // Set up the factory method for the caller to use
    config.makeDestinationWith = id^ (NSString *name) {
        weakConfig.makedDestination = [[Person alloc] initWithName:name];
        return weakConfig.makedDestination;
    };
    return config;
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
    // Destination created using the factory method
    return configuration.makedDestination;
}

@end
Copy the code

Avoid interface contamination

In addition to required dependencies, there are parameters that do not belong to the Destination class, but belong to other components in the module, and cannot be passed through the Destination interface. In MVVM and VIPER architectures, for example, model parameters cannot be passed to the View. Instead, they should be passed to the View Model or interactor. The same pattern can be used at this point.

protocol EditorViewModuleInput {
  // Factory method, which declares the parameter note, returns destination of type EditorViewInput
    func makeDestinationWith(_ note: Note) -> EditorViewInput?
}
Copy the code
Objective – C example
@protocol EditorViewModuleInput: ZIKViewModuleRoutable
// Factory method, which declares the parameter note, returns destination of type EditorViewInput
- (id<EditorViewInput>)makeDestinationWith:(Note *)note;
@end
Copy the code
class EditorViewRouter: ZIKViewRouter<EditorViewInput.ViewRouteConfig> {
    
    / / rewrite defaultRouteConfiguration, using a custom configuration
    override class func defaultRouteConfiguration() - >ViewRouteConfig {
        let config = ViewMakeableConfiguration<EditorViewInput, (Note) - >EditorViewInput> ({_ in})
        // Set up the factory method for the caller to use
        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(a)let presenter = EditorViewPresenter(view)
        let interactor = EditorInteractor(Presenter)
        // Pass the model to the data manager. The view does not touch the model
        interactor.note = note
        return view
    }

    override func destination(with configuration: ViewRouteConfig) -> EditorViewInput? {
        if let config = configuration as? ViewMakeableConfiguration<EditorViewInput, (Note) - >EditorViewInput> {
            // Destination created using the factory method
            return config.makedDestination
        }
        return nil}}Copy the code
Objective – C example
@interface EditorViewRouter: ZIKViewRouter<id<EditorViewInput>, ZIKViewMakeableConfiguration* >
@end
@implementation PersonRouter

/ / rewrite defaultRouteConfiguration, using a custom configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
    ZIKViewMakeableConfiguration *config = [ZIKViewMakeableConfiguration new];
    __weak typeof(config) weakConfig = config;
    // Set up the factory method for the caller to use
    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];
    // Pass the model to the data manager. The view does not touch the model
    interactor.note = note;
    return view;
}
  
- (nullable id<EditorViewInput>)destinationWithConfiguration:(ZIKViewMakeableConfiguration *)configuration {
    // Destination created using the factory method
    return configuration.makedDestination;
}

@end
Copy the code

We can get the module with EditorViewModuleInput:

let note: Note=...Router.makeDestination(to: RoutableViewModule<EditorViewModuleInput>(), configuring: { (config, _) in
    // config follows EditorViewModuleInput
    config.makeDestinationWith(note)
})
Copy the code
Objective – C example
Note *note = ...
ZIKRouterToViewModule(EditorViewModuleInput) makeDestinationWithConfiguring:^(ZIKViewRouteConfiguration<EditorViewModuleInput> *config) {
    // config follows EditorViewModuleInput
    config.makeDestinationWith(note);
}]
Copy the code

Rely on to find

When a module has many required dependencies, a very long method can occur if the dependencies are placed in the initialization interface.

In addition to having modules declare dependencies in interfaces, modules can also use a module management tool to dynamically find dependencies within a module, for example, using router to find the module corresponding to protocol. To use this pattern, all modules need to use the same module management tool uniformly.

The code is as follows:

class EditorViewController: UIViewController {
    lazy var storageService: EditorStorageServiceInput {
        return Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())!
    }
}
Copy the code
Objective – C example
@interface EditorViewController : UIViewController(a)
@property (nonatomic.strong) id<EditorStorageServiceInput> storageService;
@end
@implementation EditorViewController
  
- (id<EditorStorageServiceInput>)storageService {
    if(! _storageService) { _storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination]; }return _storageService;
}
  
@end
Copy the code

Circular dependencies

When using dependency injection, there are special cases to deal with, such as the infinite recursion of circular dependencies.

Circular dependencies are when two objects depend on each other.

When a dependency is dynamically injected inside the Router, it must be declared in protocol if the injected dependency also depends on the injected object.

protocol Parent {
    // Parent depends on Child
    var child: Child { get set}}protocol Child {
    // Child depends on Parent
    var parent: Parent { get set}}class ParentObject: Parent {
    var child: Child!
}

class ChildObject: Child {
    var parent: Parent!
}
Copy the code
Objective – C example
@protocol Parent <ZIKServiceRoutable>
// Parent depends on Child
@property (nonatomic.strong) id<Child> child;
@end

@protocol Child <ZIKServiceRoutable>
// Child depends on Parent
@property (nonatomic.strong) id<Parent> parent;
@end

@interface ParentObject: NSObject<Parent>
@end

@interface ParentObject: NSObject<Child>
@end
Copy the code
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
        }
        // Actively look for dependencies only if no external child is set
        let child = Router.makeDestination(to RoutableService<Child>(), preparation { child in
            // Set the child dependency to prevent the child from finding the parent dependency, resulting in a loop
            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
        }
        // Actively look for dependencies only if no parent is set externally
        let parent = Router.makeDestination(to RoutableService<Parent>(), preparation { parent in
            // Set parent dependencies to prevent parent from finding child dependencies and causing loops
            parent.child = destination
        })
        destination.parent = parent
    }
}
Copy the code
Objective – C example
@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;
    }
    // Actively look for dependencies only if no external child is set
    destination.child = [ZIKRouterToService(Child) makeDestinationWithPreparation:^(id<Child> child) {
        // Set the child dependency to prevent the child from finding the parent dependency, resulting in a loop
        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;
    }
    // Actively look for dependencies only if no parent is set externally
    destination.parent = [ZIKRouterToService(Parent) makeDestinationWithPreparation:^(id<Parent> parent) {
        // Set parent dependencies to prevent parent from finding child dependencies and causing loops
        parent.child = destination;
    }];
}

@end
Copy the code

This avoids the problem of infinite recursion caused by circular dependencies.

Module adapter

When using the Protocol management module, protocol is bound to appear in more than one module. So how do you make each module compile separately at this point?

One way to do this is to make a copy of the protocol for each module in use without changing the protocol name. Xcode does not report errors.

Another approach is to use the adapter pattern, which allows different modules to interact with the same module using different protocols.

Required Protocol and Provided Protocol

You can register multiple protocols for the same router.

Interfaces can be classified into required Protocol and Provided Protocol based on their dependencies. The module itself provides the Provided Protocol interface, and the module’s caller needs to use the Required Protocol interface.

Required Protocol is a subset of Provided Protocol, and callers need only declare the interfaces they use, rather than importing the entire Provided Protocol, further reducing the coupling between modules.

In UML component diagrams, both concepts are clearly represented. The semicircle below is the Required Interface and the circle outside the box is the Provided Interface:

So how do you implement Required Interface and Provided Interface? From the perspective of architecture layer, all modules are attached to a higher level of host APP environment, which should be used by the host app using these modules in a adapter interface adaptation, so that the caller can continue to use the required protocol internally. The Adapter ADAPTS the Required Protocol to the modified Provided Protocol. The entire process module is unconscious.

In this case, the required Protocol defined in the caller is a declaration of the external module on which it depends.

forprovidedAdd the modulerequired protocol

Module adaptation is all done by the module’s user and assembler App Context, requiring at least two lines of code.

For example, a module needs to display a login screen that can display a custom prompt.

Example caller module:

// The dependent interface declared in the caller indicates that it depends on a login interface
protocol RequiredLoginViewInput {
  var message: String? { get set } // Custom prompt displayed on the login interface
}

// The login module is invoked in the caller
Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
    destination.message = "Please log in"
})
Copy the code
Objective – C example
// The dependent interface declared in the caller indicates that it depends on a login interface
@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic.copy) NSString *message;
@end

// The login module is invoked in the caller
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
    destination.message = @" Please log in";
}];
Copy the code

The actual login interface provides ProvidedLoginViewInput:

// The interface provided by the actual login interface
protocol ProvidedLoginViewInput {
   var message: String? { get set}}Copy the code
Objective – C example
// The interface provided by the actual login interface
@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic.copy) NSString *message;
@end
Copy the code

The adaptation code is implemented by the host app to make the login interface support RequiredLoginViewInput:

// To enable the module to support Required Protocol, just add a protocol extension
extension LoginViewController: RequiredLoginViewInput {}Copy the code
Objective – C example

‘ ‘objectivec’ // Make the module support required Protocol, Just add a protocol extension @Interface LoginViewController (ModuleAAdapter) @end@implementation LoginViewController (ModuleAAdapter) @end “`

RequiredLoginViewInput: RequiredLoginViewInput:

// If the Router class is available, add RequiredLoginViewInput to the router
LoginViewRouter.register(RoutableView<RequiredLoginViewInput> ())// If the router of the corresponding module cannot be obtained, the adapter can be used for forwarding
ZIKViewRouteAdapter.register(adapter: RoutableView<RequiredLoginViewInput>(), forAdaptee: RoutableView<ProvidedLoginViewInput> ())Copy the code
Objective – C example

‘ ‘objectivec // If the router class is available, Can be directly added to the router RequiredLoginViewInput [LoginViewRouter registerViewProtocol: ZIKRoutable (RequiredLoginViewInput)]; // If you cannot get the router of the corresponding module, Can register adapter [self registerDestinationAdapter: ZIKRoutable (RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)]; ` ` `

RequiredLoginViewInput and ProvidedLoginViewInput can be used to obtain the same module:

Example caller module:

Router.makeDestination(to: RoutableView<RequiredLoginViewInput>(), preparation: {
    destination.message = "Please log in"
})

ProvidedLoginViewInput and RequiredLoginViewInput can obtain the same router
Router.makeDestination(to: RoutableView<ProvidedLoginViewInput>(), preparation: {
    destination.message = "Please log in"
})
Copy the code
Objective – C example
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<RequiredLoginViewInput> destination) {
    destination.message = @" Please log in";
}];

ProvidedLoginViewInput and RequiredLoginViewInput can obtain the same router
[ZIKRouterToView(RequiredLoginViewInput) makeDestinationWithPraparation:^(id<ProvidedLoginViewInput> destination) {
    destination.message = @" Please log in";
}];
Copy the code

The interface adapter

Sometimes the interface names of ProvidedLoginViewInput and RequiredLoginViewInput may be slightly different. In this case, you need to use category, extension, subclass, proxy class to adapt the interface.

protocol ProvidedLoginViewInput {
   var notifyString: String? { get set } // The interface name is different
}
Copy the code
Objective – C example
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic.copy) NSString *notifyString; // The interface name is different
@end
Copy the code

Interface forwarding is required during adaptation, and the login interface supports RequiredLoginViewInput:

extension LoginViewController: RequiredLoginViewInput {
    var message: String? {
        get {
            return notifyString
        }
        set {
            notifyString = newValue
        }
    }
}
Copy the code
Objective – C example
@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
Copy the code

Forwarding interfaces with intermediaries

If you can’t add required Protocol directly to the module, for example, some delegates in protocol need to be compatible:

protocol RequiredLoginViewDelegate {
    func didFinishLogin(a) -> Void
}
protocol RequiredLoginViewInput {
  var message: String? { get set }
  var delegate: RequiredLoginViewDelegate { get set}}Copy the code
Objective – C example
@protocol RequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end

@protocol RequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic.copy) NSString *message;
@property (nonatomic.weak) id<RequiredLoginViewDelegate> delegate;
@end
Copy the code

The delegate interface in a module is different:

protocol ProvidedLoginViewDelegate {
    func didLogin(a) -> Void
}
protocol ProvidedLoginViewInput {
  var notifyString: String? { get set }
  var delegate: ProvidedLoginViewDelegate { get set}}Copy the code
Objective – C example
@protocol ProvidedLoginViewDelegate <NSObject>
- (void)didLogin;
@end

@protocol ProvidedLoginViewInput <ZIKViewRoutable>
@property (nonatomic.copy) NSString *notifyString;
@property (nonatomic.weak) id<ProvidedLoginViewDelegate> delegate;
@end
Copy the code

If the same method has different parameter types, we can replace the real router with a new router and insert a mediator into the new router to forward the interface:

class ReqiredLoginViewRouter: ProvidedLoginViewRouter {

   override func destination(with configuration: ZIKViewRouteConfiguration) -> RequiredLoginViewInput? {
       let realDestination: ProvidedLoginViewInput = super.destination(with configuration)
       // The proxy forwards RequiredLoginViewInput to ProvidedLoginViewInput
       let proxy: RequiredLoginViewInput = ProxyForDestination(realDestination)
       return proxy
   }
}

Copy the code
Objective – C example
@interface ReqiredLoginViewRouter : ProvidedLoginViewRouter
@end
@implementation RequiredLoginViewRouter

- (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
   id<ProvidedLoginViewInput> realDestination = [super destinationWithConfiguration:configuration];
    // The proxy forwards RequiredLoginViewInput to ProvidedLoginViewInput
    id<RequiredLoginViewInput> proxy = ProxyForDestination(realDestination);
    return mediator;
}
@end
Copy the code

For ordinary OC classes, proxy can be implemented with NSProxy. For UI classes that are complex in UIKit, or for Swift classes, you can subclass them, and override methods in those subclasses to do module adaptation.

Declarative dependency

Using the previous static route checking mechanism, modules only need to declare the required interface to ensure that the corresponding module must exist.

Modules do not need to declare dependencies in their interfaces. If a module needs to add dependencies, it simply creates a new required interface without modifying the interface itself. In this way, interface changes caused by dependency changes can be avoided and interface maintenance costs can be reduced.

The module provides default dependency configuration

Each time a module is introduced, the host app needs to write an adaptation code, which in most cases is only two lines long, but we want to minimize the host app’s maintenance responsibilities.

At this point, you can have the module provide a default dependency and use macros to define the package, bypassing the compile check.

#if USE_DEFAULT_DEPENDENCY

import ProvidedLoginModule

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

extension ProvidedLoginViewController: RequiredLoginViewInput {

}

#endif
Copy the code
Objective – C example
#if USE_DEFAULT_DEPENDENCY

@import ProvidedLoginModule;

static inline void registerDefaultDependency() {
    [ZIKViewRouteAdapter registerDestinationAdapter:ZIKRoutable(RequiredLoginViewInput) forAdaptee:ZIKRoutable(ProvidedLoginViewInput)];
}

// Macro definition, default adaptation code
#define ADAPT_DEFAULT_DEPENDENCY \
@interface ProvidedLoginViewController (Adapter) <RequiredLoginViewInput>    \
@end    \
@implementation ProvidedLoginViewController (Adapter) \
@end    \

#endif
Copy the code

If the host app wants to use default dependencies, set Preprocessor Macros in.xcConfig and turn on macro definitions:

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) USE_DEFAULT_DEPENDENCY=1
Copy the code

For the Swift module, set the Active Compilation Conditions in the module target and add the Compilation macro USE_DEFAULT_DEPENDENCY.

The host app can directly call the default adaptation code, no longer responsible for maintenance:

public func registerAdapters(a) {
    // Register the default dependencies
    registerDefaultDependency()
    ...
}
Copy the code
Objective – C example
void registerAdapters() {
    // Register the default dependenciesregisterDefaultDependency(); . }// Use the default adaptation code
ADAPT_DEFAULT_DEPENDENCY
Copy the code

If the host app needs to replace the provided module with another one, you can close the macro definition and write another adaptation to replace the dependency.

modular

With the distinction between required Protocol and Provided Protocol, true modularity can be achieved. After the caller declares the required Required protocol, the called module can be replaced with another module with the same functionality at any time.

Referring to the ZIKLoginModule example in demo, the login module relies on a popover module that is different in ZIKRouterDemo and ZikRouterDemo-MacOS, and the code in the login module does not need to change when switching the popover module.

Use the Adapter specification

In general, it is not necessary to immediately separate all protocols into required protocol and Provided Protocol. The calling module and the destination module can temporarily share protocol, or simply rename required Protocol as a subset of Provided Protocol, When the module needs to be replaced for the first time, use category, extension, proxy, subclass and other technologies for interface adaptation.

Interface adaptation cannot be abused because it costs a lot and not all interfaces can be adapted. For example, it is difficult to adapt synchronous interfaces and asynchronous interfaces.

There are several suggestions for handling inter-module coupling:

  • If you rely on modules that provide specific functionality, there is no generality, just refer to the class
  • If you rely on some simple generic module (such as the logging module), you can set the dependency externally on the module’s interface, such as in the form of a block
  • Most modules that need to be decoupled are business modules that need to be reused. If your module doesn’t need to be reused and doesn’t need to be developed separately, just reference the corresponding class
  • In most cases, it is recommended to share the protocol, or letrequired protocolAs aprovided protocolThe interface names remain the same
  • Interface adaptation should only be done if your business module does allow users to use different dependency modules. For example, cross-platform modules are required. For example, the login interface module allows different apps to use different login service modules

With required Protocol and Provided Protocol, we achieve complete decoupling between modules.

Intermodule communication

There are many ways to communicate between modules, and the degree of decoupling is also different. Only the way interfaces interact is discussed here.

Control streams INPUT and output

The external interface of the module can be divided into input and output. The main difference between the two is the ownership of control flow initiative.

Input is an interface that is actively invoked externally by the originator of the control flow externally, such as externally calling the VIEW’s UI modification interface.

Output is the interface that the module actively calls the external implementation. The initiator of the control flow is inside and needs the method required by the external implementation of Output. Examples include outputting UI events, event callbacks, and getting an external dataSource. The delegate mode, commonly used in iOS, is also an output.

Set input and Output

Module design input and output, and then when the module is created, set the input and output relationship between modules, you can configure the communication between modules, and at the same time fully decoupled.

class NoteListViewController: UIViewController.EditorViewOutput {
    func showEditor(a) {
        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}}Copy the code

The child module

Most scenarios do not discuss the existence of submodules. If MVVM or VIPER architecture is used and a view Controller uses a Child View Controller, how will multiple view models interact with the interactor? Who initializes and manages submodules?

Some solutions create and use child View models directly in the parent view Model, but this results in the implementation of the view affecting the implementation of the View Model. If the parent view uses another child view, The code in the parent View Model also needs to be changed.

The source of the submodule

The sources of submodules are:

  • The parent view refers to an encapsulated child view control, along with the entire MVVM or VIPER module that introduces the child view
  • A Service is used in the View Model or interactor

Communication mode

It could be a UIView, it could be a Child UIViewController. Therefore, a child view may need to request data externally, or it may do all of its tasks independently without relying on the parent module.

If the child view is independent, there is no logic in the child module to interact with the parent module, only the interface to pass events through outputs. The parent view model/Presenter/Interactor does not know that the parent view provides these interfaces through the child view.

If the parent module needs to call the business interface of the child module, or receive data or business events from the child module, and does not want to affect the interface of the View, A child view Model/Presenter/Interactor can be a service of the parent View Model/Presenter/Interactor. Inject into the parent View Model/Presenter/Interactor to bypass the View layer. This allows the module to communicate with the parent module through a service, and the parent module does not know that the service came from the child module.

In such a design, the child and parent modules are unaware of each other’s existence and only interact through interfaces. The advantage is that if the parent view wants to change to another child view control with the same function, it only needs to change in the parent view, and does not affect other view model/Presenter/Interactor.

The parent module:

class EditorViewController: UIViewController {
    var viewModel: EditorViewModel!
    
    func addTextView(a) {
        let textViewController = Router.makeDestination(to: RoutableView<TextViewInput>()) { (destination) in
            // Set the interaction between modules
            // The parent view is a view model/Presenter/Interactor that cannot access the child module
            // The submodule exposes these internal components as business inputs
            self.viewModel.textService = destination.viewModel
            destination.viewModel.output = self.viewModel
        }
        
        addChildViewController(textViewController)
        view.addSubview(textViewController.view)
        textViewController.didMove(toParentViewController: self)}}Copy the code
Objective-C Sample
@interface EditorViewController: UIViewController
@property (nonatomic.strong) id<EditorViewModel> viewModel;
@end
@implementation EditorViewController
  
- (void)addTextView {
    UIViewController *textViewController = [ZIKRouterToView(TextViewInput) makeDestinationWithPreparation:^(id<TextViewInput> destination) {
        // Set the interaction between modules
        // The parent view is a view model/Presenter/Interactor that cannot access the child module
        // The submodule exposes these internal components as business inputs
        self.viewModel.textService = destination.viewModel;
        destination.viewModel.output = self.viewModel;
    }];

    [self addChildViewController:textViewController];
    [self.view addSubview: textViewController.view];
    [textViewController didMoveToParentViewController: self];
}

@end
Copy the code

A module:

protocol TextViewInput {
    weak var output: TextViewModuleOutput? { get set }
    var viewModel: TextViewModel { get}}class TextViewController: UIViewController.TextViewInput {
    weak var output: TextViewModuleOutput?
    var viewModel: TextViewModel!
}
Copy the code
Objective-C Sample
@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
Copy the code

The Output of the adapter

Module adaptation can be troublesome when using output.

For example, the pair required-provided Protocol:

protocol RequiredEditorViewInput {
    weak var output: RequiredEditorViewOutput? { get set}}protocol ProvidedEditorViewInput {
    weak var output: ProvidedEditorViewOutput? { get set}}Copy the code
Objective-C Sample
@protocol RequiredEditorViewInput <NSObject>
@property (nonatomic.weak) id<RequiredEditorViewOutput> output;
@end

@protocol ProvidedEditorViewInput <NSObject>
@property (nonatomic.weak) id<ProvidedEditorViewOutput> output;
@end
Copy the code

Because the implementer of output is not fixed, it is impossible to make all output classes adapt both RequiredEditorViewOutput and ProvidedEditorViewOutput. In this case, you are advised to use the protocol instead of the required-provided mode.

If you still want to use the required-provided mode, you need to use the factory mode to pass the output and adapt it internally with a proxy.

Router of the actual module:

protocol ProvidedEditorViewModuleInput {
    var makeDestinationWith(_ output: ProvidedEditorViewOutput?). ->ProvidedEditorViewInput? { get set}}class ProvidedEditorViewRouter: ZIKViewRouter<EditorViewController.ViewRouteConfig> {
    
    override class func registerRoutableDestination(a){
        register(RoutableViewModule<ProvidedEditorViewModuleInput>())
    }
  
    override class func defaultRouteConfiguration() - >ViewRouteConfig {
        let config = ViewMakeableConfiguration<ProvidedViewInput, (ProvidedEditorViewOutput?). ->ProvidedViewInput? > ({_ in})
        config.makeDestinationWith = { [unowned config] output in
            / / set the 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}}Copy the code
Objective-C Sample
@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) {
        / / set the 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
Copy the code

Adaptation code:

protocol RequiredEditorViewModuleInput {
    var makeDestinationWith(_ output: RequiredEditorViewOutput?). ->RequiredEditorViewInput? { get set}}// Required Router for adaptation
class RequiredEditorViewRouter: ProvidedEditorViewRouter {
    
    override class func registerRoutableDestination(a){
        register(RoutableViewModule<RequiredEditorViewModuleInput>())
    }
  
    / / compatible with the configuration
    override class func defaultRouteConfiguration() - >PerformRouteConfig {
        let config = super.defaultRouteConfiguration()
        let makeDestinationWith = config.makeDestinationWith
        
        config.makeDestinationWith = { requiredOutput in
            RequiredEditorViewOutput is converted to ProvidedEditorViewOutput
            let providedOutput = EditorOutputProxy(forwarding: requiredOutput)
            return makeDestinationWith(providedOutput)
        }
        return config
    }
}

class EditorOutputProxy: ProvidedEditorViewOutput {
    let forwarding: RequiredEditorViewOutput
    // Implement ProvidedEditorViewOutput and forward it to forwarding
}
Copy the code
Objective-C Sample
@protocol RequiredEditorViewModuleInput <ZIKViewModuleRoutable>
@property (nonatomic.readonly) id<RequiredEditorViewInput> (makeDestinationWith)(id<RequiredEditorViewOutput> output);
@end

// Required Router for adaptation
@interface RequiredEditorViewRouter: ProvidedEditorViewRouter
@end
@implementation RequiredEditorViewRouter

+ (void)registerRoutableDestination {
    [self registerModuleProtocol:ZIKRoutable(RequiredEditorViewModuleInput)];  
}
/ / compatible with the configuration
+ (ZIKViewMakeableConfiguration *)defaultRouteConfiguration {
    ZIKViewMakeableConfiguration *config = [super defaultRouteConfiguration];
    id<ProvidedEditorViewInput>(^makeDestinationWith)(id<ProvidedEditorViewOutput>) = config.makeDestinationWith;
    
    config.makeDestinationWith = id^ (id<RequiredEditorViewOutput> requiredOutput) {
        RequiredEditorViewOutput is converted to ProvidedEditorViewOutput
        EditorOutputProxy *providedOutput = [[EditorOutputProxy alloc] initWithForwarding: requiredOutput];
        return makeDestinationWith(providedOutput);
    };
    return config;
}
  
- (nullable id<PersonType>)destinationWithConfiguration:(ZIKServiceMakeableConfiguration *)configuration {
    return configuration.makedDestination;
}

@end
  
// Implement ProvidedEditorViewOutput and forward it to 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
Copy the code

As you can see, output adaptation is a bit cumbersome. So unless your module is a generic module with a real decoupling requirement, you can simply use provided Protocol.

Function extension

Having concluded our approach to module decoupling and dependency management using interfaces, we can extend the Router further. Creating a module using makeDestination is the most basic function above, but there are many useful extensions that can be made using the Router subclass, as demonstrated here.

Automatic registration

When writing router code, you need to register the Router and protocol. It is possible to register in the +load method in OC, but Swift is no longer able to use the +load method, and the registration code scattered in +load is also difficult to manage. BeeHive adds registration information to a custom zone in Mach-O using macro definitions and __attribute((used, section(“__DATA,””BeehiveServices”””)), which is then read at startup and automatically registered. Unfortunately, this doesn’t work with Swift either.

We can write the registration code in the router + registerRoutableDestination method, and then call each router class + registerRoutableDestination method one by one. Still can go a step further, with the runtime technology of traverse the Mach – O __DATA, __objc_classlist area list of classes, all access to the router class, automatic call all + registerRoutableDestination method.

If you do not want to use automatic registration after unified management of registration codes, you can switch to manual registration at any time.

// Router for the editor module
class EditorViewRouter: ZIKViewRouter {
  
    override class func registerRoutableDestination(a){
        registerView(EditorViewController.self)
        register(RoutableView<EditorViewProtocol> ())}}Copy the code
Objective-C Sample
@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    [self registerView:[EditorViewController class]];
    [self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}

@end
Copy the code

Packaging interface jump

One of the reasons for the coupling between modules in iOS is that the interface jump logic is carried out through UIViewController, and the jump function is limited to view Controller, resulting in the data flow is often unable to bypass the View layer. To better manage jump logic, encapsulation is required.

Encapsulating jumps can mask UIKit details so that jump code can reside in non-View layers (such as Presenter, View Model, Interactor, service) and can be cross-platform and easily configured to switch jumps.

For normal modules, use ZIKServiceRouter, and for interface modules, such as UIViewController and UIView, use ZIKViewRouter, which encapsulates the interface jump function.

After the encapsulation interface jumps, the usage mode is as follows:

class TestViewController: UIViewController {

    // Jump directly to the Editor interface
    func showEditor(a) {
        Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))}// Go to the Editor interface and use protocol to configure the interface
    func prepareAndShowEditor(a) {
        Router.perform(
            to: RoutableView<EditorViewProtocol>(),
            path: .push(from: self),
            preparation: { destination in
                // Perform the configuration before the jump
                // Destination is automatically inferred to be EditorViewProtocol}}})Copy the code
Objective-C Sample
@implementation TestViewController

- (void)showEditor {
    // Jump directly to the Editor interface
    [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}

- (void)prepareAndShowEditor {
    // Go to the Editor interface and use protocol to configure the interface
    [ZIKRouterToView(EditorViewProtocol) 
        performPath:ZIKViewRoutePath.pushFrom(self)
        preparation:^(id<EditorViewProtocol> destination) {
            // Perform the configuration before the jump
            // Destination is automatically inferred to be EditorViewProtocol
    }];
}

@end
Copy the code

You can use ViewRoutePath to toggle different jumps in one click:

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)}Copy the code

In addition, after the interface hops, the interface can be backstrokeback according to the jump mode, without manual separation of dismiss, POP and other situations:

class TestViewController: UIViewController {
    var router: DestinationViewRouter<EditorViewProtocol>?

    func showEditor(a) {
        / / hold the router
        router = Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))}// The Router performs a pop operation on the Editor View Controller to remove the interface
    func removeEditor(a) {
        guard let router = router, router.canRemove else {
            return
        }
        router.removeRoute()
        router = nil}}Copy the code
Objective-C Sample
@interface TestViewController(a)
@property (nonatomic.strong) ZIKDestinationViewRouter(id<EditorViewProtocol>) *router;
@end
@implementation TestViewController

- (void)showEditor {
    / / hold the router
    self.router = [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}

// The Router performs a pop operation on the Editor View Controller to remove the interface
- (void)removeEditor {
    if(! [self.router canRemove]) {
        return;
    }
    [self.router removeRoute];
    self.router = nil;
}

@end
Copy the code

Custom jump

Some interfaces have special ways of jumping, such as the tabbar interface, which requires toggling tabbar Items. In some interfaces, there is a custom jump animation. In this case, you can rewrite the corresponding method in the Router subclass to customize the jump.

class EditorViewRouter: ZIKViewRouter<EditorViewController.ViewRouteConfig> {

    override func destination(with configuration: ViewRouteConfig) -> Any? {
        return EditorViewController()}override func canPerformCustomRoute(a) -> Bool {
        return true
    }
    
    override func performCustomRoute(onDestination destination: EditorViewController, fromSource source: Any? , configuration: ViewRouteConfig) {
        beginPerformRoute()
        // Custom jump
        CustomAnimator.transition(from: source, to: destination) {
            self.endPerformRouteWithSuccess()
        }
    }
    
    override func canRemoveCustomRoute(a) -> Bool {
        return true
    }
    
    override func removeCustomRoute(onDestination destination: EditorViewController, fromSource source: Any? , removeConfiguration: ViewRemoveConfig, configuration: ViewRouteConfig) {
        beginRemoveRoute(fromSource: source)
        // Remove the custom jump
        CustomAnimator.dismiss(destination) {
            self.endRemoveRouteWithSuccess(onDestination: destination, fromSource: source)
        }
    }
    
    override class func supportedRouteTypes() - >ZIKViewRouteTypeMask {
        return [.custom, .viewControllerDefault]
    }
}
Copy the code
Objective-C Sample
@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];
    // Custom jump
    [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];
    // Remove the custom jump
    [CustomAnimator dismiss:destination completion:^{
        [self endRemoveRouteWithSuccessOnDestination:destination fromSource:source];
    }];
}

+ (ZIKViewRouteTypeMask)supportedRouteTypes {
    return ZIKViewRouteTypeMaskCustom|ZIKViewRouteTypeMaskViewControllerDefault;
}

@end
Copy the code

Support the storyboard

Many projects use storyboards, and when modularizing, you certainly can’t require all modules that use storyboards to use code instead. So we can hook a few storyboard related methods, for example – prepareSegue: sender: to invoke prepareDestination: : you can.

URL routing

Although many disadvantages of URL routing have been listed before, URL routing is the best solution if your module needs to be called from an H5 interface, such as an e-commerce app that needs to implement cross-platform dynamic routing rules.

But we don’t want to repackage the module using another framework to implement URL routing. You only need to extend the URL routing function on the router to use the interface and URL management module at the same time.

You can register the URL to the router:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol.ViewRouteConfig> {
    override class func registerRoutableDestination(a){
        / / registered url
        registerURLPattern("app://editor/:title")}}Copy the code
Objective-C Sample
@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    / / registered url
    [self registerURLPattern:@"app://editor/:title"];
}

@end
Copy the code

The router can then be obtained with the corresponding URL:

ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
Copy the code
Objective-C Sample
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
Copy the code

And handle 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
}
Copy the code
Objective-C Sample
- (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;
}
Copy the code

Each Router subclass can also perform further processing on the URL, such as processing parameters in the URL, executing corresponding methods through the URL, and sending return values to the caller after executing the route.

Each project has different requirements for URL routing. Based on ZIKRouter’s powerful scalability, you can also implement your own URL routing rules according to project requirements.

Replace router subclasses with router objects

In addition to creating router subclasses, you can also use generic Router instance objects that provide the same functionality as router subclasses in the block attribute of each object, so you don’t have to worry about having too many classes. It works the same way as replacing a subclass of Configuration with a generic Configuration.

The ZIKViewRoute object subclass overrides the block property, and the code can be chained:

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> ())Copy the code
Objective-C Sample
[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));
Copy the code

Simplified Router Implementation

Router implementation based on ZIKViewRoute object can further simplify the router implementation code.

If your class is simple and does not need the Router subclass, just register the class with one line of code:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), forMakingView: EditorViewController.self)
Copy the code
Objective-C Sample
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]].Copy the code

Or use blocks to customize the way objects are created:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), 
                 forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
                     return EditorViewController()}Copy the code
Objective-C Sample
[ZIKViewRouter
    registerViewProtocol:ZIKRoutable(EditorViewProtocol)
    forMakingView:[EditorViewController class]
    making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
        return [[EditorViewController alloc] init];
 }];
Copy the code

Or specify a C function to create an object:

function makeEditorViewController(config: ViewRouteConfig) - >EditorViewController? {
    return EditorViewController()}ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), 
                 forMakingView: EditorViewController.self, making: makeEditorViewController)
Copy the code
Objective-C Sample
id<EditorViewController> makeEditorViewController(ZIKViewRouteConfiguration *config) {
    return [[EditorViewController alloc] init];
}

[ZIKViewRouter
    registerViewProtocol:ZIKRoutable(EditorViewProtocol)
    forMakingView:[EditorViewController class]
    factory:makeEditorViewController];
Copy the code

The event processing

Sometimes the module needs to handle system events or app custom events. In this case, the router subclass can implement it and then iterate the distribution.

class SomeServiceRouter: ZIKServiceRouter {
    @objc class func applicationDidEnterBackground(_ application: UIApplication) {
        // handle applicationDidEnterBackground event}}Copy the code
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)
            }
        }
    }

}
Copy the code
Objective-C Sample
@interface SomeServiceRouter : ZIKServiceRouter
@end
@implementation SomeServiceRouter

+ (void)applicationDidEnterBackground:(UIApplication *)application {
    // handle applicationDidEnterBackground event
}

@end
Copy the code
@interface AppDelegate(a)
@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
Copy the code

Unit testing

With a scheme that uses interfaces to manage dependencies, we can freely configure mock dependencies when unit testing our modules without the need for code inside the hook modules.

For example, a login module that relies on a network module:

// Login module
class LoginService {

    func login(account: String, password: String, completion: (Result<LoginError>) -> Void) {
        // Use RequiredNetServiceInput internally for network access
        let netService = Router.makeDestination(to: RoutableService<RequiredNetServiceInput> ())letrequest = makeLoginRequest(account: account, password: password) netService? .POST(request: request, completion: completion)
    }
}

// Declare dependencies
extension RoutableService where Protocol= =RequiredNetServiceInput {
    init() {}}Copy the code
Objective-C Sample
// Login module
@interface LoginService : NSObject
@end
@implementation LoginService

- (void)loginWithAccount:(NSString *)account password:(NSString *)password  completion:(void(^)(Result *result))completion {
    // Use RequiredNetServiceInput internally for network access
    id<RequiredNetServiceInput> netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination];
    Request *request = makeLoginRequest(account, password);
    [netService POSTRequest:request completion: completion];
}

@end
  
// Declare dependencies
@protocol RequiredNetServiceInput <ZIKServiceRoutable>
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion;
@end
Copy the code

Instead of introducing a real network module when writing unit tests, you can provide a custom mock network module:

class MockNetService: RequiredNetServiceInput {
    func POST(request: Request, completion: (Result<NetError>) {
        completion(.success)
    }
}
Copy the code
// Register mock dependencies
ZIKAnyServiceRouter.register(RoutableService<RequiredNetServiceInput>(), 
                 forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? in
                     return MockNetService()}Copy the code
Objective-C Sample
@interface MockNetService : NSObject <RequiredNetServiceInput>
@end
@implementation MockNetService

- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
    completion([Result success]);
}
  
@end
Copy the code
// Register mock dependencies
[ZIKServiceRouter registerServiceProtocol:ZIKRoutable(EditorViewInput) forMakingService:[MockNetService class]].Copy the code

For external dependencies without interface interaction, such as simply jumping to the corresponding interface, you only need to register a blank proxy.

Unit test code:

class LoginServiceTests: XCTestCase {
    
    func testLoginSuccess(a) {
        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)}})
    }
    
}
Copy the code
Objective-C Sample
@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
Copy the code

Using interfaces to manage dependencies makes it easier to mock out external dependencies and make unit tests more stable.

Interface Version Management

There is another issue to be aware of when using an interface management module. The interface will change with module update. This interface has been used by many external users. How to reduce the impact of interface change?

In this case, you need to distinguish the new interface from the old interface and distinguish the version. When promoting the new interface, keep the old interface and mark the old interface as obsolete. This allows the consumer to temporarily use the old interface and change the code incrementally.

Refer to the version management macros in Swift and OC for this section.

The interface is obsolete and can be used temporarily. You are advised to replace it with a new interface as soon as possible:

// Swift
@available(iOS, deprecated: 8.0, message: "Use new interface instead")
Copy the code
// Objective-C
API_DEPRECATED_WITH_REPLACEMENT("performPath:configuring:", ios(7.0.7.0));
Copy the code

The interface is invalid:

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

Final shape

Finally, a router looks like this:

// Router for the editor module
class EditorViewRouter: ZIKViewRouter<EditorViewController.ViewRouteConfig> {

    override class func registerRoutableDestination(a){
        registerView(EditorViewController.self)
        register(RoutableView<EditorViewProtocol>())
        registerURLPattern("app://editor/:title")}override func processUserInfo(_ userInfo: [AnyHashable : Any] = [:], from url: URL) {
        let title = userInfo["title"]
        // Process parameters in the URL
    }

    // Subclass override to create a module
    override func destination(with configuration: ViewRouteConfig) -> Any? {
        let destination = EditorViewController(a)return destination
    }

    // Configure the module to inject static dependencies
    override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
        // Inject a service dependency
        destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput> ())// Other configurations
        // Process parameters from the URL
        if let title = configuration.userInfo["title"] as? String {
            destination.title = title
        } else {
            destination.title = "Default title"}}// Event processing
    @objc class func applicationDidEnterBackground(_ application: UIApplication) {
        // handle applicationDidEnterBackground event}}Copy the code
Objective-C Sample
// Router for the editor module
@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"];
    // Process parameters in the URL
}

// Subclass override to create a module- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { EditorViewController  *destination = [[EditorViewController alloc] init];return destination;
}

// Configure the module to inject static dependencies
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
    // Inject a service dependency
    destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
    // Other configurations
    // Process parameters from the URL
    NSString *title = configuration.userInfo[@"title"];
    if (title) {
        destination.title = title;
    } else {
        destination.title = @" Default title"; }}// Event processing
+ (void)applicationDidEnterBackground:(UIApplication *)application {
    // handle applicationDidEnterBackground event
}

@end
Copy the code

Advantages of interface-based decoupling

We can see the advantages of an interface based management module:

  • Rely on compile checking for strict type safety
  • Rely on compile checks to reduce the cost of refactoring
  • The dependencies required by the module are explicitly declared through an interface, allowing external dependency injection
  • While maintaining the dynamic feature, check routes to avoid using non-existent routing modules
  • Interfaces are used to distinguish required Protocol from Provided Protocol for explicit module adaptation and complete decoupling

Looking back at the previous eight decoupling metrics, the ZIKRouter meets them perfectly. Router provides multiple module management methods (makeDestination, prepareDestination, dependency injection, page jump, storyboard support) that can override most existing scenarios, enabling progressive modularity and reducing the cost of refactoring existing code.