在现有工程中实施基于CTMediator的组件化方案

国内业界你们对组件化的讨论从今年年初开始到年尾,不外乎两个方案:URL/protocol注册调度,runtime调度。html

 

我以前批评过URL注册调度是错误的组件化实施方案,在全部的基于URL注册调度的方案中,存在两个广泛问题:git

 

  1. 命名域渗透
  2. 因注册是没必要要的,而带来一样没必要要的注册列表维护成本

 

其它各家的基于URL注册的不一样方案在这两个广泛问题上还有各类各样的其余问题,例如FRDIntent库中的FRDIntent对象其本质是鸡肋对象、原属于响应者的业务被渗透到调用者的业务中、组件化实施方案的过程当中会产生对原有代码的侵入式修改等问题。github

 

另外,我也发现仍是有人在都没有理解清楚的前提下就作出了本身的解读,流毒甚广。我以前写过关于CTMediator比较理论的描述,也有Demo,但唯独没有写实践方面的描述。我原本觉得Demo就足够了,可如今看来仍是要给一篇实践的文章的。swift

 

在更早以前,卓同窗的swift老司机群里也有人提出由于本身并无理解透彻CTMediator方案,因此不敢贸然直接在项目中应用。因此这篇文章的另外一个目的也是但愿可以让你们明白,基于CTMediator的组件化方案实施其实很是简单,并且也是有章法可循的。这篇文章可能会去讨论一些理论的东西,但主要还会是以实践为主。争取作到可以让你们看完文章以后就能够直接在本身的项目中顺利实施组件化。设计模式

 

最后,我但愿这篇文章可以终结业界持续近一年的关于组件化方案的无谓讨论和错误讨论。xcode




准备工做



我在github上开了一个orgnization,里面有一个主工程:MainProject,咱们要针对这个工程来作组件化。组件化实施完毕以后的主工程就是ModulizedMainProject了。抽出来的独立Pod、私有Pod源也都会放在这个orgnization中去。安全

 

在一个项目实施组件化方案以前,咱们须要作一个准备工做,创建本身的私有Pod源和快手工具脚本的配置:工具

 

  1. 先去开一个repo,这个repo就是咱们私有Pod源仓库
  2. pod repo add [私有Pod源仓库名字] [私有Pod源的repo地址]
  3. 创立一个文件夹,例如Project。把咱们的主工程文件夹放到Project下:~/Project/MainProject
  4. 在~/Project下clone快速配置私有源的脚本repo:git clone git@github.com:casatwy/ConfigPrivatePod.git
  5. 将ConfigPrivatePod的template文件夹下Podfile中source 'https://github.com/ModulizationDemo/PrivatePods.git'改为第一步里面你本身的私有Pod源仓库的repo地址
  6. 将ConfigPrivatePod的template文件夹下upload.sh中PrivatePods改为第二步里面你本身的私有Pod源仓库的名字



最后你的文件目录结构应该是这样:组件化

 

Project
├── ConfigPrivatePod
└── MainProject



到此为止,准备工做就作好了。测试




实施组件化方案第一步:建立私有Pod工程和Category工程



MainProject是一个很是简单的应用,一共就三个页面。首页push了AViewController,AViewController里又push了BViewController。咱们能够理解成这个工程由三个业务组成:首页、A业务、B业务。

 

咱们这一次组件化的实施目标就是把A业务组件化出来,首页和B业务都还放在主工程。

 

由于在实际状况中,组件化是须要按部就班地实施的。尤为是一些已经比较成熟的项目,业务会很是多,一时半会儿是不可能彻底组件化的。CTMediator方案在实施过程当中,对主工程业务的影响程度极小,并且是可以支持按部就班地改造方式的。这个我会在文章结尾作总结的时候提到。

 

既然要把A业务抽出来做为组件,那么咱们须要为此作两个私有Pod:A业务Pod(之后简称A Pod)、方便其余人调用A业务的CTMediator category的Pod(之后简称A_Category Pod)。这里多解释一句:A_Category Pod本质上只是一个方便方法,它对A Pod不存在任何依赖。

 

咱们先建立A Pod




  1. 新建Xcode工程,命名为A,放到Projects下
  2. 新建Repo,命名也为A,新建好了以后网页不要关掉

 

此时你的文件目录结构应该是这样:



Project
├── ConfigPrivatePod
├── MainProject
└── A



