This article belongs to “Jane Book — Liu Xiaozhuang” original, please note:

< Jane books – Liu Xiaozhuang > http://www.jianshu.com/p/67a6004f6930


Some time ago, the company planned to reconstruct the project. To be precise, it should rewrite a project according to the previous product logic πŸ˜‚. Before the reconstruction of the project, the selection of architecture was involved. My friends and I studied the componentalized architecture together and planned to reconstruct the project into a componentalized architecture. Of course, it is not a direct copy of the company, or to the specific business needs of the company to design the architecture.

In the process of learning componentize architecture, I learned a lot from many high-quality blogs, such as Mogujie Li Zhong, Casatwy, Bang’s blog. I also encountered some problems in the learning process. I communicated with some iOS friends on Weibo and QQ. Thank you very much for your help.

This article mainly analyzes the componentization scheme proposed by Mogujie and Casatwy before. The componentization architecture of Didi, Taobao and wechat will be briefly mentioned later. Finally, the componentization architecture designed by our company will be briefly described.


The origin of componentized architecture

With the continuous development of mobile Internet, there are more and more program code and business, and the existing architecture is no longer suitable for the development speed of the company’s business, and many of them are facing the problem of reconstruction.

In corporate projects, if the project is small, the plain single-project +MVC architecture will suffice for most of the requirements. However, for large projects like Taobao, Mogujie and wechat, the original single engineering framework is not enough to meet the architectural requirements.

Take Taobao for example. In the “All in Wireless” strategy launched by Taobao in 2013, most of alibaba’s businesses were added to mobile Taobao, resulting in the outbreak of client business. In this case, a single-engineering architecture is no longer enough to meet existing business requirements. In this case, Taobao started the reconstruction of plug-in architecture in 2013, and then ushered in the largest reconstruction in the history of mobile Taobao in 2014, reconstructing the project into a component-based architecture.

The modular architecture of Mushroom Street

why

As a project gets bigger and bigger, with more and more developers, it can run into a lot of problems.

  • Service modules are not clearly divided, and the coupling degree between modules is very high, which makes it difficult to maintain.
  • All module code is written in a project to test a module or function, and the entire project needs to be compiled and run.

To solve the above problem, consider adding an intermediate layer to coordinate calls between modules. All calls between modules will pass through the middle layer.

But it turns out that by adding this middle layer, coupling still exists. The middle tier is coupled to the invoked module, and other modules need to be coupled to the middle tier to make calls. There is the same coupling problem that existed before, and it is inherently more troublesome than before.

Architecture to improve

So what you should do is only have other modules couple to the middle layer, and the middle layer does not couple to other modules. For this problem, you can use a componentized architecture that treats each module as a component. And create a main project that is responsible for integrating all the components. The benefits are numerous:

  • The business is divided more clearly, it is easier for newcomers to take over, and development tasks can be assigned by component.
  • Project maintainability is stronger and development efficiency is improved.
  • Better troubleshooting, a component failure, directly to the component to handle.
  • During development tests, you can compile only your own code, rather than the entire project code.
  • Easy integration, the project needs which module directly throughCocoaPodsIntegration.

After component-based development, each component can be treated as an independent APP, and each component can even adopt different architectures, such as MVVM, MVC, MVCS, etc., according to their own programming habits.

MGJRouter scheme

Mogujie realizes the middle layer through MGJRouter, which forwards messages between components. It is more like a “router” in name. This is done roughly by pre-registering a block in the component that provides the service and then calling the block from the URL in the caller component. Here’s how.

Architecture design

MGJRouter is a singleton object, and maintains a registry in the format of “URL -> Block” internally. Through this registry, the block registered by the service side is saved, and the caller can map the block through THE URL, and initiate calls to the service side through the MGJRouter.

MGJRouter is the scheduling center of all components, responsible for the call, switch, special processing and other operations of all components, and can be used to deal with the relationship between all components. In addition to native page parsing, you can also jump to H5 page based on URL.

Each server component provides a PublicHeader in which all the functions provided by the current component are declared. In this way, other components can look at the PublicHeader to find out what functions the current component has. Each block corresponds to a URL from which the caller can make a call to the block.

#ifndef UserCenterPublicHeader_h
#define UserCenterPublicHeader_h

/** Jumps to the user login page */
static const NSString * CTBUCUserLogin = @"CTB://UserCenter/UserLogin";
/** Jump to user registration page */
static const NSString * CTBUCUserRegister = @"CTB://UserCenter/UserRegister";
/** Get user status */
static const NSString * CTBUCUserStatus = @"CTB://UserCenter/UserStatus";

#endif
Copy the code

In the component internal implementation of the block registration work, as well as block to provide services to the external code implementation. When registering, you need to pay attention to the registration timing and ensure that the block corresponding to the URL has been registered when calling.

The Mogujie project uses Git as a version control tool, treats each component as a separate project, and establishes a master project to integrate all components. The integration is done through CocoaPods in the main project, integrating all components into the project as a two-party library. Detailed integration techniques are covered in the section “Standard componentized Architecture Design” below.

MGJRouter call

The following code simulates the registration and invocation of the detail page, passing the ID parameter during the invocation. Parameters can be passed in two ways, similar to Get requests concatenating parameters after urls and passing parameters through dictionaries. Here is the sample code for registration:

[MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) {
 	// Now you can get the parameters and provide the corresponding services for other components
 	NSString uid = routerParameters[@"id"];
}];
Copy the code

A call is made to the block method registered on the detail page with the URL parameter passed in to the openURL: method. The call is similar to a GET request, with the URL address followed by concatenation of parameters.

