3

1. Background

As one of the mainstream ways of mobile application architecture, business componentization (or modularization) has been the direction of active exploration and practice in the industry in recent years. The Youzan mobile team has been trying various componentized solutions since 16 years. It has been implemented in many applications such as Youzan WeChat Mall, Youzan Retail, and Youzan Industry. We stepped on some pits and gained a lot of valuable experience, and precipitated the iOS-related framework Bifrost (Rainbow Bridge in Thor). In the process, we deeply realized the meaning of "there is no absolutely correct structure, only the most suitable structure".

There are many iOS componentization/modularization solutions. We only provide an implementation idea to inspire students who encounter similar problems. We are not prepared to give a standard answer to the componentized architecture design solution. Different from functional modules/components (such as image libraries, network libraries), this article discusses the architecture design related to business modules/components (such as order modules, commodity modules).

2. Business modularization/componentization

The traditional App architecture design emphasizes more layering. Based on the single responsibility principle, one of the six principles of design patterns, the system is divided into a basic layer, a network layer, a UI layer, etc., to facilitate maintenance and expansion. But with the development of business, the system becomes more and more complex, and it is not enough to just do layering. The coupling between the various subsystems in the App is severe, and the boundaries are becoming more and more blurred. It often happens that you are in me and you are in me (Figure 1). This has a great impact on code quality, function expansion, and development efficiency. At this time, each subsystem is generally divided into relatively independent modules, the interaction code is converged through the intermediary mode, and the interaction between the modules is encapsulated, and all inter-module calls are made through the intermediary (Figure 2). At this time, the architecture logic will be much clearer, but because the intermediary still needs to rely on the business module in reverse, this does not fundamentally eliminate the problem of cyclical dependence. From time to time, one module is changed, and multiple modules are affected and cannot be compiled. Furthermore, through technical means, the intermediary's dependence on business modules is eliminated, and a business modular architecture design is formed (Figure 3).

在这里插入图片描述
In general, through business modular architecture, it is generally possible to clarify module responsibilities and boundaries, improve code quality, reduce complex dependencies, optimize compilation speed, and improve development efficiency.

Three, common modular schemes

The business modular design avoids circular two-way dependence by decoupling and transforming each business module to achieve the purpose of improving development efficiency and quality. However, the dependence of business requirements cannot be eliminated, so the first thing that a modular solution must solve is how to achieve cross-module communication without code dependence.

iOS can do this easily because of its powerful runtime features, whether it is based on NSInvocation or peformSelector method. But we cannot decoupling for decoupling. It is our goal to improve quality and efficiency. The code directly based on the hardcode string + reflection will obviously greatly damage the development quality and efficiency, and run counter to the goal. Therefore, a more accurate description of modular decoupling requirements should be "how to achieve cross-module communication without code dependency while ensuring development quality and efficiency."

At present, the common communication schemes between modules in the industry are roughly as follows:

  • Unified jump management of UI pages based on routing URLs.
  • The remote interface call package based on reflection.
  • Service registration scheme based on protocol-oriented thinking.
  • Notification-based broadcasting scheme.

3.1 Route URL unified hop scheme

Unified jump routing is the most common way of page decoupling, which is widely used in front-end pages. By binding a URL to a page, the corresponding page can be easily opened through the URL when needed, as shown below.

//kRouteGoodsList = @"//goods/goods_list"
UIViewController *vc = [Router handleURL:kRouteGoodsList]; 
if(vc) {
    [self.navigationController pushViewController:vc animated:YES];
}

Of course, some scenarios will be more complicated than this, for example, some pages require more parameters. However, the basic types of parameters are naturally supported by the URL protocol.

//kRouteGoodsDetails = @“//goods/goods_detail?goods_id=%d”
NSString *urlStr = [NSString stringWithFormat:@"kRouteGoodsDetails", 123];
UIViewController *vc = [Router handleURL:urlStr];
if(vc) {
   [self.navigationController pushViewController:vc animated:YES];
}

For complex type parameters, you can provide an additional dictionary parameter complexParams, and then put the complex parameters in the dictionary.

+ (nullable id)handleURL:(nonnull NSString *)urlStr
           complexParams:(nullable NSDictionary*)complexParams
              completion:(nullable RouteCompletion)completion;