而后cd到ConfigPrivatePod下,执行./config.sh脚原本配置A这个私有Pod。脚本会问你要一些信息,Project Name就是A,要跟你的A工程的目录名一致。HTTPS RepoSSH Repo网页上都有,Home Page URL就填你A Repo网页的URL就行了。

 

这个脚本是我写来方便配置私有库的脚本,pod lib create也能够用,可是它会直接从github上拉一个完整的模版工程下来,只是国内访问github其实会比较慢,会影响效率。并且这个配置工做其实也不复杂,我就索性本身写了个脚本。

 

这个脚本要求私有Pod的文件目录要跟脚本所在目录平级,也会在XCode工程的代码目录下新建一个跟项目同名的目录。放在这个目录下的代码就会随着Pod的发版而发出去,这个目录之外的代码就不会跟随Pod的版本发布而发布,这样子写用于测试的代码就比较方便。

 

而后咱们在主工程中,把属于A业务的代码拎出来,放到新建好的A工程的A文件夹里去,而后拖放到A工程中。原来主工程里面A业务的代码直接删掉,此时主工程和A工程编译不过都是正常的,咱们会在第二步中解决主工程的编译问题,第三步中解决A工程的编译问题。

 

此时你的主工程应该就没有A业务的代码了,而后你的A工程应该是这样:



A
├── A
|   ├── A
|   │   ├── AViewController.h
|   │   └── AViewController.m
|   ├── AppDelegate.h
|   ├── AppDelegate.m
|   ├── ViewController.h
|   ├── ViewController.m
|   └── main.m
└── A.xcodeproj




咱们再建立A_Category Pod



一样的,咱们再建立A_Category,由于它也是个私有Pod,因此也照样子跑一下config.sh脚本去配置一下就行了。最后你的目录结构应该是这样的:

 

Project
├── A
│   ├── A
│   │   ├── A
│   │   ├── AppDelegate.h
│   │   ├── AppDelegate.m
│   │   ├── Assets.xcassets
│   │   ├── Info.plist
│   │   ├── ViewController.h
│   │   ├── ViewController.m
│   │   └── main.m
│   ├── A.podspec
│   ├── A.xcodeproj
│   ├── FILE_LICENSE
│   ├── Podfile
│   ├── readme.md
│   └── upload.sh
├── A_Category
│   ├── A_Category
│   │   ├── A_Category
│   │   ├── AppDelegate.h
│   │   ├── AppDelegate.m
│   │   ├── Info.plist
│   │   ├── ViewController.h
│   │   ├── ViewController.m
│   │   └── main.m
│   ├── A_Category.podspec
│   ├── A_Category.xcodeproj
│   ├── FILE_LICENSE
│   ├── Podfile
│   ├── readme.md
│   └── upload.sh
├── ConfigPrivatePod
│   ├── config.sh
│   └── templates
└── MainProject
    ├── FILE_LICENSE
    ├── MainProject
    ├── MainProject.xcodeproj
    ├── MainProject.xcworkspace
    ├── Podfile
    ├── Podfile.lock
    ├── Pods
    └── readme.md

 

而后去A_Category下,在Podfile中添加一行pod "CTMediator",在podspec文件的后面添加s.dependency "CTMediator",而后执行pod install --verbose

 

接下来打开A_Category.xcworkspace,把脚本生成的名为A_Category的空目录拖放到Xcode对应的位置下,而后在这里新建基于CTMediator的Category:CTMediator+A。最后你的A_Category工程应该是这样的:



A_Category
├── A_Category
|   ├── A_Category
|   │   ├── CTMediator+A.h
|   │   └── CTMediator+A.m
|   ├── AppDelegate.h
|   ├── AppDelegate.m
|   ├── ViewController.h
|   └── ViewController.m
└── A_Category.xcodeproj

到这里为止,A工程和A_Category工程就准备好了。




实施组件化方案第二步:在主工程中引入A_Category工程,并让主工程编译经过



去主工程的Podfile下添加pod "A_Category", :path => "../A_Category"来本地引用A_Category。

 

而后编译一下,说找不到AViewController的头文件。此时咱们把头文件引用改为#import <A_Category/CTMediator+A.h>

 

而后继续编译,说找不到AViewController这个类型。看一下这里是使用了AViewController的地方,因而咱们在Development Pods下找到CTMediator+A.h,在里面添加一个方法:



- (UIViewController *)A_aViewController;



再去CTMediator+A.m中,补上这个方法的实现,把主工程中调用的语句做为注释放进去,未来写Target-Action要用:



- (UIViewController *)A_aViewController { /*  AViewController *viewController = [[AViewController alloc] init];  */ return [self performTarget:@"A" action:@"viewController" params:nil shouldCacheTarget:NO]; } 



补充说明一下,performTarget:@"A"中给到的@"A"实际上是Target对象的名字。通常来讲,一个业务Pod只须要有一个Target就够了,但一个Target下能够有不少个Action。Action的名字也是能够随意命名的,只要到时候Target对象中可以给到对应的Action就能够了。

 

关于Target-Action咱们会在第三步中去实现,如今不实现Target-Action是不影响主工程编译的。

 

category里面这么写就已经结束了,后面的实施过程当中就不会再改动到它了。

 

而后咱们把主工程调用AViewController的地方改成基于CTMediator Category的实现:



    UIViewController *viewController = [[CTMediator sharedInstance] A_aViewController]; [self.navigationController pushViewController:viewController animated:YES]; 



再编译一下,编译经过。

到此为止主工程就改完了,如今跑主工程点击这个按钮跳不到A页面是正常的,由于咱们尚未在A工程中实现Target-Action。

 

并且此时主工程中关于A业务的改动就所有结束了,后面的组件化实施过程当中,就不会再有针对A业务线对主工程的改动了。




实施组件化方案第三步:添加Target-Action,并让A工程编译经过



此时咱们关掉全部XCode窗口。而后打开两个工程:A_Category工程和A工程。

 

咱们在A工程中建立一个文件夹:Targets,而后看到A_Category里面有performTarget:@"A",因此咱们新建一个对象,叫作Target_A

 

而后又看到对应的Action是viewController,因而在Target_A中新建一个方法:Action_viewController。这个Target对象是这样的:



头文件:
#import <UIKit/UIKit.h> @interface Target_A : NSObject - (UIViewController *)Action_viewController:(NSDictionary *)params; @end 实现文件: #import "Target_A.h" #import "AViewController.h" @implementation Target_A - (UIViewController *)Action_viewController:(NSDictionary *)params { AViewController *viewController = [[AViewController alloc] init]; return viewController; } @end 



这里写实现文件的时候,对照着以前在A_Category里面的注释去写就能够了。

 

由于Target对象处于A的命名域中,因此Target对象中能够随意import A业务线中的任何头文件。

 

另外补充一点,Target对象的Action设计出来也不是仅仅用于返回ViewController实例的,它能够用来执行各类属于业务线自己的任务。例如上传文件,转码等等各类任务其实均可以做为一个Action来给外部调用,Action完成这些任务的时候,业务逻辑是能够写在Action方法里面的。

 

换个角度说就是:Action具有调度业务线提供的任何对象和方法来完成本身的任务的能力。它的本质就是对外业务的一层服务化封装。

 

如今咱们这个Action要完成的任务只是实例化一个ViewController并返回出去而已,根据上面的描述,Action能够完成的任务其实能够更加复杂。

 

而后咱们再继续编译A工程,发现找不到BViewController。因为咱们此次组件化实施的目的仅仅是将A业务线抽出来,BViewController是属于B业务线的,因此咱们不必把B业务也从主工程里面抽出来。但为了可以让A工程编译经过,咱们须要提供一个B_Category来使得A工程能够调度到B,同时也可以编译经过。

 

B_Category的建立步骤跟A_Category是同样的,不外乎就是这几步:新建Xcode工程、网页新建Repo、跑脚本配置Repo、添加Category代码。

 

B_Category添加好后,咱们一样在A工程的Podfile中本地指过去,而后跟在主工程的时候同样。

 

因此B_Category是这样的:



头文件:
#import <CTMediator/CTMediator.h> #import <UIKit/UIKit.h> @interface CTMediator (B) - (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText; @end 实现文件: #import "CTMediator+B.h" @implementation CTMediator (B) - (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText { /*  BViewController *viewController = [[BViewController alloc] initWithContentText:@"hello, world!"];  */ NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"contentText"] = contentText; return [self performTarget:@"B" action:@"viewController" params:params shouldCacheTarget:NO]; } @end 