[MGJRouter openURL:@"mgj://detail? id=404"];
Copy the code

You can also pass parameters by dictionary. MGJRouter provides methods with dictionary parameters so that you can pass parameters of other types than strings, such as object type parameters.

[MGJRouter openURL:@"mgj://detail" withParam:@{@"id" : @ "404"}];
Copy the code
Transfer values between components

Sometimes in the process of inter-component invocation, the server is required to return the corresponding parameters after completion of the invocation. Mogujie offers another way to do this.

[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary *routerParamters){
 	return @42;
}];
Copy the code

Make the call as follows and get the return value returned by the server. All you need to do is pass the correct URL and parameters.

NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"];
Copy the code
Short chain management

One problem is that there are a lot of hard-coded urls and parameters in the Mushroom Street componentized architecture. The problem is that the invocation fails when the URL is written incorrectly during code implementation, and the argument is a dictionary type, so the caller doesn’t know what the server needs.

For the management of these data, Mogujie developed a Web page, which manages all urls and parameters uniformly. Android and iOS both use this set of urls to maintain uniformity.

Based on the component

There are many common parts of a project, such as encapsulated network requests, caching, data processing, and resource files used in the project. Mogujie also regards these parts as components, which are divided into basic components, located below the business components. Using the same set of base components for all business components also ensures the uniformity of the common parts.

Protocol scheme

The overall architecture

In order to solve the problems of URL hard coding and unclear dictionary parameter types in MGJRouter scheme, Mogujie introduced the Protocol scheme on the basis of the original componentalization scheme. The Protocol scheme consists of the ModuleManager class for communication between components and the MGJComponentProtocol class.

Messages are called and forwarded through the middleware ModuleManager, and a mapping table is maintained inside the ModuleManager, which is changed from “URL -> block” to “Protocol -> Class”.

Create MGJComponentProtocol file in middleware, the server component can be used to call methods are defined in Protocol, all the Protocol of the server are defined to MGJComponentProtocol file, If there are more protocols, you can also separate several file definitions. In this way, all callers still rely only on the middleware and do not need to rely on other components besides the middleware.

Each component in the Protocol scheme requires an MGJModuleImplement, which implements the Protocol method corresponding to the current component, that is, the implementation of external services. Register its own Class with the ModuleManager when the program starts running and reflect Protocol as a string as a key.

The Protocol scheme still needs to register the service in advance, because the Protocol scheme returns a Class and calls the method as an object, without directly calling the internal logic of the Class. You can register the classes of the Protocol scheme in the MGJModuleImplement corresponding to the Class, or create a RegisterProtocol Class specifically.

The sample code

Create MGJUserImpl class as User component public class, and define MGJUserProtocol in MGJComponentProtocol.h, by MGJUserImpl class to achieve the method defined in the protocol, complete the process of providing services externally. Here is the protocol definition:

@protocol MGJUserProtocol <NSObject>
- (NSString *)getUserName;
@end
Copy the code

Class complies with the Protocol and implements the defined methods. The external world gets the Class through Protocol and instantiates it as an object to invoke the Protocol methods implemented by the service provider.

Protocol registration method for the ModuleManager. During registration, Protocol is reflected as a string and stored as a key, and the Class that implements the Protocol is stored as a value. When a Class is fetched through Protocol, the Class is mapped from the ModuleManager through Protocol.

[ModuleManager registerClass:MGJUserImpl forProtocol:@protocol(MGJUserProtocol)];
Copy the code

During invocation, the registered Class is mapped from the ModuleManager through Protocol, the obtained Class is instantiated, and the Protocol method implemented by Class is invoked to complete the service invocation.

Class cls = [[ModuleManager sharedInstance] classForProtocol:@protocol(MGJUserProtocol)];
id userComponent = [[cls alloc] init];
NSString *userName = [userComponent getUserName];
Copy the code

Project Invocation process

Mogujie is a mixed method of MGJRouter and Protocol. The two implementations have different invocation methods, but the general invocation logic and implementation idea are similar. If the MGJRouter cannot meet the requirements or is not convenient to call, it can be invoked through Protocol.

  1. After entering the program, register the server component with the MGJRouter. Each URL corresponds to an implementation of a block. The code in the block is the service provided by the component. The caller can invoke the service through the URL.

  2. The caller calls the openURL: method through the MGJRouter and passes in the URL corresponding to the invoked code. The MGJRouter will look up the corresponding block implementation according to the URL, thus calling the code of the component to communicate.

  3. When a block is called and registered, it has a dictionary for passing parameters. The advantage of this is that the type and number of parameters are theoretically unlimited, but many hard-coded key names are required in the project.

Memory management

There are two modularization schemes for Mogujie: Protocol and MGJRouter, but register operation is required for both. Protocol registers classes, MGJRouter registers blocks, and the registry is a dictionary of type NSMutableDictionary. The owner of the dictionary is a singleton object, which results in memory persistence.

Here is an analysis of the memory consumption of the two implementations:

  • Let’s start with the memory problems that MGJRouter can cause. Because blocks hold objects inside code blocks, they can easily leak memory if used improperly. Blocks themselves do not cause large memory leaks. They are mainly internally referenced variables, so you need to pay attention to strong references and use weak to modify the corresponding variables appropriately. And, when appropriate, release the corresponding variables. Except for references to external variables, try not to create objects directly inside a block, instead using method calls.

  • The protocol implementation is similar to block memory resident. You just change the block object stored to a Class object. This is actually a stored class object, and class objects are inherently singleton, so there is no extra memory footprint.

Casatwy componentization scheme

The overall architecture

