This paper records a debate with a classmate about Router design. As for the debate between URL Router and Protocol Router, there is no superior architecture but suitability. I hope more students can discuss it together.


For componentization, I believe you must be familiar with, but for componentization scheme and ideas, we more or less have some of their own ideas, if not clear componentization students can first preview through the following article.

Reference reading:

alibaba/BeeHive

The componentization of Mogujie App

The road to componentization of Mogujie App

IOS application architecture: Componentization solutions

IOS componentization solution

A ramble on component architecture

IOS componentization — Analysis of route Design

IOS VIPER Architecture Practice iii: Interface oriented routing design

Hybird Constructs a Router to implement componentization

The origin of

It all started when I posted the Architecture Primer series on Nuggets and had a heated debate about Router design with one of the big guys. As a beginner iOS player, I felt I should share the debate and let everyone talk about it to improve.

Here is my simple design for the Router, which was also covered in the previous article.

// // Router. Swift // RouterPatterm // // Created by Shuangquan Zhuon 17/4/12. // Copyright © 2017 Doubles_z.all Rights reserved. // import UIKit class Router { static let shareRouter = Router() var params: [String : Any]? var routers: [String : Any]? fileprivate let map = ["J1" : "Controller"] func guardRouters(finishedCallback : @escaping () -> ()) { Http.requestData(.get, URLString: "http://localhost:3001/api/J1/getRouters") { (response) in guard let result = response as? [String : Any] else { return } guard let data:[String : Any] = result["data"] as? [String : Any] else { return } guard let routers:[String : Any] = data["routers"] as? [String : Any] else { return } self.routers = routers finishedCallback() } } } extension Router { func addParam(key: String, value: Any) { params? [key] = value } func clearParams() { params? .removeAll() } func push(_ path: String) { guardRouters { guard let state = self.routers? [path] as? String else { return } if state == "app" { guard let nativeController = NSClassFromString("RouterPatterm.\(self.map[path]!) ") as? UIViewController.Type else { return } currentController? .navigationController? .pushViewController(nativeController.init(), animated: true) } if state == "web" { let host = "http://localhost:3000/" var query = "" let ref = "client=app" guard let params =  self.params else { return } for (key, value) in params { query += "\(key)=\(value)&" } self.clearParams() let webViewController = WebViewController("\(host)\(path)? \(query)\(ref)") currentController? .navigationController? .pushViewController(webViewController, animated: true) } } } }Copy the code

You only need to perform the following operations:

The Router. ShareRouter. Params = [" text ":" app end incoming data ", "code" : 1001] the Router. ShareRouter. Push (" J1 ")Copy the code
override func viewDidLoad() {
    super.viewDidLoad()
    self.text = Router.shareRouter.params["text"]
}
Copy the code

This is my understanding of the essence of router, the most concise function, optional add parameters, AOP to clear parameters, jump. Of course, routing is to be designed according to the business, here is just to achieve a most basic function.

Then, a student named @Black Panda Zuik commented on my article. @black Panda Zuik is the administrator of iOS Developer column (30K + followers). After that, we had a communication about router map and protocol.

debate

Hooligan girlies

How does the Router’s push method pass parameters to the target viewController

Castie1

The router is designed as a singleton to hold a parameter dictionary. If you need to pass parameters, call addParam because not all jumps need to pass parameters. Use AOP to delete the parameter dictionary during the target page hook lifecycle. Of course, the design of router is largely related to the requirements and specifications of the company’s project, so it cannot be separated from the business and can only describe the essence.

Hooligan girlies

Castie1: We also considered componentialization before, but considering that the map and the entire architecture rely on strings, remember there are some other limitations. After looking at the sharing solutions of Lianjia Mogujie and others, we still haven’t solved these problems, so we finally gave up ᕙ(⇀‸↼)ᕗ

Castie1

Relying on strings is not a bad thing, because string errors are fixed in the development environment, and extern can also reduce them, casatwy.com/iOS-Moduliz… The idea of this article is to completely de-model it. It may be radical, but for your reference.

Thanks, I have read this article before, because we do not need to configure the router for the server, we just want to decouple the large modules. In this case, considering that the benefits of componentization do not outweigh the disadvantages, we still do not adopt it, thanks again 😁

Black super panda Zuik