The completion parameter in the above method is a callback block, which handles scenarios where a callback function is needed to open a page. For example, open the member selection page, search for members, click OK after searching, and return member data.

//kRouteMemberSearch = @“//member/member_search”
UIViewController *vc = [Router handleURL:urlStr complexParams:nil completion:^(id  _Nullable result) {
    //code to handle the result
    ...
}];
if(vc) {
    [self.navigationController pushViewController:vc animated:YES];
}

Considering the actual situation, it is necessary to bind the URL of the page that provides the routing service to a block. Put the required initialization code in the block, and then bind the initialization block to the routing URL where appropriate, such as in the +load method.

+ (void)load {
    [Router bindURL:kRouteGoodsList
           toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
        return [[GoodsListViewController alloc] init];
    }];
}

URL itself is a universal protocol across multiple terminals. The advantage of using the routing URL unified jump solution is dynamic and multi-terminal unification (H5, iOS, Android, Weex/RN); the disadvantage is that the interactive scenes that can be processed are relatively simple. So it is generally more suitable for simple UI page jump. Some complex operations and data transmission, although they can also be implemented in this way, are not very efficient.

3.2 Remote call package based on reflection

In normal development, when it is not possible to directly import the header file of a certain class but still need to call its method, the most commonly used method is reflection. Reflection is the basic feature of object-oriented languages, and both Java and oC have this feature.

Class manager = NSClassFromString(@"YZGoodsManager");
NSArray *list = [manager performSelector:@selector(getGoodsList)];
//code to handle the list
...

But there are a lot of hardcode strings in this way. The code cannot be automatically completed, and spelling errors are prone to occur. Moreover, such errors can only work normally after triggering related methods at runtime. Both development efficiency and development quality have a greater impact.

How to optimize it? This is actually a problem that needs to be solved for remote calls on all sides. The most common remote call on the mobile terminal is to send a network request to the back-end interface. For such problems, it is easy for us to think of creating a network layer to encapsulate this type of "dangerous code". When the upper-layer business calls the network layer interface, there is no need for hardcode strings and no need to understand internal troublesome logic.

Similarly, I can encapsulate the communication between modules into a "network layer" (or message forwarding layer). In this way, the dangerous code only exists in a few files, so code review and joint debugging can be performed specially. In the later stage, unit testing can also be used to ensure quality. In the modular scheme, we can call this type of "forwarding layer" Mediator (of course you can also name it individually). At the same time, because the performSelector method has a limited number of attached parameters and no return value, it is more suitable to use NSInvocation to achieve.

//Mediator提供基于NSInvocation的远程接口调用方法的统一封装
- (id)performTarget:(NSString *)targetName
             action:(NSString *)actionName
             params:(NSDictionary *)params;

//Goods模块所有对外提供的方法封装在一个Category中
@interface Mediator(Goods)
- (NSArray*)goods_getGoodsList;
- (NSInteger)goods_getGoodsCount;
...
@end
@impletation Mediator(Goods)
- (NSArray*)goods_getGoodsList {
    return [self performTarget:@“GoodsModule” action:@"getGoodsList" params:nil];
}
- (NSInteger)goods_getGoodsCount {
    return [self performTarget:@“GoodsModule” action:@"getGoodsCount" params:nil];
}
...
@end

Then, if each business module relies on Mediator, these methods can be called directly.

//业务方依赖Mediator模块,可以直接调用相关方法
...
NSArray *list = [[Mediator sharedInstance] goods_getGoodsList];
...

The advantage of this scheme is that it is simple and convenient to call, and both automatic code completion and compile-time checking are still effective. The disadvantage is that the category has the risk of duplicate name coverage, which needs to be avoided through development specifications and some checking mechanisms. At the same time, Mediator only converges HardCode, but does not eliminate HardCode, which still has a certain impact on development efficiency. The industry-renowned CTMediator componentized solution and Meituan are all implemented using similar solutions.

3.3 Service Registration Scheme