Casatwy componentization scheme can handle two kinds of invocation, remote invocation and local invocation, with two interfaces for two different invocation modes.

  • After the remote invocation is sent to the current application through the AppDelegate proxy method, the remote interface is called and some processing is performed internally. After the processing is complete, the local interface is called inside the remote interface to realize the local invocation as the remote invocation service.

  • Local calls by performTarget: action: params: method is responsible for, but the caller is generally not directly call performTarget: method. CTMediator provides methods that specify parameters and method names externally, and calls performTarget: the method and parameter conversion inside the method.

Architectural Design Ideas

Casatwy realizes componentization through CTMediator class, in which an interface with definite parameter type is provided externally. Inside the interface, Target and Action of the service component are called through performTarget method. Since the invocation of the CTMediator class is actively discovered by the Runtime, the service provider is completely decoupled from this class.

However, if all methods provided by CTMediator are placed in this category, it will cause a great burden and code quantity to CTMediator. The solution is to create a CTMediator Category for each of the server components and place the performTarget calls to the server in the corresponding Category, which belongs to the CTMediator middleware, thus achieving a sensory separation of interfaces.

For a server component, each component provides one or more Target classes in which Action methods are declared. The Target class is a “service class” provided by the current component. Target defines all the services in the current component. CTMediator actively discovers the service through the Runtime.

All Action methods in Target have only one dictionary parameter, so the parameters that can be passed are flexible, which is the concept of de-modeling proposed by Casatwy. In the Action method implementation, the dictionary parameters passed in are parsed and classes and methods inside the component are called.

The architecture analysis

Casatwy provides us with a Demo, through which we can have a good understanding of the design ideas of Casatwy. Now I will explain this Demo according to my understanding.

When you open the Demo, you can see that the file directory is very clear. In the figure above, the blue box is the middleware part and the red box is the business component part. I made a simple comment for each folder, including its responsibilities in the schema.

Two methods for remote and local calls are defined in CTMediator, and other business-related calls are done by categories.

// Remote App call entry
- (id)performActionWithUrl:(NSURL *)url completion:(void(^) (NSDictionary *info))completion;
// Local component call entry
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
Copy the code

ModuleA’s Category, defined in CTMediator, provides a function for other components to get the controller and jump to it. Here is the code. Because casatwy uses performTarget to call, there are a lot of hard coded strings involved. Casatwy uses constant strings to solve this problem, which is easier to manage.

#import "CTMediator+CTMediatorModuleAActions.h"

NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";

@implementation CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail {
	UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key":@"value"}];
	if ([viewController isKindOfClass:[UIViewController class]]) {
    	// After the View Controller is delivered, it can be selected by the outside world as push or present
    	return viewController;
	} else {
    	// The exception scenario is handled here, depending on the product logic
    	return [[UIViewControlleralloc] init]; }}Copy the code

The following services provided by the ModuleA component, defined in the Target_A class, can be invoked by CTMediator via the Runtime, a process called discovery service.

The passed parameters are handled in Target_A, and the internal business logic is implemented. The method occurs inside the ModuleA, so that services inside components are not affected by external services and are not intrusive to internal services.

- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
	// Parse the dictionary arguments passed and call the code inside ModuleA
	DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = params[@"key"];
	return viewController;
}
Copy the code

Naming conventions

In large projects, there is a lot of code and naming conflicts need to be avoided. Casatwy prefixes this problem. As can be seen from the Demo of Casatwy, the Target of its ModuleA is named Target_A, which can distinguish the Target of each component. The called Action named Action_nativeFetchDetailViewController:, can distinguish between components within the method and the available methods.

Casatwy names classes and methods according to their functions as prefixes, so that component correlation and component internal code are well separated.

Results analysis

Protocol

According to the results of my research and use, Protocol is not recommended. First of all, the code volume of Protocol scheme is more than that of MGJRouter scheme, and the call and registration code volume is very large, which is not very convenient to call.

In essence, Protocol scheme is to instance a variable through class object and call the method of variable. It does not change the interaction scheme between components in a real sense, but MGJRouter scheme changes and unify the call mode between components through URL Router.

In addition, Protocol does not support Remote Router and cannot directly handle calls from Push, so it is not as flexible as MGJRouter.

CTMediator

I do not recommend CTMediator, which is a very bloated scheme. Although many categories are provided for CTMediator, in practice the calling logic between components is coupled to the middleware. ** The same problem exists with the Protocol scheme, that is, the call code is very large, and it is not convenient to use.

There are many problems of hard coding in CTMediator scheme. For example, target, action and parameter names are hard coded in middleware, so this method of calling is not flexible and direct.

But Casatwy proposed the idea of de-modeling, which I think is very flexible in terms of componentization, which I agree with. Compared with MGJRouter, it also adopts de-model parameter passing mode, rather than directly passing Model objects. Componentized parameter passing is not suitable for passing Model objects, but models can still be used inside components.

MGJRouter

MGJRouter program is a very lightweight program, its middleware code is a total of less than two hundred lines, very concise. It is easy to call directly from the URL when called, and I recommend using this scheme as middleware for componentized architectures.

One of the most powerful aspects of MGJRouter is that it unifies remote and local calls. This makes it possible to make any allowed inter-component calls via Push, which is a great help to project operations.

The three schemes all realize the decoupling between components. MGJRouter and Protocol are the coupling between the caller and middleware, while CTMediator is the coupling between middleware and components, both of which are unidirectional coupling.

Interface class

In the three schemes, the server component provides a PublicHeader or Target externally, uniformly defines the externally provided service in the file, and most of the implementation codes of communication between components are in the file.

