The status quo

At present, the solutions of domestic industry: URL routing, CTMediator, Beehive. This time I’m going to introduce a retro, simple and new solution.

The problem

Two questions first.

What is a component?

What is a component? At the most granular level, a class that conforms to the single responsibility principle is a component; On a larger scale, classes that perform a separate function can be called components, such as the NSArray class cluster, the related class that implements the composite view UICollectionView; Larger ones, such as Foundation, UIKit, CoreData, etc., can also be called components, usually called framework or library. On a larger scale, a fully independent App can be called a component.

The business components we talk about in so-called componentization are basically at the second or third level.

How do the components at the first level communicate? Values are passed by constructs, attributes, block closures, protocol callbacks, and singletons.

What about level two? Actually, these are the four. What about the third level? Same as above.

What about the fourth level? Route by uniform resource locator (URL).

In fact, each large level can eventually be abstracted as the first level. Because no matter how many classes you have, you have to make it as simple as possible at the end, to follow Demeter’s law.

What is coupling?

class Car {
    private let engine = Engine( "V8" )
    public init() { 
        // implemenation }}Copy the code

This one up here is called tight coupling. An instance of a class is instantiated in another class. As a result, all cars in the system are V8 engines, and the car class cannot be tested.

class Car {
    private let engine: Engine
    public init(_ engine: Engine) { 
        // implemenation }}let bmw = Car(Engine("V8"))
let benz = Car(Engine("V10"))
Copy the code

This is called loose coupling. It is also easy to test by practicing the open close and inner substitution principle and decoupling through constructors or attribute passes. Usually this step is ok.

protocol EngineProtocol { let model: String }
class Car {
		private let engine: EngineProtocol
    public init(_ engine: EngineProtocol) { 
        // implemenation }}Copy the code

This is called almost no coupling. Any manufacturer can build a standard engine and build their own car, which is extremely scalable.

This decoupling is a feature of most languages, and solving the communication problem between components is nothing more than solving the transfer and coupling problems.

discuss

So let’s think about how apps communicate with each other.

When the user clicks on an App, is equivalent to tourists want to go to a scenic spot, he doesn’t have to know how to get to, someone will take him to the natural (of course he can know, but everyone knows that, you won’t get tourist industry and tour guide career, like project bigger case), the tour guide is (a class) in the system, Clicking on the App icon tells the guide to get there, and the user doesn’t have to worry about how to get there. The communication between apps is through openURL. Url is just like a protocol. When URL Schemes are specified by an application, the system will implement the binding between URL and application. When openURL is invoked, the system calls the implementation of the URL, finds the corresponding application to open, and calls the Application’s Delegate method.

Similarly, in our App, business components can be equivalent to App, so communication between components can be equivalent to URL routing, which has been practiced in the industry, such as URLNavigator and Mogujie routing. This approach is straightforward, but there are still scenarios. After all, apps are completely independent, and the development team of each App is completely independent. There is no other way between them except through the uniform resource Locator URL. In the same app, the business components are still independent and the development team is usually together, so urls are obviously not straightforward enough at the code level and not good enough for performance (additional parsing or memory resident). This is where design patterns solve the problem.

General people of different components have different maintenance, such as A component A and component B, now, if now to A demand, A business on A page by clicking A button to enter the B component of A page, then the two page is equivalent to two spots, A should not know who is the next scenic spot and how to get to, and CTMediator way is:

// In the A component
#pragma mark - event response
- (void)didTappedPushBViewControllerButton:(UIButton *)button
{
    UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"];
    [self.navigationController pushViewController:viewController animated:YES];
}
Copy the code

At the same time, this also caused the authors call named domain problem, the author also said that the category of the repo is maintained by B component, so obviously A component brothers to complete this function must be waiting for the B component brothers, A development stage of the personnel should not wait for each other, A component brothers only need to responsible for broker just send A message, For decoupling and synchronization to work, the A component should rely on its own abstraction to define A protocol:

@protocol AViewControllerMediating - (void)avc:(UIViewController *)vc didTappedPushBVCButtonWithContentText:(NSString *)text;
@end
#pragma mark - event response
- (void)didTappedPushBViewControllerButton:(UIButton *)button
{
    [self.mediator avc:self didTappedPushBVCButtonWithContentText:@"hello, world!"];
}
Copy the code

This way, the component A doesn’t need to wait for the component B. It can write A unit test mock class to test whether it sends the message correctly. If it can’t jump, it must be brother B’s pot.

And B components brother work somewhat heavy, he should not wait for A brother call to finish the development again, if A page level deeper, apparently layers into it A waste of time, usually is on the front page or write A jump, where temporary when B brothers after the development will inevitably ask A brother, All you need to do is move the previous implementation to the mediator’s AViewControllerMediating protocol implementation.

This advantage is that, when which day demand becomes not jump component B or B business will be cut off, when wanting to jump C business component, rewrite the person responsible for C component implementation can, you just need to change A place, don’t need to trouble A brother, but apparently CTMediator) that way involves two changes, changes in place is less, The higher the maintainability.

This is true decoupling, practicing the dependency inversion principle, rather than turning compilable class names into strings for what is called runtime decoupling.

Push, present, dismiss, pop are the real routing logic, the code should be in a place called the mediator, and if the jump logic needs to be changed or a bug, you only need to go to the mediator, not some component’s VC. There are certainly a lot of project view controllers involved in componentization, and not a lot of mediators.

At the level of abstraction, the main project can be regarded as the Controller in MVC, no matter how you split it into MVVM or VIPER, the work of coordinating Model and View in Controller is still in place. Aren’t M and V the first level of components? The main project is a natural place to schedule components, and the code that schedules different business components should be in the main project. Suppose we have two projects, one for iPhone and the other for iPad. Due to screen and interaction, the page jump of iPad is different from that of iPhone. Obviously, it is best to put these different jump logic into their respective main projects. And it does not affect you at all to subdivide the same part into a Mediator component.

The main project is a place where the components are integrated, and the mediator schedules the components. There is no problem with the mediator being in the main project. Explicit dependencies are clearly more robust than implicit ones.

Defects in existing schemes

The defect of CTMediator

Why do we say not to abuse singletons? In addition to singletons sharing state, it is easy to create tight coupling, like the one above, and even if it is used, it is through dependency injection, which also increases testability. The use of CTMediator obviously resulted in tight coupling of singletons.

The hard coding of CTMediator made me give it up, which was ugly. Depending on the OC runtime, it is not very versatile, and it must inherit NSObject to use in Swift, which is not elegant. According to the author, the category repO is maintained by the server, and the caller is dependent on the category, so isn’t it still dependent on the server brother? Instead of relying directly on the B component, we now have a B_category component and rely on it, which is permeated like a named domain, with the name of the service provider appearing in the dependency list.

Protocol – a class register

Mushroom Street and Beehive both use this approach. Although this method uses the agreement, but with my above agreement is completely different meaning. Component A calls component B, and then component B provides A protocol that implements itself. This protocol is placed in component B and sinks in publicProtocolDomain. h, and is retrieved from module A via the ModuleManager. Manual black question mark? That’s why most people find this scheme tricky.

An agreement is defined by one person and followed by others. An agreement is like a contract. Do you write a contract and sign it?

B component to provide external services, provide a facade class is enough (preferably, do not provide, ensure that the public of the public, the private of the private). The protocol is defined by A component, and then implemented by the mediator, because the mediator itself depends on each component, it is natural to implement A protocol to call B methods.

The new project Coordinator

According to the previous thinking, decoupling is simple and there is no need to create a category repO. The so-called Target is actually a component module’s appearance class that provides a set of easy-to-use methods. I prefer to call it a Service or Coordinator.

Take a look at the origins of the Coordinator pattern. In fact, the new scheme is not new, and the idea used has been mentioned countless times before, so I called it retro at the beginning, and there is a new implementation on this basis.

This solution uses the true idea of POP and is perfectly suitable for componentization, with no registration, no runtime, low memory footprint, fast execution, compile-time checking, Mock testing, no singletons, and URL routing.

Coordinator is either a Mediator or a Router. However, Apple is also called a Coordinator. In fact, Apple also uses this mode. Including but not limited to, UICollectionViewDropCoordinator, UIViewControllerTransitionCoordinator. The idea is the same, the concrete realization is different.


conclusion

The purpose of componentization is to make the code more reusable, not to put the main project on stilt, as long as you follow the principle of SOLID flexible use of design pattern, and folder classification, even if you in a project can be called componentization, make Pod or Package is the finishing touch, split too much or even counterproductive. If the team is small and the business modules are small, there is no need to isolate the business at the Pod level. Isolation of the underlying components is sufficient.

Finally, without further ado, let’s go to the code. The demo is here.