Is there a way to absolutely avoid hardcode? If you have been exposed to the service transformation of the back-end, you will find that it is very similar to the business modularization of the mobile terminal. Dubbo is one of the classic frameworks of servicing. It implements remote interface calls through service registration. That is, each module provides its own protocol statement for external services, and then registers this statement to the middle layer. The caller can see which service interfaces exist from the middle layer, and then directly make the call.

//Goods模块提供的所有对外服务都放在GoodsModuleService中
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
//Goods模块提供实现GoodsModuleService的对象, 
//并在+load方法中注册
@interface GoodsModule : NSObject<GoodsModuleService>
@end
@implementation GoodsModule
+ (void)load {
    //注册服务
    [ServiceManager registerService:@protocol(service_protocol) 
                  withModule:self.class]
}
//提供具体实现
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
@end

//将GoodsModuleService放在某个公共模块中,对所有业务模块可见
//业务模块可以直接调用相关接口
...
id<GoodsModuleService> module = [ServiceManager objByService:@protocol(GoodsModuleService)];
NSArray *list = [module getGoodsList];
...

It can be seen that this scheme is relatively simple to implement, and all the implementation of the protocol is still in the module, so there is no need to write reflection code. At the same time, only the agreement is exposed, which is in line with the "protocol-oriented programming" idea of teamwork. The disadvantage is that if the service provider and the user rely on the same protocol in the common module, when the content of the agreement changes, there is a risk that all service dependent modules will fail to compile. At the same time, a registration process is required to bind the Protocol to the specific implementation.

3.4 Notification broadcast scheme

The notification-based communication scheme between modules is very simple to implement, and it can be directly based on the system's NSNotificationCenter.
The advantage is that it is simple to implement and very suitable for handling one-to-many communication scenarios.
The disadvantage is that it is only suitable for simple communication scenarios. Complicated data transmission, synchronous call and other methods are not very convenient.
Among the modular communication schemes, the notification scheme is more often used as a supplement to the above several schemes.

3.5 Other

In addition to the realization of communication between modules, the business modular architecture also needs to consider the internal design of each module, such as its life cycle control, complex object transmission, and the processing of repeated resources. Perhaps because each company has its own actual scenarios, the industry solutions do not describe many of these issues. But in fact, they are very important. Youzan has done a lot of relevant thinking and attempts in the process of modularization, which will be introduced in the following links.

Fourth, modular practice

4.1 Explore and try

In 16 years, apps such as Youzan WeChat Mall and Youzan Cashier have experienced rapid iteration of initial functions, with chaotic internal dependencies and severe coupling, which urgently need to be optimized and reconstructed. Traditional optimization methods such as MVVM and MVP cannot solve these problems at a global level. Later, I listened to Mogujie's componentized solution sharing in InfoQ's "Mobile Development Frontline" WeChat group, and I was very inspired. However, there were still some concerns at the time. For example, WeChat mall and cashier were both small and medium-sized projects, and there were only 4-6 developers at each end. After the business modular transformation, a certain development threshold will be formed, which will bring a certain decline in development efficiency. Are small projects suitable for modular transformation? Can the income match the payment? However, considering that the boundaries of App modules were already stable at that time, even if there were problems with the modular transformation, it could be downgraded to the traditional intermediary model at a small cost, so the transformation began.

4.1.1 Design of communication mode between modules

First, we sort out our communication requirements between modules, which mainly include the following three scenarios:

  • UI page jump : For example, the IM module clicks on the user avatar to open the user details page of the membership module.
  • action execution and complex data transmission : For example, the commodity module transmits the commodity data model to the billing module and performs price calculation.
  • -to-many notification broadcast : For example, the account module sends out a broadcast during logout, and each service module performs cache cleaning and other corresponding operations.

After analyzing the specific business, we finally chose a combination of routing URL + remote interface call encapsulation + broadcasting. For the encapsulation method of remote interface calls, we did not completely copy the Mediator solution. At that time, it was very desirable to retain the modular compilation isolation properties. For example, when a certain interface provided by module A changes, it will not cause compilation errors of modules that depend on this interface. This can avoid the dependent module being forced to interrupt the work at hand to solve the compilation problem first. At that time, Beehive's service registration method was not adopted, for the same reason. After discussion, I chose to refer to the network layer encapsulation method and design an external "network layer" ModuleService in each module. Put the reflection calls to the interfaces of other modules into the ModuleService of each module.

