A preliminary study on componentization

Why componentization

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

What items do not need to be componentized

  • The project is small, the interaction between modules is simple, less coupling
  • A module is not referenced by multiple external modules and is simply a small module
  • Modules do not need to be reused, and code is rarely modified
  • Small team size

Componentized layered design

The underlying components typically include the underlying class libraries, classifications, macro definition files, and so on

Note: Componentized layering is really about interface isolation to achieve dependencies between layers, and only upper-level dependencies on lower-level dependencies project common code resources sink, and horizontal dependencies sink best

Cocoapods creates private libraries

The process for creating a private library is as follows:

Write a Demo to illustrate the process

1. On the terminal, go to the path where you want to create the pod library project and create the pod library. The command is as follows:

plz@plzdeMacBook-Pro:~ better$ cd desktop
plz@plzdeMacBook-Pro:desktop better$ pod lib create PrivateHelloWorld
Copy the code

When you press Enter, the terminal will ask you a few questions

What language do you want to use?? [Swift/ObjC] > ObjC # create a demo project in your project. I chose Yes Would you like to include a demo application with your library? [Yes/No] > Yes # Which testing frameworks will you use? [Specta/Kiwi/None] > None # [Yes/No] > No # What is your class prefix? > HGCopy the code

The Pod private library is successfully created. Normally the project will be opened automatically when it is successfully created

2. Install the CocoaPods project. Go to the PrivateHelloWorld folder you just created and click in

3, Add you want to add the code file (copy and paste)

Pod Install PrintHelloWorld

# to Example directory BetterdeMacBook - Pro: desktop better $CD/Users/better/desktop/PrivateHelloWorld/Example # CocoaPods project installation BetterdeMacBook-Pro:Example better$ pod install --no-repo-updateCopy the code

Go to the Example folder and open the project by clicking on the file with the suffix xcworkspace.

Edit the CocoaPods configuration file (podSpec)

You can see that in the PrintHelloWorld folder

This file can be edited in a variety of ways, such as Xcode, Text editor, Sublime Text series, and Atom. I opened it with a Text editor, which is ugly but convenient

From here on, you can write any website at all. You are advised to write on the homepage of your project, but be sure to change it, otherwise you will get an error by default because there is no default url

S.ource needs to fill a private git address library, github charges, I choose gitLab

Copy the private library address and replace the s.ource address in the. Podspec file

The configuration is complete

Moving to our Example file again, pod updates

BetterdeMacBook-Pro:Example better$ pod update --no-repo-update
Copy the code

Open the project and see if it succeeded

5. Add PrintHelloWorld, run the test to open the project, and import printhelloWorld.h in byViewController.m

In order to ensure the correctness of the project, the POD file configuration is ok, we need to verify before submitting

Use terminal to move to our project path

BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/PrivateHelloWorld 
Copy the code

At this point, we are done importing the source code, verifying that the project works, and verifying the POD configuration file locally

7. When the project is released, the tag 0.0.1 terminal is moved to the project file to execute git related commands

Betterdemacbook-pro :PrivateHelloWorld better$git remote add Origin betterdemacbook-pro :PrivateHelloWorld better$git remote add Origin https://gitee.com/Better_Y/PrintHelloWorld.git # add file betterdemacbook-pro :PrivateHelloWorld better$git add. And write the description Betterdemacbook-Pro :PrivateHelloWorld Better $git commit -a -m "First submitted version is 0.0.1" # -- allow-suggested -histories # git The pull origin MAste will fail. refusing to merge Merge master # The reason is that the branch master on the remote warehouse Origin and the local branch Master are considered different repositories by Git and so can't be merged directly, Need to add -- Allow-Suggested - Histories BetterdemacBook-Pro :PrivateHelloWorld Better $git Pull Origin Master -- Allow-Suggested - Histories # Push To betterdemacBookpro :PrivateHelloWorld Better $git Push on the Master branch of the Code Cloud's PrintHelloWolrd project Betterdemacbook-pro :PrivateHelloWorld better$git tag 0.0.1 # Push to remote branch Betterdemacbook-pro :PrivateHelloWorld Better $git Push Origin 0.0.1Copy the code

The steps for creating a Git private library are the same as those for the code cloud

Execute the Specs creation commands at the terminal

BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo add PrintSpecs https://gitlab.com/peiliuzhen/testspecs.git
Copy the code

Now, we can publish directly

# PrintSpecs is just add above management library name # PrivateHelloWorld podspec PrintHelloWorld project filename suffix for podspec inside BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo push PrintSpecs PrivateHelloWorld.podspecCopy the code

After the successful release, we can go to the code cloud to see if PrivateSpecs’ Git project has been successfully submitted

At this point, our private library release is complete

9. Verify private library releases

Create a new privateDemo project, create a Podfile file, and install Podfile as follows

Platform :ios,'8.0' target 'privateDemo' do pod 'PrintHelloWorld',:git => 'https://gitlab.com/peiliuzhen/privatehelloworld.git' endCopy the code

Open terminal and execute POD installation instruction:

BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/privateDemo 
BetterdeMacBook-Pro:privateDemo better$ pod install --no-repo-update
Copy the code

Let’s open the PrivateDemo project directory and take a look

At this point, verify our private library release is complete!!

Componentize basic operations

Extract, release and pack

Using a common list page example:

Cell encapsulation extraction:

Componentized module processing

Module coupling

The module of decoupling

Component communication

Module to module communication

Open source componentized framework recommendation (MJRouter, CTMediator, BeeHive)

There are three main componentization methods at present

  • URL routing

  • target-action

  • Protocol match

Here are the representative frameworks corresponding to the three approaches

MJRouter (URL Router)

Common iOS routing tools generally use URL matching, or use runtime method to dynamically call according to the convention.

The advantage of this approach is that it is easy to implement, but the disadvantage is that the string table needs to be maintained, or the problem cannot be exposed at compile time according to the dependent naming convention, and the error is discovered at run time.

The typical framework of URL routing mode is The Open source MGJRouter of Mogujie, as well as JLRoutes and HHRouter

MGJRouter implementation analysis:

  1. App startup instantiates each component module or registers it with class, and the component reports toModuleManagerregisteredUrl
  2. When component A needs to call component B, theModuleManagerPass the URL, parameters can be concatenated after the URL or passed in a dictionary, similaropenURL. And then byModuleManagerResponsible for scheduling component B and finally completing the target

The registration and call codes are as follows:

[MGJRouter registerURLPattern:@" MGJ ://foo/bar" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]); }]; [MGJRouter openURL:@" MGJ ://foo/bar"]; [MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]); // @{@"user_id": @1900} }]; [MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];Copy the code

Advantages of URL routing

  • It is highly dynamic and suitable for apps that often carry out operation activities. For example, Mogujie itself belongs to the e-commerce industry

  • This helps manage routing rules on multiple platforms in a unified manner

  • Easy to adapt to URL schemes

Disadvantages of URl routing

  • There are limited ways to pass arguments, and no compiler can do parameter type checking, so all arguments are converted by string

  • Applies only to interface modules, not to generic modules

  • The format of the parameter is not clear, it is a flexible dictionary, and there needs to be a place to look up the format of the parameter.

  • Do not support the storyboard

  • Rely on string hard coding, difficult to manage, mushroom street to do a background management.

  • There is no guarantee that the module being used exists

  • The decoupling capability is limited, and the “registration”, “implementation” and “use” of URL must use the same character rules. Once any party makes modification, the code of other parties will become invalid, and it is difficult to reconstruct

CTMediator (target – the action)

The target-action solution is based on the OC runtime and category feature to get the module dynamically. For example, the class is obtained from string and the instance is created by NSClassFromString. The method is dynamically called by performSelector + NSInvocation. The typical framework is CTMediator

Implementation analysis:

  1. Add a new interface to the route using the classification, and get the corresponding class through a string in the interface

  2. The instance is created through Runtime and its methods are invoked dynamically

The code for page skipping is as follows:

#import "CTMediator+A.h" @implementation CTMediator (A) - (UIViewController *)A_Category_Swift_ViewControllerWithCallback:(void (^)(NSString *))callback { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"callback"] = callback; params[kCTMediatorParamsKeySwiftTargetModuleName] = @"A_swift"; return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO]; } - (UIViewController *)A_Category_Objc_ViewControllerWithCallback:(void (^)(NSString *))callback { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"callback"] = callback; return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO]; } @end //******* #import "target_a.h" #import "aviewController.h" @implementation Target_A - (UIViewController *)Action_Category_ViewController:(NSDictionary *)params { typedef void (^CallbackType)(NSString *); CallbackType callback = params[@"callback"]; if (callback) { callback(@"success"); } AViewController *viewController = [[AViewController alloc] init]; return viewController; } - (UIViewController *)Action_Extension_ViewController:(NSDictionary *)params { typedef void (^CallbackType)(NSString *); CallbackType callback = params[@"callback"]; if (callback) { callback(@"success"); } AViewController *viewController = [[AViewController alloc] init]; return viewController; } //******* 3, use // objective-c -> Category -> objective-c UIViewController *viewController = [[CTMediator sharedInstance] A_Category_Objc_ViewControllerWithCallback:^(NSString *result) { NSLog(@"%@", result); }]; [self.navigationController pushViewController:viewController animated:YES];Copy the code

advantages

  • Classification allows you to explicitly declare interfaces for compilation checks

  • The implementation is lightweight

disadvantages

  • It is necessary to add every interface in Mediator and target, and the code is cumbersome when modularizing

  • It still hardcodes strings in categories and internally uses dictionaries to pass parameters

  • There is no guarantee that the module being used exists, and after target is modified, the user can only detect the error at run time

  • Too many Target classes may be created

CTMediator core source code analysis

PerformTarget to CTMediator calling through the classification of the specific implementation, namely performTarget: action: params: shouldCacheTarget:, mainly by passing in the name, Find the corresponding target and action

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { if (targetName == nil || actionName == nil) { return nil; } // When used in swift, you need to pass in the target name of the corresponding project, Otherwise you will find the view controller nsstrings * swiftModuleName = params [kCTMediatorParamsKeySwiftTargetModuleName]; // generate target NSString *targetClassString = nil; If (swiftModuleName. Length > 0) {// Swift stringWithFormat:@"% @.target_ %@", swiftModuleName, targetName]; } else {//OC in target filename join targetClassString = [NSString stringWithFormat:@"Target_%@", targetName]; } / / the cache lookup target NSObject * target = [self safeFetchCachedTarget: targetClassString]; Target if (target == nil) {Class targetClass = NSClassFromString(targetClassString); Target = [[targetClass alloc] init]; NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; Sel action = NSSelectorFromString(actionString); If (target == nil) {// If (target == nil) {// If (target == nil) {// If (target == nil) {// If (target == nil) { In practice, you can give a fixed target in advance to be used at this time, Then process this request of [self NoTargetActionResponseWithTargetString: targetClassString selectorString: actionString originParams:params]; return nil; } / / whether to cache the if (shouldCacheTarget) {[self safeSetCachedTarget: target key: targetClassString]; } / / whether the response sel the if ([target respondsToSelector: action]) {/ / dynamic invocation methods return [self safePerformAction: the action target: the target params:params]; } else {// This is where the unresponsive request is handled. If there is no response, try calling the notFound method corresponding to target to handle SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else {// This is also where unresponsive requests are handled. If notFound is not present, this demo will return directly. In practice, you can use the fixed target above mentioned. [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; @synchronized (self) { [self.cachedTarget removeObjectForKey:targetClassString]; } return nil; }}}Copy the code

Enter safePerformAction: target: params: implementation, mainly through the invocation parameters + message forwarding

- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {// get method signature NSMethodSignature* methodSig = [target methodSignatureForSelector:action]; if(methodSig == nil) { return nil; Const char* retType = [methodSig methodReturnType]; //void type if (STRCMP (retType, @encode(void)) == 0) {... } / /... Other types can read CTMediator source code}Copy the code

BeeHive (Protocol Class)

Protocol matching is implemented as follows:

  • 1. Dictionary match protocol with the corresponding class

  • 2. Create an instance dynamically by using protocol to get the class

A typical tripartite framework for Protocol is Alibaba’s BeeHive. BeeHive borrowed from the Spring Service, Apache DSO architecture concept, using AOP+ extension App life cycle API form, the business functions, basic functional modules in modular way to solve complex problems in large applications, and between modules in the form of Service call, complex problems will be segmtioned, Modularize services in an AOP manner.

BeeHive source code has not been further explored, want to know can refer to the link: BeeHive – an iOS module decoupling practice

Attached references:

Building a complete iOS componentization solution: How to decouple modules for interfaces