However, the three implementation schemes have different implementation methods. The two schemes of Mogujie require registration, and both Block and Protocol can provide services only after registration. Casatwy’s scheme does not need to be called directly through the Runtime.

Component architecture design

In the above article, CTMediator of Casatwy scheme, MGJRouter and ModuleManager of Mogujie scheme were mentioned, which will be collectively referred to as middleware later. Now let’s design a set of componentiized architecture.

The overall architecture

In a componentized architecture, you need a master project that integrates all components. Each component is a separate project, with a different Git private repository created to manage it, and each component is developed by a corresponding developer. Developers only need to focus on the code of their related components, not the other components, so that newcomers can get started.

Component partitioning requires attention to component granularity, which can be large or small according to the service. Component division Each service module can be divided into components. Basic modules such as networks and databases can also be divided into components. The project will use many resource files, configuration files, etc., should also be divided into corresponding components, to avoid duplicate resource files. The project is fully componentized.

Each component needs to provide calls externally, registering the corresponding URL within the exposed class or component. The code that a component handles middleware calls should be invader to other code and only be responsible for parsing the data that is passed through and for functions that are invoked within the component.

Component integration

Each component is a separate project that is uploaded to a Git repository after the component is developed. The main project integrates components through Cocoapods and only requires pod Updates to integrate and update components. This makes it easy to manage each component as a third party.

Cocoapods can control the version of each component, such as rolling back a component to a specific version in the main project by modifying the PodFile. I chose Cocoapods because of its power to easily integrate projects and reuse code. With this integration approach, the problem of code conflicts in traditional projects can be avoided.

Integrated way

After reading Bang’s blog, I asked Bang specifically about the integration method of componentized architecture. Based on what I learned from chatting with Bang on Weibo and other blogs, there are two main ways to integrate components in the main project — source code and framework, but both are integrated through CocoaPods.

Whether you use CocoaPods to manage source code, or directly manage the framework, the integration method is the same, are directly pod update CocoaPods operations.

These two component integration schemes have their own advantages and disadvantages in practice. Directly in the main project integrated code file, you can see its internal implementation source code, convenient debugging in the main project. Integrate with the framework in a way that speeds up compilation and provides good confidentiality for each component’s code. If your company is serious about code security, consider a framework.

Such as mobile QQ or alipay such a large program, generally will take the form of the framework. Typically, such large companies will have their own component library, which often represents a large functional or business component that can be added directly to the project. About componentization library in the back of taobao componentization architecture will be mentioned.

Resource file

For integration of images in a project, the image can be treated as a single component, with only the image file and no code in the component. Images can be managed by using bundles and image assets. For bundles, different bundles are created for different business modules; for image assets, different assets are created according to different module classification and all resources are placed in the same component.

I still prefer using assets over Image assets, because assets provide many functions (such as setting the stretching range of images), and after packaging, images are packaged in a. Cer file and cannot be seen. (Now you can also use the tool to parse the. Cer file and get the image inside.)

With Cocoapods, all resource files are stored in a Single PodSpec. The main project can refer to this podspec directly. If the name of the podspec is Assets, the configuration information of Assets can be written as follows:

s.resources = "Assets/Assets.xcassets/ ** / *.{png}"
Copy the code

For the main project, add it directly to your podfile:

pod 'Assets', :path => '.. /MainProject/Assets'Copy the code

This allows direct access to Assets files (not only images, but also SQLite, JS, and HTML, as well as configuration information in S. Resources) in the main project.

advantages

  • Component-based development can greatly improve code reuse, and components can be directly used in other projects. This advantage will be emphasized in the following Taobao architecture.

  • For debugging, it can be done in each component. Individual business components can be submitted directly for testing, making it easier to test. Finally, after the component development is completed and the test is passed, all components are updated to the main project and submitted to the test for integration test.

  • Through this division of components, the development progress of components is not affected by other services, and multiple components can be developed in parallel. The communication between components is entrusted to the middleware. The classes that need to communicate only need to contact the middleware, while the middleware does not need to couple other components, thus realizing the decoupling between components. Middleware is responsible for dealing with the scheduling between all components and plays the role of controlling the core among all components.

  • Componentization framework clearly divides different modules, constraines developers to implement componentization development from the overall architecture, and realizes the physical isolation between components. A componentized architecture creates a natural barrier between modules, preventing one developer from lazily referring directly to header files, causing coupling between components and breaking the architecture.

  • When developing with a component-based architecture, since everyone is responsible for their own components and code commits only to their own repository of responsible modules, the problem of code conflicts is much less.

  • In the future, if a service changes significantly and the related code needs to be refactored, the refactoring can be carried out in a single component. Componentized architecture reduces the risk of refactoring and ensures robust code.

The architecture analysis

In the MGJRouter scenario, the call is made by calling the OpenURL: method and passing in the URL. In view of the fixed format such as URL protocol name, the switch between H5 and Native can be controlled by using the configuration table by judging the protocol name. The configuration table can be updated from the background, and only the protocol name needs to be changed.

mgj://detail? id=123456 http://www.mogujie.com/detail?id=123456Copy the code

Suppose there is a serious bug in the native component on the line, change the original local URL of the configuration file into the URL of H5 in the background, and update the client configuration file.

The switch can be completed by passing in the URL of H5 when calling MGJRouter. MGJRouter determines that if the URL of H5 is passed in, it will directly jump to webView. And URL can pass parameters to MGJRouter, just need to do parameter interception within MGJRouter.

With componentized architecture development, communication between components has a cost. Therefore, try to encapsulate services inside components and provide only simple interfaces externally. The principle of “high cohesion, low coupling”.