At the same time, we hope that each business module does not need to understand the internal complex implementation of the dependent modules. For example, module A depends on the interface method1 of class D1 of module D, the interface method2 of class D2, and the interface method3 of class D3. A needs to know the internal information of the D module to complete the reflection function. If these names are changed in the D module, the call will fail. So we refactored each module using Facade mode. The D module creates a facade layer FacadeD. It provides all services to the outside through the FacadeD object, while hiding the internal complex implementation. The caller also only needs to understand which interfaces are included in the FacadeD header file.

Facade mode : Provides a consistent interface for a group of interfaces in the subsystem. Facade mode defines a high-level interface, which makes this subsystem easier to use. After the appearance role is introduced, the user only needs to directly interact with the appearance role, and the complex relationship between the user and the subsystem is realized by the appearance role, thereby reducing the coupling degree of the system.

在这里插入图片描述

In addition, why do you need to route URLs?
In fact, from a functional point of view, the network layer of the remote interface can completely replace the routing URL to achieve page redirection, and there is no hardcode problem of the routing URL. And routing URL and
There is a certain overlap of functions in the remote interface, which will also cause confusion about whether to choose the routing URL or the remote interface when implementing new functions in the future. The main reason for choosing to support routing URLs here is that we have dynamic and multi-end unified needs. For example, the various message data models issued by the message module are completely dynamic. After the back-end is equipped with display content and jump requirements, the client does not need to understand the specific needs, but only needs to perform the jump action through the unified routing and jump protocol.

4.1.2 In-module design and App structure adjustment

In addition to the transformation of Facade mode, the following issues need to be considered for each module:

  • Appropriate registration and initialization methods.
  • Receive and process global events.
  • App layer and Common layer design.
  • Module compilation output and the way to integrate it into the App.

Considering that there will not be many business modules in each App, we created a Module object for each module and made it a singleton. Register itself to the modular SDK Bifrost in the +load() method. After testing, the memory usage caused by the singleton and the startup speed caused by the +load method are minimal. The global events that the module needs to listen to are mainly those methods in UIApplicationDelegate. So we defined a protocol BifrostModuleProtocol that inherits UIApplicationDelegate, so that the Module object of each module obeys this protocol. App's AppDelegate object will poll all registered business modules and make necessary calls.

@protocol BifrostModuleProtocol <UIApplicationDelegate, NSObject>
@required
+ (instancetype)sharedInstance;
- (void)setup;
...
@optional
+ (BOOL)setupModuleSynchronously;
...
@end

After all the business code is moved into the Module object of each business module, the AppDelegate of the project is very clean.

@implementation YZAppDelegate
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [Bifrost setupAllModules];
    [Bifrost checkAllModulesWithSelector:_cmd arguments:@[Safe(application), Safe(launchOptions)]];
    return YES;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [Bifrost checkAllModulesWithSelector:_cmd arguments:@[Safe(application), Safe(launchOptions)]];
    return YES;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
    [Bifrost checkAllModulesWithSelector:_cmd arguments:@[Safe(application)]];
}
...
@end

Each business module is integrated into the App Project as a sub-project. At the same time, a special module Common is created to place some general business and global base classes. The App layer only retains global classes such as AppDelegate and special configurations such as plist, and basically does not have any business code. Since the Common layer has no clear business responsibility, it should be as thin and light as possible. The business modules are invisible to each other, but they can directly rely on the Common module. Set the module dependencies through the search path.

The output of each business module includes two parts: executable files and resource files. There are two options: generate framework and generate static library + resource bundle.

The advantage of using framework is that the output is in the same object, which is convenient for management. The disadvantage is that it is loaded as a dynamic library, which affects the loading speed. So the static library + bundle form was chosen. However, I feel that this area still needs to be measured specifically, and it will be more appropriate to do it slowly and make fewer decisions. But because the two are not much different, we haven't made any adjustments in the follow-up.

In addition, if you use framework, you need to pay attention to the problem of resource reading. Because the traditional resource reading method cannot locate the resources in the framework, you need to pass bundleForClass:.