This article is more about the Hybird architecture. If you want a more native routing approach, check out Ali’s BeeHive, which has a method to find modules using native Protocol, but it is easy to implement. A more thorough approach can be found in my article: juejin.cn/post/684490…

Castie1

Reply to Zuik: Using protocol to address means that each VC needs to maintain its own parameter interface and jump through the polymorphic way. What is the advantage of this and directly hook the parameter list and assign values? Additional configuration is required for dynamic H5 active pages, as well as jumps for degrade schemes and URlschemes. For the Router, it depends on the business scenario and personal feelings.

Black super panda Zuik

Reply to Castie1: It is true that we should first look at the business scenario. Hybird App uses URL Router to be more simple and direct, while native APP uses Protocol Router to be more secure and efficient. There are also comparisons between the two routers in my article. My Protocol Router implementation does not use hook. Instead, it uses abstract factory mode. Each module that needs routing creates a router subclass and registers the protocol that can be used for routing. Protocol is used to obtain the router subclass, and then the route obtain module, you can use the registered protocol to pass parameters to the module and call methods. The benefits are:

  1. Development and refactoring allow the compiler to check for errors, ensuring full security. Let the developer know exactly what interfaces the module is using and make it more efficient to change them when they change.

  2. When implementing routing, modules are not required to modify, but only need to implement the registered protocol. Therefore, mapped modules can be replaced by another module that implements the same protocol at any time, with better decoupling capability.

  3. The URL Router is more suitable for interface modules, while the Protocol Router is suitable for all functional modules.

  4. More customization can be done in the Router subclass to inject dependencies into the module. In the case of page degradation, you can actually modify the map to achieve the same.

The disadvantage is that:

  1. URL schemes require additional support in the Protocol Router. However, this should be a function that is functionally separate from the router.

  2. The Protocol Router encounters difficulties in unifying multi-terminal routing rules. In this case, we still need to encapsulate another LAYER of URL router to unify the routing tables of PC, iOS, and Android.

Castie1

Reply to Zuik: 0. Nice technical exchange, no attack on your protocal ideas either, you can see that the Router section is only a small part of this article, and most of the others are designed based on Protocal, so you and I agree on the benefits of protocal abstraction.

But for the Router, there’s a little bit of a problem with native jumps based on Protocal.

  1. First of all, let’s clarify the noun positioning. The router I understand is the interface module router, while the functional module router you refer to is a middleware service in my opinion. The concept of middleware is completely different from the front-end router. See react-router/vue-router.

  2. For router design, to quote you, “create a router subclass for each module that needs routing, Register the protocol that can be used for routing. In my humble understanding, the business side needs to maintain a set of router interfaces every time it writes a VC. In fact, the Adapter mode is a variant, causing additional OBJC files, which will slow down the startup speed.

  3. “Efficient modification when interface changes”. For a single protocal interface modification is efficient because of the discrete design, but the extensibility of the interface modification as a whole is not reflected in the map clustered in the same router file. Same thing we did before, but we changed the mapping. The advantage of using router. map is that all routing logic can be stored in a file for unified maintenance.

  4. “Development and refactoring allows the compiler to check for errors, ensuring full security.” This is safe, just for string misspellings, not for wild Pointers, and relying on strings is not a bad thing because string errors are taken care of in the development environment.

My understanding:

The essence of my understanding of router is not the complex design of Mogujie, but just a singleton with map mapping, method of adding and clearing parameters and jump logic. I think it is enough. In this way, when the business side writes VC, it only needs to add parameters to the route and then jump is enough. Map and parameters can be dynamically configured on the server to increase scalability and activity. For details, see vue-Router design.

A little question:

See how your Github demo uses a lot of block callbacks to increase the memory footprint that can’t be released early. Has the delegate design been tested to see if it can reduce memory usage?

Black super panda Zuik

