随着公司业务需求的不断迭代发展,工程的代码量和业务逻辑也愈来愈多,原始的开发模式和架构已经没法知足咱们的业务发展速度了,这时咱们就须要将原始项目进行一次重构大手术了。这时咱们应该很清晰此次手术的动刀口在哪,就是以前的高度耦合的业务组件和功能组件,手术的目的就是将这些耦合拆分红互相独立的各个组件。html
咱们先来张图看看在没有使用组件化前,咱们各个模块间的依赖关系ios
从上面这种各个业务组件的依赖关系来看,他们是互相依赖的,业务组件和业务组件间产生了严重的耦合关系,这样一来对咱们工程的扩展性就会大大的下降,维护成本就会变高。git
举个例子:假设某天产品经理说,我们公司的业务发展的太好了,我们的营销模块须要独立出来成一个单独的应用,以便于我们能够添加更多高效的营销手段。这时咱们就傻眼了,须要独立出一个app出来,这可怎么搞啊,营销模块的代码和其余的不少业务代码耦合在一块儿了,如今要独立出来,那就只能从新写一个营销应用了,以前的代码剥离不干净了。github
从上面咱们列举的一个简单的例子能够体会到:在项目没有作到真真意义上的组件化以前,各个业务模块和业务模块间的高度耦合,功能组件和功能组件间的高度耦合对将来公司的业务扩展来讲,成本很高,不能作到一样业务逻辑的代码的高度复用,这样对咱们开发来讲也是效率的下降。小程序
好了,有的同窗可能会说,既然上面各个模块间耦合这么高,那我就来将这些耦合解耦,因而,可能会出现下面这张图的模块间的关系。设计模式
从下面这张图来看,咱们发现,如今确实能作到各个业务模块间彻底的解耦了,他们再也不互相依赖了,同时咱们引入了一个中间调度者的一个角色,如今是各个业务模块和这个中间调度者角色产出了严重的依赖。咱们思考下发现,咱们的各个业务模块依赖这个中间调度者,这个是彻底正常的,由于他们须要这个调度者来作统一的事件分发工做,可是这个调度者却又依赖了每一个业务模块,这层依赖是有必要的吗?咱们回头想一想真正的组件化开发是彻底的去依赖化,这个依赖是彻底没有必要的。例如:假设咱们如今有一个新的B APP须要开发,这时咱们也须要用到这个中间调度者组件,可是咱们不能直接拿过来用,由于它又依赖了不少A App的业务组件。所以,咱们的组件化架构设计又须要一次升级变动了,升级成以下图所示的模型。数组
从上面的这张图,咱们能够看出,各个业务模块间只会依赖中间调度者,而且中间调度者不对各个模块产生任何的依赖。缓存
好了,从上面的三张图之间的对比,咱们就能够很好的理解为何咱们的工程急须要实现组件化架构开发了,以及各自的优劣势。bash
关于组件该如何拆分,这个没有一个完整的标准,由于每一个公司的业务场景不同,对应衍生出来的各个业务模块也就不同,因此业务组件间的拆分,这个根据本身公司的业务模块来进行合理的划分便可。这里咱们来讲下整个工程的组件大体的划分方向
至于组件的拆分颗粒度,这个着实很差去判定,因人而异,不一样的需求功能复杂度拆分出来的组件大小也不尽相同
在讲如何从零到一来实现一个组件化架构项目前,咱们须要熟练掌握使用pod来制做组件库。下面咱们就围绕提供的组件化示例项目来展开讲解。
首先,咱们来看示例Demo中包含哪些业务组件(以下图所示:):
示例Demo中,我提供了三个业务组件来做为演示效果,其中业务模块A和业务模块B是临时业务模块组件,电子发票业务组件时真实的企业需求功能组件。
咱们再来看下示例Demo中都提供了哪些工具组件(以下图所示)
注意了:这里提供的6个工具组件也都是做者已经封装好的功能组件,你们也能够直接 install 安装使用的哦。
第一步:
咱们先建立一个空的iOS工程项目:MainProject,这个空项目做为咱们的主工程项目,就是上面所说的壳子工程项目,而后初始化pod,这里不清楚pod的使用的小伙伴们请自行查阅资料。
第二步:
咱们建立一个空工程项目:ModuleA,这个ModuleA 项目做为咱们的业务A组件。而后咱们初始化pod,初始化podspec文件。
第三步:
咱们建立一个空工程项目:ModuleB,这个ModuleB 项目做为咱们的业务B组件。而后咱们初始化pod,初始化podspec文件。
第四步:
咱们建立一个空工程项目:ComponentMiddleware,这个项目就是咱们上面所说的中间调度者。而后咱们初始化pod,初始化podspec文件。
第五步:
咱们建立一个空工程项目: ModuleACategory,这个工程是对应业务组件A的一个分类工程。而后咱们初始化pod,初始化podspec文件。
第六步:
咱们建立一个空工程项目: ModuleBCategory,这个工程是对应业务组件B的一个分类工程。而后咱们初始化pod,初始化podspec文件。
好了,上面的主工程和两个业务组件工程,以及两个组件分类工程都已建立完毕,下面咱们来说解他们各个之间如何工做的。我就从主工程加载业务组件开始往下捋,顺藤摸瓜式的引出每一个工程的用意。
第七步:
咱们在主工程MainProject的Podfile中引入咱们的业务组件B工程ModuleB,以及引入咱们的ModuleB的分类工程:ModuleBCategory。而后咱们pod install。这时已将这两个组件库引入到咱们的主工程中了。
示例代码以下:
# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/guangqiang-liu/GQSpec.git'
target 'GQComponentDemo' do
pod 'ModuleB'
pod 'ModuleBCategory'
end
复制代码
而后咱们在主工程中添加一个按钮事件,这个事件是点击 push 到业务组件B的 页面。
示例代码以下:
#import <ModuleBCategory/ComponentScheduler+ModuleB.h>
- (void)moduleB {
UIViewController *VC = [[ComponentScheduler sharedInstance] ModuleB_viewControllerWithCallback:^(NSString *result) {
NSLog(@"resultB: --- %@", result);
}];
[self.navigationController pushViewController:VC animated:YES];
}
复制代码
第八步:
上面第七步中,咱们用到了ModuleBCategory 这个分类工程。这个工程咱们只对外暴露了两个文件。这两文件是上面的中间调度者的分类,也就是说是中间件的分类。咱们先来看下这个分类文件的.h 和.m 实现。
.h
#import "ComponentScheduler.h"
@interface ComponentScheduler (ModuleB)
- (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback;
@end
复制代码
.m
#import "ComponentScheduler+ModuleB.h"
@implementation ComponentScheduler (ModuleB)
- (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback {
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"callback"] = callback;
return [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
}
@end
复制代码
咱们发现这个分类实现很是的简单,就是对外暴露一个函数,而后执行[self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
,并将执行的返回值返回出去。
这个分类的做用你能够理解为咱们提早约定好Target的名字和Action的名字,由于这两个名字中间件组件中会用到。
上面的performTarget:action:params:shouldCacheTarget
函数是中间件提供的函数。由于ModuleBCategory 是 ComponentScheduler(中间件)的分类文件,因此能够调用到这个函数啦。
在ModuleBCategory 工程中须要引用到了中间件工程因此咱们须要在ModuleBCategory 的Podfile文件中引用 中间件组件
示例代码以下:
# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/guangqiang-liu/GQSpec.git'
target 'ModuleB-Category' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for ModuleB-Category
pod 'ComponentScheduler'
end
复制代码
第九步:
由于上面第八步中引用到中间件工程,这里咱们就来看下中间件工程到底作了什么工做。还记得上面第八步中,咱们调用了一个中间件提供的函数:performTarget:action:params:shouldCacheTarget
吧,这个是中间件核心函数。
核心函数代码块以下:
还记得上面第八步中,咱们调用这个函数传递的参数吧,咱们在把调用代码拿过来看下
[self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
咱们能够看到 TargetName
是咱们传递的 ModuleB
,action
是咱们传递的viewController
,而后咱们将 这两个参数传给了下面的函数:
[self safePerformAction:action target:target params:params];
咱们来看下这两个参数的值具体是什么:
这个函数最终调用到苹果官方提供的函数:[target performSelector:action withObject:params];
看到 performSelector: withObject:
你们应该就比较熟悉了,iOS的消息传递机制。
[Target_ModuleB performSelector:Action_viewController withObject:params];
复制代码
上面这行伪代码意思是: Target_ModuleB
这个类 调用它的 Action_viewController:
方法,而后传递的参数为 params
。
细心的小伙伴们就会发现,咱们没有看到过哪里有这个Target_ModuleB
类啊,更没有看到Target_ModuleB
调用它的 Action_viewController:
方法啊。
是的,这个Target_ModuleB
类和类的Action_viewController
方法就在第十步中讲解到。
第十步:
终于到了最后一步了,写的好艰辛,嗯,小伙们不要捉急,快了,快讲完了
细心的小伙们发现,咱们上面讲的9步中,好像都没有提业务组件B的东西。是的,业务组件B除了提供组件B的业务功能外,业务组件B还须要为咱们提供一个Target文件。
咱们先来看下业务组件B的业务代码:
示例代码以下:
#import "ModuleBViewController.h"
#import "PageBViewController.h"
@interface ModuleBViewController ()
@end
@implementation ModuleBViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"我是模块B业务组件";
self.view.backgroundColor = [UIColor whiteColor];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(0, 0, 300, 100);
btn.backgroundColor = [UIColor greenColor];
btn.center = self.view.center;
[btn setTitle:@"模块B业务功能组件" forState: UIControlStateNormal];
[btn addTarget:self action:@selector(push) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
}
- (void)push {
PageBViewController *VC = [[PageBViewController alloc] init];
[self.navigationController pushViewController:VC animated:YES];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
复制代码
咱们发现,业务组件B的业务代码也很简单,就是作一个push 跳转操做,从PageA 控制器跳转到 PageB 控制器。 这个没有什么好讲的
咱们再来看上面提到的target文件
示例代码以下:
.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Target_ModuleB : NSObject
- (UIViewController *)Action_viewController:(NSDictionary *)params;
@end
复制代码
.m
#import "Target_ModuleB.h"
#import "ModuleBViewController.h"
@implementation Target_ModuleB
- (UIViewController *)Action_viewController:(NSDictionary *)params {
ModuleBViewController *VC = [[ModuleBViewController alloc] init];
return VC;
}
@end
复制代码
从上面的实现文件中,咱们能够看到,Target文件的做用也很简单,就是为咱们提供导航跳转的目标控制器实例对象。这里的目标控制器实例就是业务组件B的ModuleBViewController
实例。
细心的小伙伴们发现,咦!咱们在第九步中打印出来的target
和 action
不就正是Target文件的Target_ModuleB
和 Action_viewController:
。
上面咱们只是串讲了业务组件B的一系列流程,业务组件A的用法和业务组件B的用法同样,若是后面再有业务组件C,D,都是同样的道理,就再也不一一讲解了。
好了,如今小伙伴们应该看懂了这一连串的工做流程了吧,若是尚未看懂,能够看看Casa的讲解CTMediator。做者建议直接运行提供的示例Demo项目进行调试,这样便于理解各个组件之间的关系。
最后,咱们再来看张组件化完整的架构图:
上面咱们讲解的只是简单的项目组件化架构的基础框架搭建,可是在真正的企业开发中,咱们只搭建这样一个简单项目框架结构还远远不能知足需求的开发,咱们还须要在项目框架中添枝加叶来知足现有需求。在上面提供的示例Demo中,我将电子发票业务组件独立成一个完整的工程,并结合了当下比较流行的MVVM设计模式和RAC数据绑定框架来实现电子发票模块的功能开发。若是有小伙们对 MVVM + RAC 实战开发感兴趣的,能够单独 install 电子发票工程查看,工程地址:iOS-MVVM-RAC
本篇文章主要借鉴了casatwy的CTMediator思想从新实践了一遍,下面也有蘑菇街的MGJRouter 和 阿里的 BeeHive 供你们学习参考。