Grasp the fine degree of component partition granularity, too detailed project is too scattered, too large project components overstaffed. But projects grow from small to large, so constant refactoring is the best way to get a handle on how detailed this component is.

Pay attention to the point

If you integrate components into your main project in binary form, such as a framework, you need to pay attention to the use of precompiled instructions. Because the precompiled instructions are already packaged in the component binaries when the framework is packaged, the precompiled instructions are no longer in effect in the main project, but are encoded into fixed binaries when the precompiled instructions are packaged.

Our Company structure

For the project architecture, it is important to design the architecture based on the business. Different projects and businesses will have different componentized scheme designs, so the architecture that best suits the business of the company should be designed.

Architecture design

The project of our company is a map navigation application. The core modules and basic modules under the business layer account for a large proportion, involving map SDK, route calculation, voice and other modules. The basic module is relatively independent and provides many call interfaces externally. From this, we can see that the company’s project is a project focusing on logic, unlike e-commerce and other apps.

The overall architectural design of the project is: hierarchical architecture + componentized architecture. The specific implementation details will be explained in detail below. Adopting this kind of mixed structure for the overall architecture is beneficial to the management and hierarchy division of components and meets the business needs of the company.

When designing the architecture, we split the entire project into components, with a high degree of componentization. Each component is integrated in the project through Podfile and unified communication between all components through URLRouter.

Componentized architecture is the overall framework of the project, and for the implementation of each business module in the framework, it can be any architecture, such as MVVM, MVC, MVCS, etc., as long as the communication mode between components is unified through MGJRouter.

Layered architecture

Componentized architecture has no hierarchy in physical structure, only the partition relationship between components. However, on the basis of componentized architecture, we should design our own hierarchical architecture according to projects and businesses. This hierarchical architecture can be used to distinguish the layers and responsibilities of components. Therefore, we designed the overall architecture of hierarchical architecture + componentized architecture.

At the beginning of the project, our company designed a three-layer architecture: business layer -> core layer (high + LOW) -> basic layer, and the core layer is divided into high and LOW. However, this kind of architecture can cause the problem that the core layer is too heavy and the base layer is too light, which is not suitable for componentized architecture.

In the three-tier architecture, it can be found that the low layer does not have coupled business logic, and is relatively independent in the same layer, with single and basic responsibilities. The low layer sinks into the base layer and merges with the base layer. So the architecture was redivided into three layers: business layer -> core layer -> base layer. Previously, the base layer was mostly resource files and configuration files, which did not have a high presence in the project.

In a hierarchical architecture, only the upper layer can depend on the lower layer, and the lower layer cannot depend on the upper layer. The lower layer does not contain the upper-layer service logic. Common resources and code that exist in the project should be sunk into the lower layers.

Responsibility division

In the three-tier architecture, the service layer processes upper-layer services and divides different services into corresponding components, such as IM components, navigation components, and user components. The relationship between components at the business layer is complex, involving the communication between components and the reference of components at the business layer to components at the lower layer.

The core layer is located below the service layer and provides service support for the service layer. Components such as network and voice recognition should be divided into the core layer. The core layer should minimize and minimize dependencies between components. The core layer also needs to support each other at times, for example, the longitude and latitude components need the network components to provide support for network requests, which is inevitable.

Other basic modules are placed in the base layer as basic components. For example, AFN, map SDK, encryption algorithm, etc., these components are independent and unadulterated with any business logic. Their responsibilities are more single and lower than the core layer. It can include third-party libraries, resource files, configuration files, and base libraries. The base layer components should not have any dependencies on each other.

Components should be designed in accordance with the “high cohesion, low coupling” design specification, component invocation should be simple and direct, reducing the other processing of the caller. The core layer and the base layer can be divided according to whether it involves business, whether it involves communication between peer components, and whether it changes frequently. If these points are met, they are placed in the core layer; if not, they are placed in the base layer.

Integrated way

After creating a new project, first integrate the configuration file, URLRouter, App container, etc. into the main project, do some basic project configuration, and then integrate the required components. Once the project has been split into a componentized architecture, the application integrates all components the same way, using podfiles to integrate the required components into the project. Through the componentized way, the development of new projects becomes very fast.

After the integration of business layer and core layer components, the communication between components is carried out by URLRouter, and the project is not allowed to rely on component source code directly. The base layer components are directly dependent upon after integration, such as resource files and configuration files, which are used directly in the main project or components. The third party library is encapsulated by the business of the core layer, after which the URLRouter communicates, but the core layer is also directly dependent on the source code of the third party library.

There are two ways to integrate components, source code and framework form, we use framework way integration. Because the project is generally relatively large to use componentization, but large projects will have the problem of compilation time, if through the framework will greatly reduce the compilation time, can save the developer time.

Intercomponent communication

For communication between components, MGJRouter scheme is adopted. Because MGJRouter is now very stable, and can meet the needs of mogujie App of such magnitude, it is proved to be very good, there is no need to write a set of their own and then slowly step on the pit.

The benefit of MGJRouter is that it can be called in a flexible way, registering with MGJRouter and handling callbacks in the block, either directly through the URL or through the URL+Params dictionary. Because the value is passed through URL concatenation parameter or Params dictionary, the parameter type has no quantitative limit and the transmission is more flexible. Completion logic can be handled in the completionBlock after being called through openURL:.

The problem with MGJRouter is that there is a lot of Hardcode involved in writing the code for communication between components. For the problem of Hardcode, Mogujie developed a background system that defined all urls and parameter names required by the Router into this system. We maintain a Plist table, internally divided by component, containing urls and parameter names and callback parameters.