//传统方式只能定位到指定bundle,比如main bundle中资源
NSURL *path = [[NSBundle mainBundle] URLForResource:@"file_name" withExtension:@"txt"]; 

// framework bundle需要通过bundleForClass获取
NSBundle *bundle = [NSBundle bundleForClass:classA]; //classA为framework中的某各类
// 读UIStoryboard
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@“sb_name” bundle:bundle];
// 读UIImage
UIImage *image = [UIImage imageNamed:@"icon_name" inBundle:bundle compatibleWithTraitCollection:nil];
...

4.1.3 Complex object transmission

The most tangled point at the time was the transmission of complex objects. For example, the product model, which contains dozens of fields. If it is to transfer a dictionary or json, both the data provider (commodity module) and the user (billing module) need to specifically understand and implement the various fields of this model, which has a great impact on development efficiency.
Is there a way to pass the model object directly? Here involves where the class files of the model are placed. The easiest solution to think of is to sink into the Common module. But once this hole is released, more and more models will be put into Common in the future, which is contrary to the goal of simplifying the Common layer mentioned earlier. And because the Common module does not have a clear business group affiliation, all groups can edit, and its quality and stability are difficult to guarantee. In the end, we adopted a tricky solution, copying the code of the complex model to be transferred in the user module, and at the same time by modifying the class name prefix to distinguish, so as to avoid link conflict errors during packaging.

For example, the commodity module is called YZGGoodsModel, and the billing module is called YZSGoodsModel. The interface of the commodity module returns YZGGoodsModel, and the billing module can force it to YZSGoodsModel.

//YZSaleModuleService.m内
#import "YZSGoodsModel.h"

- (YZSGoodsModel*)goodsById:(NSString*)goodsId {
    //Sale Module远程调用Goods Module的接口
    id obj = [Bifrost performTarget:@"YZGoodsModule"
                           action:@"goodsById:"
                             params:@[goodsId]];
    //做一次强转
    YZSGoodsModel *goods = (YZSGoodsModel*)obj;
    return goods;
}

Although this method is relatively crude, considering that there should not be many complex objects that interact between the two modules, and the cost of copy and paste operations is controllable, so it is acceptable. At the same time, this method can also achieve the expected effect of compilation isolation. However, there is still a risk of inconsistency in the definition and implementation of the two models. In order to solve the consistency problem, we made a check script tool, which is triggered at compile time. According to the naming rules, it will find the code of this type of "same name" model and make a comparison. If inconsistencies are found, report warning. Note that it is not an error, because we want one module to modify the interface, and there is a choice for another module, whether to update the interface immediately, or complete the work at hand before updating in the future.

4.1.4 Duplicate resource handling

Class resources mainly include pictures, audio and video, data models, and so on. First of all, we ruled out the scheme of putting in Common without a brain. Because sinking into Common will destroy the integrity of each business module, and also affect the quality of Common. After discussion, it was decided to divide the resources into three categories:

  • The resources used by common functions are organized into functional components and put into Common together.
  • Most of the resources of business functions can be controlled in volume through lossless compression, and small resources allow a certain degree of duplication.
  • Larger resources are placed on the server side, and the App side is dynamically pulled and placed in the local cache.

At the same time, automatic tools are used to detect useless resources and the size of duplicate resources before the project is packaged, so as to optimize the package size in time.

4.1.5 Achievements display

Based on the above design, we spent about 3 months to carry out business modular transformation (renovation while doing business) on existing projects. Because there were a lot of considerations in the details of the plan, and everyone had expectations for some possible problems, everyone held a positive attitude after the transformation at that time, but the final result was still impressive.

After the 1.0 version transformation, the overall structure of the App is shown in the figure below.

在这里插入图片描述
The overall project structure is shown in the figure below.
在这里插入图片描述

4.2 Optimization

Although the modular design scheme introduced above is feasible, there are still two serious problems:

  • The encapsulation of the network layer between modules is based on reflection code, which is still a little troublesome to write. And need to write an additional test to ensure quality.
  • The handling of complex objects brings additional problems. For example, the copy and paste method is ugly, and repeated code will increase the size of the package.

In response to the above problems, we set out to optimize from the following aspects.