Reply to Castie1: HMMM, I saw this comment mentioned so I discuss it, I’m glad bloggers are talking to each other so seriously. About your question:

  1. As a Protocol Router, it naturally realizes the function of obtaining any module. The function is to obtain a module that implements a specified interface. For interface routing, it only encapsulates another layer of interface jump method. I have limited knowledge of the front end and don’t know how it achieves this kind of module decoupling.

  2. It is normal for router subclasses to increase the number of classes. The Router was developed while practicing VIPER, which created many additional classes for each interface module, with each part following a single responsibility, and I think decoupling is more important in large apps than the number of classes. Having too many classes does affect objC’s startup speed, but the effect is limited. In an app like wechat, there are only more than 600 View Controllers, and an additional 600 Router classes will not affect performance. For simple interfaces that do not need to pass parameters, you can put them all into the same Router subclass and dynamically return different interfaces.

  3. Add a unified interface to the Router. The router subclass is invisible when used. In my implementation, I maintain a mapping between protocol and router subclass. I find the corresponding Router subclass through protocol, and call the unified interface on the abstract Router parent class when used. If you want to add a unified interface for all routers, you can add it on the Router parent class.

  4. Compilation checking is not just about avoiding string errors, but more importantly, passing parameters safely and avoiding dictionary passing parameters. When the interface changes, the parameter name and type may change, and dictionary transmission makes it impossible to detect the correctness of the parameter and may pass in the wrong parameter type. Some modules have parameters that must be passed in and that need to be represented on the interface. The URL Router can only look up documents. From the perspective of decoupling, dictionary parameter passing requires the module to actively parse the received parameters, and the caller also needs to find out from the document which parameters can be passed. String names cause coupling in dictionary parameter passing, and reconstruction becomes troublesome when they need to be replaced with another identical module whose parameter key name is different. So dictionary parameter passing should be avoided most of the time if possible.

  5. Block problem. It does not cause memory problems when used because blocks are called immediately and synchronously when routing is performed. Using a block is better than a delegate because every call corresponds to a protocol, and protocol can be used directly in a block, whereas using a delegate requires a lot of extra judgment and can be cumbersome.

Castie1

Thank you for your detailed answer. I took the time to read your source code.

Indeed, the protocol Router scheme is indeed more friendly to the native end, making the transmission of values between native components clearer.

1. For your “interface change, parameter name and type may change, and dictionary transmission can not detect the correctness of the parameter, may pass in the wrong parameter type “,

The Router. ShareRouter. Params = [" text ":" app end incoming data ", "code" : 1001] the Router. ShareRouter. Push (" J1 ")Copy the code

Yes, there is no control over the type in dictionary form, which is a drawback, but this is only a bug that can be found during the development phase, and anyone with a bit of development experience will probably read the documentation, so it doesn’t really cost much.

Therefore, AT this point, I have accepted routing in the form of protocol. In fact, map is only for native design level. Protocol Router and URL Router should be parallel, and URlscheme is an additional module. There is already a consensus on this.

2. For “and the module parameter key name is different, reconstruction is very troublesome. So you should avoid dictionary pass-throughs most of the time.”

#define ZIKInfoViewProtocol_routable @protocol(ZIKInfoViewProtocol) @protocol ZIKInfoViewProtocol <ZIKViewRoutable> @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @property (nonatomic, weak) id<ZIKInfoViewDelegate> delegate; @end self.infoViewRouter = [[ZIKViewRouter.toView(ZIKInfoViewProtocol_routable) alloc] initWithConfiguring:^(ZIKViewRouteConfiguration * _Nonnull config) { config.source = self;  config.routeType = ZIKViewRouteTypePush;  //prepareDestination is hold in configuration, should be careful about retain cycle if this view controller will hold the router. Same with routeCompletion, successHandler, errorHandler, stateNotifier. config.prepareDestination = ^(UIViewController<ZIKInfoViewProtocol> *destination) { NSLog(@"provider: prepare destination"); destination.name = @"Zuik"; destination.age = 18; destination.delegate = weakSelf; };  - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.nameLabel.text = self.name;  self.ageLabel.text = [@(self.age) stringValue]; }Copy the code

If you need to change parameters or types, and if you need to refactor, as your demo shows, you also need to change at least three things, so the so-called solution to the refactoring problem is not really helpful.

3. How to distribute the granularity of each component when cocoapod-SEPc is used for componentization? If the division is based on pages, both the current page and the destination page need to rely on protocol, does that mean that all protocols are divided into an independent component to rely on? But this is obviously problematic, please advise how to actually divide.

4. Business parties need to learn how to configure routers. As your framework is complex, it may not be friendly to business parties who are not proficient in using it, which increases the learning cost.

override func viewDidLoad() {
    super.viewDidLoad()
    self.text = Router.shareRouter.params["text"]
}

Copy the code

In this case, using my simple demo idea, the business side can get the required parameters directly in the viewDidLoad method without exposing the header file at all, which is very cheap to learn. Since page jumps are the main thread, there are no thread-safety issues, and refactoring changes are not as troublesome as you say.