Routing layer security

Componentized architecture needs to pay attention to the security of routing layer. The MGJRouter scheme can handle local and remote OpenURL calls. If it is OpenURL calls between components in the program, no verification is required. However, cross-application OpenURL calls need to be checked for validity. This is to prevent third-party forgery of OpenURL calls, so the validity check of OpenURL calls from outside applications, such as calls from other applications and server Remote Push, is carried out.

In the design of validity check, each valid URL invoked from outside the application carries a token, which is verified locally. The advantage of this approach is that there are no network request constraints and delays.

Proxy method

In the project, the proxy mode is often used for value transmission. The proxy mode is mainly divided into three parts in iOS, namely, agreement, agent and principal.

Componentized architecture, however, involves proxy value transfer between components. The proxy needs to be set as the delegate of the principal, but components cannot be coupled directly. For this cross-component proxy situation, we pass the proxy object directly to another component as a parameter through the MGJRouter, where the proxy is set up.

HomeViewController *homeVC = [[HomeViewController alloc] init];
NSDictionary *params = @{CTBUserCenterLoginDelegateKey : homeVC};
[MGJRouter openURL:@"CTB://UserCenter/UserLogin" withUserInfo:params completion:nil];

[MGJRouter registerURLPattern:@"CTB://UserCenter/UserLogin" toHandler:^(NSDictionary *routerParameters) {
    UIViewController *homeVC = routerParameters[CTBUserCenterLoginDelegateKey];
    LoginViewController *loginVC = [[LoginViewController alloc] init];
    loginVC.delegate = homeVC;
}];
Copy the code

The definition of the protocol is placed in the publicHeader. h file of the proxy component. The proxy component only references this publicheader. h file without coupling the internal proxy code. To avoid coupling in a defined proxy method, no objects related to the business inside the component can appear in the method, only the classes of the system can be passed. If the interaction case is involved, the return value of the protocol method is used.

Component and the cords

MGJRouter can pass an NSDictionary parameter to openURL:. After I contact RAC, I wonder if I can change the NSDictionary parameter to RACSignal and send a signal directly.

Registered MGJRouter:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@" Liu Xiaozhuang"];
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"disposable");
    }];
}];

[MGJRouter registerURLPattern:@"CTB://UserCenter/getUserInfo" withSignal:signal];
Copy the code

Call MGJRouter:

RACSignal *signal = [MGJRouter openURL:@"CTB://UserCenter/getUserInfo"];
[signal subscribeNext:^(NSString *userName) {
    NSLog(@"userName %@", userName);
}];
Copy the code

It works. The advantage of using RACSignal is that it is more flexible than passing dictionaries directly and has many of the features of RAC. But there are also many disadvantages, signal control is not good, it is also easy to dig holes, whether to use or depends on the team.

Constants defined

Constants, such as notification names and constant strings, are often defined in a project. These constants have a strong relationship with the component to which they belong and cannot be separated from other components. But these variables are not numerous, and they are not present in every component.

Therefore, we declare these variables in the publicHeader. h file. Other components can only reference the publicheader. h file, not the internal business code of the component, so as to avoid the coupling problem between components.

H5 and Native communication

H5 page is often used in the project, if you can click on the H5 page to call up the Native page, so that the fusion of Native and H5 will be better. Therefore, we designed a set of interactive scheme between H5 and Native. This scheme can use URLRouter to call up the Native page, and the implementation is very simple. Moreover, this scheme does not conflict with the original jump logic of H5.

After creating an H5 page through iOS built-in UIWebView, H5 can communicate with Native by calling the following JS functions. The call can be passed in a new URL, which can be set to the URL of the URLRouter.

window.location.href = 'CTB://UserCenter/UserLogin? userName=lxz&WeChatID=lz2046703959';Copy the code

When the H5 page is refreshed with JS, the following proxy methods are invoked. If the method returns YES, it jumps based on the URL protocol.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
Copy the code

The system checks the communication protocol and refreshes the current page if it is a standard protocol, such as HTTP. If the jump protocol is registered in the URL Schame, it will be called to the system proxy method of the AppDelegate through the system openURL: method, and the URLRouter will be called in the proxy method, and the native page will be invoked through the H5 page.

AppService

During application startup, some initialization operations are usually performed. Some initialization operations are required to run the program, such as crash statistics, establishing a long connection to the server, and so on. Some components may have dependencies on initialization operations, such as network components that rely on requestToken.

An AppService should be created to centrally manage startup initialization operations, including creating a root controller. Some initialization operations need to be performed as soon as possible, and some do not need to be performed immediately. You can set priorities for different operations to manage all initialization operations.

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger.CTBAppServicePriority) {
    CTBAppServicePriorityLow.CTBAppServicePriorityDefault.CTBAppServicePriorityHigh};@interface CTBAppService : NSObject
+ (instancetype)appService;
- (void)registerService:(dispatch_block_t)serviceBlock 
               priority:(CTBAppServicePriority)priority;
@end
Copy the code

The Model layer design

There are many model definitions in the project. Where should these models be defined after componentization?

Casatwy’s view of Model classes is de-modelization, which simply means using dictionaries instead of models to store data. This is a good way to address data transfer between components for componentized architectures. But going to Model, there’s a lot of field reading code, and it’s not nearly as easy to use as Model classes.

Because model classes are business related, they must theoretically reside in the business layer, the business component layer. However, to pass a model object as a parameter from one component to another, the model class is not appropriate in either the caller or the called-in component, and more than two components may use the model object. This way, using model objects in other components inevitably leads to references and coupling.