4.2.1 Encapsulation optimization of remote interface

First of all, how to avoid reflection and hardcode. Ali Beehive's service registration-based method does not require hardcode code. However, it has an additional service registration process, which may affect the startup speed, and its performance is weaker than the reflection-based interface encapsulation scheme.

How much does the use of service registration affect the startup speed? We did a test and registered 1000 Service Protocols in the +load method. The startup time impact is about 2-4 ms, so the impact is very small.

在这里插入图片描述
Because each of our modules is designed based on the appearance model. Therefore, each module only needs to expose one Service Protocol to the outside. The actual number of modules of our App is about 20, so the impact on the startup speed is negligible. And as mentioned above, each module originally needs to register its own appearance class (Module object) to handle the life cycle and receive AppDelegate messages. Here the implementer of Service Protocl is this Module object, so there is no additional performance consumption.

4.2.2 Complex object transmission optimization

There is another reason why Beehive was not used in the previous business modularization solution, which is that the service provider and the user rely on the same Protocol, which does not meet our requirements for compilation and isolation. But since we can copy and paste complex object code, can we also copy and paste Protocol declarations?

The answer is feasible. And even if there are multiple Protocols with the same name in the project at the same time, it will not cause compilation problems, and even the step of renaming is omitted. Take the commodity model as an example, define a GoodModelProtocol for it, and the service user's billing module can directly copy the protocol statement into its own module without changing the name, and the operation cost is very low. Then the protocol can be used in the commodity module. At the same time, because the same protocol object is used, the risk of type forced conversion in v1.0 is gone.

NSString *goodsID = @"123123123";
id<YZGoodsModelProtocol> goods = [BFModule(YZGoodsModuleService) goodsById:goodsID];
self.goodsCell.name = goods.name;
self.goodsCell.price = goods.price;
...

In addition, in order to minimize the frequency of copying and pasting, we put the interface services, routing definitions, notification definitions, and complex object protocol definitions provided by each module in ModuleService.h. Management is very convenient and standardized, and other modules are easy to copy. You only need to copy this ModuleService.h file into your own module, and you can directly rely on and call the interface. And if you need to pull related configurations from the server in the future, a file will be much more convenient. But you also need to consider if the above content is put into the same header file, will it cause the file to be too large. At that time, the interaction between analysis modules was limited, otherwise it was necessary to consider whether the module division was appropriate. So the problem should not be big. From the results, currently our largest ModuleService.h, plus comments, is about 300 lines.

4.2.3 Other optimizations

In addition, we found that each module also has requirements for the initialization sequence. For example, the initialization of the account module may take precedence over other modules, so that other modules can use its services during initialization. So we also added a priority interface to ModuleProtocol. Each module can define its own initialization priority.

/**
 The priority of the module to be setup. 0 is the lowest priority;
 If not provided, the default priority is BifrostModuleDefaultPriority;

 @return the priority
 */
+ (NSUInteger)priority;

After the above optimization and transformation, all the hidden dangers of quality and efficiency of the previous modularization have been basically solved, and the business modularization plan is approaching mature.

Precipitation and perfection

After solving problems such as the transmission of complex objects, the modular scheme has basically matured. However, the architecture is still not very friendly to some newcomers, so we continue to think.

4.3.1 Thinking about compilation isolation

The way to copy header files still has some understanding costs. The scale of the mobile team is growing rapidly, and some newcomers will still ask questions. We did several inspections in 18 years and found that ModuleService version inconsistency between modules occurred from time to time. Although the retail mobile team reached more than 30 people at that time, it was still a closely coordinated whole, and the pace of release was basically the same. The code of each business module is in the same git project, and basically the latest version of each module is used for each release. In fact, I did several investigations and found that the modification of the dependent module caused by the interface change in ModuleService is actually very low cost and quick to change. At this point, we began to think about whether the compilation isolation we pursued before is suitable for the current stage and whether it has practical value.