Your source code has a lot of exception handling operations, in fact, I do not quite understand why a Router needs to design so complex, may be my consideration is not comprehensive enough, please big man to clarify.

Black super panda Zuik

Castie1: Thanks for watching so carefully. Refactoring certainly requires code changes, but protocol calls rely on compiler checks to be 100% sure that all parameters passed are correct. If you need to manually check dozens or hundreds of parameter types in a dictionary, you cannot ensure that there are no omissions. This will affect development efficiency, although the amount of change is the same, but the time taken will vary greatly. Protocol is also used to describe the functionality of the module, directly in the header file without looking up documentation, to improve efficiency, and this is also necessary for module decoupling.

If you want to use Cocoapods to encapsulate common interface components, you can have multiple protocols refer to the same Router subclass without relying on a single protocol. The component provides a View Controller and a Router subclass, which registers a Provided Protocol. In the app that uses this component, the component is retrieved with another interface similar to required Protocol, and the app simply maps the Required Protocol to the corresponding Router subclass. Here, required Protocol is the module interface that the consumer needs to use, and Provided Protocol is the interface that the module actually provides. If provided Protocol and Required Protocol are the same, they can be seamlessly compatible. If the interfaces provide the same functionality but have different interfaces named, you can use extensions, NSProxy, etc., to support required Protocol. You can also have required Protocol point to a new Router subclass that is responsible for providing an intermediary that implements required Protocol and turning calls to Required Protocol into Provided Protocol. This part of the interface compatibility work is coupled by the App context, although it is in the same app, but the caller is not aware of this, the caller may also be another independent component. This enables complete inter-module decoupling without dependence on the fixed protocol. Therefore, protocol is required if interface compatibility is to be implemented. Check out my VIPER practice for this part: juejin.cn/post/684490… And this demo: github.com/Zuikyo/ZIKV…

Regarding the learning of the tool, the complexity of the tool increases step by step. Simply adding routing support for a module requires only three steps: 1. Create subclass2. Register module and protocol in subclass3. Return module instance in subclass. So if you look at a demo, you’ll see that pretty quickly. When more complex functions are needed, such as adding dependency injection, interface compatibility, and routing removal for modules, you can learn them on demand. To decouple, there must be some learning cost.

There should be only one caveat regarding possible problems in use. After the route is executed, a Router instance is returned, allowing the user to remove the route that has been executed, for example, by eliminating the interface that has already been displayed. If the caller holds the Router instance, be careful to use weak self in some blocks to prevent circular references.

Check whether the router is implemented correctly, whether the registered module has implemented the corresponding protocol, etc. 2. Check whether the call at runtime is correct, such as whether the wrong protocol is passed in, mainly because OC is a dynamic language, while Swift can ensure that the call is correct. 3. UIKit interface jump will fail in many cases, such as the implementation of push on an interface without navigation. Nothing happens, and the log can be logged and an assertion error can be triggered.

Divided into two replies, should not have omitted anything…… In fact, the most important thing is that dictionary parameter transmission requires both the caller and the module to use the same set of string keys, which will lead to certain coupling. It is impossible to seamlessly replace another module with the same function, and interface compatibility is also difficult. Fetching parameters with key in ViewDidLoad again leads to coupling between the module and the Router tool. If you want multiple apps to share common components, try to find a more decoupled approach. If you only use them within your own app, feel free to change them later if you need to.

thinking

Indeed, for componentized design ideas are really different, a person’s thinking will narrow, so send out everyone brainstorm.

I’ve always been reticent about third-party libraries for the following reasons: 1. You don’t fully know the implementation of third-party libraries, even if you read the source code; 2. Tripartite libraries are often over-designed for projects and do not use so many redundant methods. 3. For app slimming, although resources account for a large proportion, reducing the use of third-party libraries is another aspect. 4. You learn more by knowing the nature of a thing and building your own wheel, don’t you think?

Hope you can also have a heated discussion about Router design!! Voice your opinion!!

About:


Click the link below to jump!!

🌟 source code please click here 🌟 >>> like friends please click like >>> download the source code of the classmate please send the little star >>> have spare money of the trenches they can be rewarded >>> younger brother will launch a better article as soon as possible to share with you >>> your motivation is my motivation!!