而后咱们对应地在A工程中修改头文件引用为#import <B_Category/CTMediator+B.h>,而且把调用的代码改成:



    UIViewController *viewController = [[CTMediator sharedInstance] B_viewControllerWithContentText:@"hello, world!"]; [self.navigationController pushViewController:viewController animated:YES]; 



此时再编译一下,编译经过了。注意哦,这里A业务线跟B业务线就已经彻底解耦了,跟主工程就也已经彻底解耦了。




实施组件化方案最后一步:收尾工做、组件发版



此时还有一个收尾工做是咱们给B业务线建立了Category,但没有建立Target-Action。因此咱们要去主工程建立一个B业务线的Target-Action。建立的时候其实彻底不须要动到B业务线的代码,只须要新增Target_B对象便可:

 

Target_B头文件: #import <UIKit/UIKit.h> @interface Target_B : NSObject - (UIViewController *)Action_viewController:(NSDictionary *)params; @end Target_B实现文件: #import "Target_B.h" #import "BViewController.h" @implementation Target_B - (UIViewController *)Action_viewController:(NSDictionary *)params { NSString *contentText = params[@"contentText"]; BViewController *viewController = [[BViewController alloc] initWithContentText:contentText]; return viewController; } @end 

 

这个Target对象在主工程内不存在任何侵入性,未来若是B要独立成一个组件的话,把这个Target对象带上就能够了。

 

收尾工做就到此结束,咱们建立了三个私有Pod:A、A_Category、B_Category。

 

接下来咱们要作的事情就是给这三个私有Pod发版,发版以前去podspec里面确认一下版本号和dependency。

 

Category的dependency是不须要填写对应的业务线的,它应该是只依赖一个CTMediator就能够了。其它业务线的dependency也是不须要依赖业务线的,只须要依赖业务线的Category。例如A业务线只须要依赖B_Category,而不须要依赖B业务线或主工程。

 

发版过程就是几行命令:



git add .
git commit -m "版本号"
git tag 版本号
git push origin master --tags
./upload.sh



命令行cd进入到对应的项目中,而后执行以上命令就能够了。

 

要注意的是,这里的版本号要和podspec文件中的s.version给到的版本号一致。upload.sh是配置私有Pod的脚本生成的,若是你这边没有upload.sh这个文件,说明这个私有Pod你还没用脚本配置过。

 

最后,全部的Pod发完版以后,咱们再把Podfile里原来的本地引用改回正常引用,也就是把:path...那一段从Podfile里面去掉就行了,改动以后记得commit并push。

 

组件化实施就这么三步,到此结束。




总结



hard code

 

这个组件化方案的hard code仅存在于Target对象和Category方法中,影响面极小,并不会泄漏到主工程的业务代码中,也不会泄漏到业务线的业务代码中。

 

并且在实际组件化的实施中,也是依据category去作业务线的组件化的。因此先写category里的target名字,action名字,param参数,到后面在业务线组件中建立Target的时候,照着category里面已经写好的内容直接copy到Target对象中就确定不会出错(仅Target对象,并不会牵扯到业务线自己原有的对象)。

 

若是要消除这一层hard code,那么势必就要引入一个第三方pod,而后target对象所在的业务线和category都要依赖这个pod。为了消除这种影响面极小的hard code,并且只要按照章法来就不会出错。为此引入一个新的依赖,实际上是不划算的。



命名域问题

 

在这个实践中,响应者的命名域并无泄漏到除了响应者之外的任何地方,这就带来一个好处,迁移很是方便。

 

好比咱们的响应者是一个上传组件。这个上传组件若是要替换的话,只须要在它外面包一个Target-Action,就能够直接拿来用了。并且包Target-Action的过程当中,不会产生任何侵入性的影响。

 

例如原来是你本身基于AFNetworking写的上传组件,如今用了七牛SDK上传,那么整个过程你只须要提供一个Target-Action封装一下七牛的上传操做便可。不须要改动七牛SDK的代码,也不须要改动调用方的代码。假若是基于URL注册的调度,作这个事情就很蛋疼。



服务管理问题

 

因为Target对象处于响应者的命名域中,Target对象就能够对外提供除了页面实例之外的各类Action。

 

并且,因为其本质就是针对响应者对外业务逻辑的Action化封装(其实就是服务化封装),这就可以使得一个响应者对外提供了哪些Action(服务)Action(服务)的实现逻辑是什么获得了很是好的管理,可以大大下降未来工程的维护成本。而后Category解决了服务应该怎么调用的问题。

 