In the end, we decided to save every effort and maximize efficiency. The ModuleService of each business is sunk to the Commom module, and each business module directly depends on these ModuleServie header files in Common, and the copy operation is no longer required. The price of such a transformation is the formation of more dependence. Originally, a business module could not rely on Common, but now it must be relied on. But considering the actual situation, there is no business module that does not rely on Common. This pursuit has no value, so it should not be a big problem. At the same time, because the sinking is some header files, there is no specific implementation. If further isolation between modules is required in the future, such as separate packaging of modules, you only need to make these Moduleservies server-side configurable + automatic download generation, and the cost of transformation very small.

But after this transformation, another thing happened. A newcomer directly wrote the code in the Common module to call the function of the upper-level business module through these ModuleServices, forming the reverse dependency of the lower-level Common module on the upper-level business module. So we further split out a new module Mediator, put Bifrost SDK and these ModuleSevice into it. The Common module and Mediator are invisible to each other. At this time, the final App architecture is:
在这里插入图片描述

Some solutions in the industry store ModuleServie separately, which is equivalent to splitting the Mediator part of the above solutions, and each business module has one. The advantage of this approach is that the responsibilities are clear. You don't need to modify a common module at the same time. At the same time, the dependency relationship can be very clear. The disadvantage is that the number of modules has doubled and the maintenance cost has increased a lot. Considering our current situation, the Mediator module is a very thin layer, and it is acceptable to modify and maintain this module together, so it is not disassembled at present. In the future, if necessary, it can be split and remodeled. The workload of remodeling is very small.

4.3.2 Thinking about code isolation

In addition to not pursuing compilation isolation at inappropriate stages, we also found that code isolation is not suitable for us.

One of the effects of business modularization is that a business module can be packaged separately and put into a shell project to run. A transformation that is easy to think of is to split each module into a different git. There are many benefits, such as separate permission control, independent version number, and rollback can be packaged with the old version in time if a problem is discovered when the version is released. Our WeChat Mall App has made this attempt. Moved the code to a lot of git and managed it through pod. But the experience in subsequent development is not very good. At that time, the number of modules in the WeChat Mall App was much larger than the number of students who developed them, and each student maintained multiple modules at the same time. Sometimes a project, one person needs to modify the code of multiple modules in multiple gits at the same time. After the modification is completed, operations such as submission, version numbering, and integration testing have to be performed multiple times, which is very inefficient. At the same time, because multiple gits are involved, the Merge Request for code submission and related compilation checks are also a lot more complicated. Similarly, because the development and release rhythms of different modules in the WeChat Mall App are basically the same, the advantages of different version management and rollbacks of multiple git and multiple pods have not been reflected. Finally, the module code was moved back to the main git.

But is compilation isolation and code isolation really worthless? Of course not, mainly because we don't need it at this stage. Premature adjustment has increased costs but has no value output, so it is not appropriate. In fact, we also have some business modules that are used across apps, such as IM modules, asset modules, and so on. They are all independently released by independent git. Compilation isolation and code isolation properties are very effective for them.

In addition, individual git for each module can have more fine-grained authority management. In a git, there have been several examples of problems when a small partner changed other people's modules (although there is MR, it is inevitable that there are omissions). Later, we solved this problem by controlling the modification permissions through git commit hook + modifying the file path. There will be more details in the follow-up article introducing Youzan Mobile's infrastructure construction.

4.3.3 Some suggestions for business modularization

We recommend that all teams entering a stable period of business division (business modules are basically determined and will not undergo major changes) adopt a business modular architecture design. Even if the module division is not completely clear, you can consider modular transformation of some of the clear modules. Because it will be used sooner or later, it is better to use it late than early. The current inter-module communication method based on routing URL + protocol registration basically does not damage the development efficiency.

Five, summary

The business modular architecture design of mobile applications, its real goal is to improve the quality and efficiency of development. From the perspective of implementation alone, there are no black magic or technical difficulties. It is more about combining the actual development and collaboration methods of the team and the specific considerations of the business scenarios-"the best for you is the best". Through 4 years of technical practice, Youzan Mobile’s technical team found that the blind pursuit of performance, the absolute pursuit of isolation between modules, and the premature pursuit of module code management isolation have all deviated from the true purpose of modular design, which is not worth the gain.

A more appropriate way is to consider future optimization methods to a certain extent at a controllable transformation cost, and more consideration of current actual scenarios, to design a modular method that suits you.


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》