It is not desirable to maintain a copy of the same model class in all components that use the model object, or to maintain a different structure of the model class, so that later business changes to the model class will be cumbersome.

Design scheme

What if all the model classes were pulled out separately and a single model component was defined?

It seems feasible to sink the component that defines the model down to the base layer. The model component does not contain the business, but only declares the class of the model object. By pulling out the original Model class definitions for each component and placing them in a single component, the original component’s Model layer becomes lightweight, which also benefits the overall project architecture.

When making inter-component calls through the Router, values are passed through the dictionary, which provides flexibility. When using the Model layer inside a component, use the Model classes defined in the Model component. The Model layer suggests that it is more convenient to use the form of Model objects, rather than the overall use of de-modeling design. When you receive dictionary parameters from other components, you can convert the dictionary into a Model object through an initialization method provided by the Model class, or through another transform Model framework.

@interface CTBStoreWelfareListModel : NSObject
/** * Custom initialization method */
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
Copy the code

The persistence solution of our company uses CoreData. All model definitions are in CoreData components, so there is no need to create a separate model component.

Dynamic concept

Our company’s project is a conventional map project, the home page and Baidu, Autonavi and other mainstream map navigation App, there are a lot of controls added on the map. In some versions controls are added, in others they are removed, and the corresponding functionality is hidden.

So, when I was in a meeting with my friends in the group, I was thinking about whether I could send code to the server for the layout of the home page! This allows for dynamic layout of the front page, such as displaying a control at a specified time when there is activity, to avoid slow App Store approval issues. Or there is a problem with a module online, you can immediately remove the problematic module.

To solve this problem, we designed a set of dynamic configuration scheme, which can configure the entire App.

Configuration table design

For the problem of dynamic configuration, we simply designed a configuration table, the initial plan is to test the water on the home page first, later may be arranged on more pages. In this way, the entry of each module of the application program can be controlled through the configuration table, and the jump between pages can be controlled through the Router, which is very flexible.

Using the built-in configuration table the first time you install the program, and then replacing the local configuration table with the server each time, allows you to dynamically configure the application. The following is a simple design of the configuration data. The CONFIGURATION information in JSON is the home page configuration information, which is used to simulate the data delivered by the server. The real server will send more fields than this.

{
    "status": 200."viewList": [{"className": "UIButton"."frame": {
                "originX": 10."originY": 10."sizeWidth": 50."sizeHeight": 30
            },
            "normalImageURL": "http://image/normal.com"."highlightedImageURL": "http://image/highlighted.com"."normalText": "text"."textColor": "#FFFFFF"."routerURL": "CTB://search/***"}}]Copy the code

For the data returned by the server, we create a set of parsers that parse and “transform” the JSON into standard UIKit controls. Click events are redirected to the Router, so the flexibility of the home page is proportional to the use of the Router.

This solution is similar to React Native, delivering pages from the server to display effects, but not as comprehensive as React Native. Is a relatively lightweight configuration solution, mainly used for page configuration.

Dynamic Resource Configuration

In addition to page configuration, we found that map apps generally have the problem of too large IPA, which consumes much traffic and time in downloading. So we were wondering if we could dynamically configure resources and load resource packages while the user is running the program.

We want to put the image resource files on the server through the configuration table, and the URL of the image is also obtained from the server along with the configuration table. When using the request picture and cache to the local, become a real network APP. On this basis, a cache mechanism is designed to periodically clean up the local image cache and reduce the user disk occupation.

Didi componentized architecture

I have seen the technology sharing by Li Xianhui, the head of Didi iOS. What he shared was the development history of Didi iOS client architecture. Here is a brief summary.

The development course

Didi’s structure was chaotic at the beginning. Then, in the 2.0 era, it was refactored into the MVC architecture to make the project division clearer. In 3.0, a new line of business was launched, and the state machine mechanism in game development was adopted, which temporarily satisfied the existing business.

However, in the later period, the existing architecture became very bloated with serious code coupling due to the continuous introduction of multiple business lines such as hitch ride, agency driving and bus. Thus, in 2015, we started The program code-named “The One”, which is didi’s componentization program.

Architecture design

Didi’s componentization solution, similar to Mogujie’s solution, splits the project into components, which are integrated and managed through CocoaPods. The project is split into the business part, which includes components such as ride-sharing, carpooling, and buses, and is managed using a PODS, and the technology part. The technical component is divided into basic components such as login sharing, networking, and caching, which are managed using different Pods.

The communication between components is carried out through the ONERouter middleware, which is similar to the MGJRouter and coordinates and invokes the various components. The communication between components is through the OpenURL method to make corresponding calls. ONERouter keeps a map of the Class to the URL, finds the Class and calls it, and registers the Class in the +load method.

Didi uses a hybrid architecture of MVVM+MVCS inside its business components, both of which are derivative versions of MVC. Store in MVCS is responsible for data related logic, such as order status, address management and other data processing. By slimming the Controller down with the VM in MVVM, the Controller ends up with less code.

Didi Home Page Analysis

According to the Didi article, the home page can only have one map instance, which is what many map navigation related apps do. The main controller of Didi home page holds the navigation bar and map. Each home controller of the business line is added to the main controller, and the background of the controller of the business line is set as transparent. The transparent part of the response events are transferred to the map below, and only its own response events are responded to.

The master controller switches the home page of each line of business, and updates map data according to different lines of business after switching pages.

Taobao componentized architecture

This chapter is derived from a technology sharing by Zongxin in Ali Technology salon

Architecture development