但在基于URL注册机制和Protocol共享机制的组件化方案中,因为服务散落在响应者各处,服务管理就显得十分困难。若是仍是执念于这样的方案,你们只要拿上面提到的三个问题,对照着URL注册机制和Protocol共享机制的组件化方案比对一下,就能明白了。

 

另外,若是这种方案把全部的服务归拢到一个对象中来达到方便管理的目的的话,其本质就已经变成了Target-Action模式,Protocol共享机制其实就已经没有存在乎义了。



高内聚

 

基于protocol共享机制的组件化方案致使响应者业务逻辑泄漏到了调用者业务逻辑中,并无作到高内聚

 

若是这部分业务在其余地方也要使用,那么代码就要从新写一遍。虽然它能够提供一个业务高内聚的对象来符合这个protocol,但事实上这就又变成了Target-Action模式,protocol的存在乎义就也没有了。



侵入性问题

 

正如你所见,CTMediator组件化方案的实施很是安全。由于它并不存在任何侵入性的代码修改。

 

对于响应者来讲,什么代码都不用改,只须要包一层Target-Action便可。例如本例中的B业务线做为A业务的响应者时,不须要修改B业务的任何代码。

 

对于调用者来讲,只须要把调用方式换成CTMediator调用便可,其改动也不涉及原有的业务逻辑,因此是十分安全的。

 

另一个非侵入性的特征体如今,基于CTMediator的组件化方案是能够按部就班地实施的。这个方案的实施并不要求全部业务线都要被独立出来成为组件,实施过程也并不会修改未组件化的业务的代码。

 

在独立A业务线的过程当中若是涉及其它业务线(B业务线)的调用,就只须要给到Target对象便可,Target对象自己并不会对未组件化的业务线(B业务线)产生任何的修改。并且未来若是对应业务线须要被独立出去的时候,也仅须要把Target对象一块儿复制过去就能够了。

 

但在基于URL注册和protocol共享的组件化方案中,都必需要在未组件化的业务线中写入注册代码和protocol声明,并分配对应的URL和protocol到具体的业务对象上。这些其实都是没必要要的,无故多出了额外维护成本。



注册问题

 

CTMediator没有任何注册逻辑的代码,避免了注册文件的维护和管理。Category给到的方法很明确地告知了调用者应该如何调用。

 

例如B_Category给到的- (UIViewController *)B_viewControllerWithContentText:(NSString *)contentText;方法。这可以让工程师一眼就可以明白使用方式,而没必要抓瞎拿着URL再去翻文档。

 

这能够很大程度提升工做效率,同时下降维护成本。



实施组件化方案的时机

 

MVP阶段事后,越早实施越好。

 

这里说的MVP不是一种设计模式,而是最小价值产品的意思,它是产品演进的第一个阶段。

 

通常来讲天使轮就是用于MVP验证的,在这个阶段产品闭环还没有肯定,所以产品自己的逻辑就会各类变化。可是过了天使轮以后,产品闭环已经肯定,此时就应当实施组件化,以应对A轮以后的产品拓张。

 

有的人说我如今项目很小,人也不多,因此不必实施组件化。确实,把一个小项目组件化以后,跟以前相比并无多大程度的改善,由于原本小项目就不复杂,改为组件化以后,也不会更简单。

 

但这实际上是一种很短视的认知。

 

组件化对于一个小项目而言,真正发挥优点的地方是在将来的半年甚至一年以后。

 

由于趁着人少项目小,实施组件化的成本就也很小,三四天就能够实施完毕。因而等未来一年以后业务拓张到更大规模时,就不会束手束脚了。

 

但若是等到项目大了,人手多了再去实施组件化,那时候实施组件化的复杂度确定比如今规模还很小的时候的复杂度要大得多,三四天确定搞不定,并且实施过程还会很是艰辛。到那时你就后悔为何当初没有早早实施组件化了。



Swift工程怎么办?

 

其实只要Target对象继承自NSObject就行了,而后带上@objc(className)。action的参数名永远只有一个,且名字须要固定为params,其它照旧。具体swift工程中target的写法参见A_swift

由于Target对象是游离于业务实现的,因此它去继承NSObject彻底没有任何问题。完整的SwiftDemo在这里。








本文Demo

 

本文转自:https://casatwy.com/modulization_in_action.html

相关文章
相关标签/搜索