Taobao iOS client was a common project of a single project at the beginning, but with the rapid development of business, the existing architecture could not bear more and more business requirements, resulting in serious coupling between codes. In the later stage, the development team continued to reconstruct the project into a component-based architecture. For taobao iOS and Android platforms, the general architecture is similar except for some unique features of one platform or inconvenient implementation of some solutions.

The development course

  1. It started out as a normal single project, developed with a traditional MVC architecture. As the business continues to grow, the project becomes very bloated and coupled.

  2. In 2013, Taobao launched the “All In Wireless” plan, which turned Taobao into a large platform and integrated most businesses of Alibaba into this platform, resulting in a huge explosion of business. Taobao began to implement plug-in architecture, dividing each business module into a sub-project, and integrating the components into the main project in the form of framework two-party library. However, this method does not achieve the real split, but still in a project using Git merge, which can cause merge conflicts, difficult rollback and other problems.

  3. Ushered in the largest reconstruction of Taobao mobile terminal in history, and reconstructed it into a componentized architecture. Treat each module as a component, each component as a separate project, and package the components as a framework. The main project integrates the framework of all components through Podfile to achieve true isolation between businesses, and implements componentized architecture through CocoaPods.

Architecture advantages

Taobao uses Git for source code management, so merge operation should be avoided as far as possible in plug-in architecture, otherwise the cost of collaboration in a large team will be very high. Using CocoaPods for componentized development avoids this problem.

Podfiles make it easy to configure components in CocoaPods, including adding and removing components, and controlling the version of a component. The reason for using CocoaPods is largely to resolve conflicts caused by code management tool Merge code in large projects. And you can easily configure projects by configuring podFiles.

Each component project has two targets, one for compiling the current component and running debugging, and one for packaging the framework. Do the test in the component project first, and then integrate the test into the main project.

Each component is an independent app that can be developed and tested independently, making business components more independent and all components can be developed in parallel. The lower layer provides the lower layer library that can meet the requirements for the upper layer to ensure the normal development of the upper business layer, and encapsulates the lower layer library into a framework and integrates it into the main project.

The advantage of using CocoaPods for component integration is that when you integrate and test your own components, you can use the source code of the current component directly from your own project through your Podfile. You don’t need to submit it to the server repository.

Taobao four-tier architecture

The core idea of Taobao architecture is that everything is a component, and all codes in the project are abstracted into components.

Taobao architecture is mainly divided into four layers. The top layer is component Bundle(business component), followed by container (core layer), middleware Bundle(function encapsulation), and basic library Bundle(bottom library). The container layer is the core of the architecture and is responsible for scheduling and messaging between components.

Bus design

Bus design: URL routing + service + message. Unified communication standards for all components, and services communicate with each other through the bus.

URL bus

The URL bus is used to unify the three terminals. A URL can be used to call iOS, Android and front-end platforms. Product operation and server only need to send a set of urls to call corresponding components.

URL routes can initiate requests and accept return values, similar to MGJRouter. URL routing requests can be parsed and used directly. If not, redirect to H5 page. This completes a compatibility with calls to nonexistent components, allowing the older version in the user’s hand to still display the new component.

Services provide common services that are implemented by the service component and invoked through Protocol.

The message bus

The application distributes events centrally through the message bus, similar to the notification mechanism of iOS. For example, if the client switches from front to back, it can be distributed to the receiving component via the message bus. Because the URLRouter is only used for one-to-one message distribution and scheduling, if the same URL is registered multiple times, it will be overwritten.

Bundle App

On the basis of component-based architecture, Taobao puts forward the concept of Bundle App, which can form a new App through simple configuration of existing components. It solves the problem of service reuse for multiple applications and prevents the repeated development of the same service or function.

Bundle refers to App and container refers to OS. All Bundle apps are integrated into the OS, making the development of each component as simple as App development. In this way, the lightness of returning from giant APP to ordinary app has been achieved, and the development problems of large projects have been completely solved.


conclusion

Take a moment to think

So far the componentized architecture article is finished, the article is really quite long, see here is really hard you 😁. Here is a quick thought. Copy the following string to the wechat input box and send it to a friend. Then click the link below and you can guess the componentization scheme of wechat.

weixin://dl/profile
Copy the code

conclusion

You can come to the comments section of my blog to discuss the technical details mentioned in the article, the problems encountered in your company’s architecture, or your own unique insights, etc. Whether you are an architect or a newcomer to iOS development, you are welcome to discuss it with a technical mindset. In the comments section, your questions can be seen by others, which may give them some inspiration.

My blog address

Demo address: Mogujie and Casatwy componentization scheme, its Github Demo is given, here posted its Github address.

Mushroom Street -MGJRouter Casatwy -CTMediator

After reading this article, many friends asked if there was a Demo. In fact, architecture is ideological, the key is to understand the idea of architecture. The overview of the ideas in this article is already comprehensive, with multiple project examples to describe the componentized architecture. Even if a Demo is provided, it can’t be applied to other projects because it’s not necessarily appropriate for the project.

After some thought, I wrote a simple Demo of the integration method of component-based architecture, which can solve many people’s problems in the integration of architecture. I put the Demo on Github, used Coding server to simulate the private server of our company, and directly took MGJRouter as the Router in the Demo project. Below is the Demo address, please remember to click star😁.

Componentized architecture integration with Demo

Because the layout of the brief book is not very good, so I made a PDF version of “Modular Architecture Ramble” and put it on my Github. The PDF has the table of contents for easy reading. Here is the address.

If you feel good, please help to transfer the PDF to other groups, or your friends, so that more people understand the component architecture, heartfelt thanks! 😁

Making address: https://github.com/DeveloperErenLiu/ComponentArchitectureBook