浅谈 iOS 组件化开发

背景

组件化做为目前移动应用架构的主流方式之一,近年来一直是业界积极探索和实践的方向。html

起初的这个项目,App只有一条产品线,代码逻辑相对比较清晰,后期随着公司业务的迅速发展,如今App里面承载了大概五六条产品线,每一个产品线的流程有部分是同样的,也有部分是不同的,这就须要作各类各样的判断及定制化需求。大概作了一年多后,出现了不一样产品线提过来的需求,开发人员都须要在主工程中开发,可是开发人员开发的是不一样的产品线,也得将整个工程跑起来,代码管理、并行开发效率、分支管理、上线时间明显有所限制。大概就在去年末,咱们的领导提出了这个问题,但愿做成组件化,将代码重构拆分红模块,在主工程中组装拆分的模块,造成一个完整的App。 前端

注:

  1. 区别于功能模块/组件(好比图片库,网络库),本文讨论的是业务模块/组件(好比订单模块,商品模块)相关的架构设计。
  2. 相比组件(Component),我的感受称之为模块(Module)更为合适。组件强调物理拆分,以便复用;模块强调逻辑拆分,以便解耦。并且若是用过 Android Studio, 会发现它建立的子系统都叫 Module. 但介于业界习惯称之为组件化,因此咱们继续使用这个术语。本文下面所用名词,“模块”等同于“组件”。

正文

1、组件化初识

传统的 App 架构设计更多强调的是分层,基于设计模式六大原则之一的单一职责原则,将系统划分为基础层,网络层,UI层等等,以便于维护和扩展。但随着业务的发展,系统变得愈来愈复杂,只作分层就不够了。App 内各子系统之间耦合严重, 边界愈来愈模糊,常常发生你中有我我中有你的状况(如图一)。ios

图一
这对代码质量,功能扩展,以及开发效率都会形成很大的影响。此时,通常会将各个子系统划分为相对独立的模块,经过中介者模式收敛交互代码,把模块间交互部分进行集中封装, 全部模块间调用均经过中介者来作(如图二)。

图二

这时架构逻辑会清晰不少,但由于中介者仍然须要反向依赖业务模块,这并无从根本上解除循坏依赖等问题。时不时发生一个模块进行改动,多个模块受影响编译不过的状况。进一步的,经过技术手段,消除中介者对业务模块依赖,即造成了业务模块化架构设计(图三)。git

图三

经过业务模块化架构,通常能够达到明确模块职责及边界,提高代码质量,减小复杂依赖,优化编译速度,提高开发效率等效果。不少文章都有相关分析,在此再也不累述。github

组件化开发的缺点:编程

  • 代码耦合严重
  • 依赖严重
  • 其它app接入某条产品线难以集成
  • 项目复杂、臃肿、庞大,编译时间过长
  • 难以作集成测试
  • 对开发人员,只能使用相同的开发模式

组件化开发的优势:swift

  • 项目结构清晰
  • 代码逻辑清晰
  • 拆分粒度小
  • 快速集成
  • 能作单元测试
  • 代码利用率高
  • 迭代效率高

2、常见组件化方案

业务模块化设计经过对各业务模块的解耦改造,避免循环双向依赖,达到提高开发效率和质量的目的。但业务需求的依赖是没法消除的,因此模块化方案首先要解决的是如何在无代码依赖的状况下实现跨模块通讯的问题。iOS 由于其强大的运行时特性,不管是基于 NSInvocation 仍是基于 peformSelector 方法, 均可以很很容易作到这一点。但不能为了解耦而解耦,提高质量与效率才是咱们的目的。直接基于 hardcode 字符串 + 反射的代码明显会极大损害开发质量与效率,与目标背道而驰。因此,模块化解耦需求的更准确的描述应该是“如何在保证开发质量和效率的前提下作到无代码依赖的跨模块通讯”。 目前业界常见的模块间通信方案大体以下几种:vim

基于路由 URL 的 UI 页面统跳管理。 基于反射的远程接口调用封装。 基于面向协议思想的服务注册方案。 基于通知的广播方案。 根据具体业务和需求的不一样,大部分公司会采用以上一种或者某几种的组合。后端

2.1 URL 跳转方案

统跳路由是页面解耦的最多见方式,大量应用于前端页面。经过把一个 URL 与一个页面绑定,须要时经过 URL 能够方便的打开相应页面。设计模式

//经过路由URL跳转到商品列表页面
//kRouteGoodsList = @"//goods/goods_list"
UIViewController *vc = [Router handleURL:kRouteGoodsList];  
if(vc) {  
    [self.navigationController pushViewController:vc animated:YES];
}
复制代码

固然有些场景会比这个复杂,好比有些页面须要更多参数。 基本类型的参数,URL 协议自然支持:

//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];
}

复制代码

复杂类型的参数,能够提供一个额外的字典参数 complexParams, 将复杂参数放到字典中便可:

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

复制代码

上面方法里的 completion 参数,是一个回调 block, 处理打开某个页面须要有回调功能的场景。好比打开会员选择页面,搜索会员,搜到以后点击肯定,回传会员数据:

//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];
}

复制代码

考虑到实现的灵活性,提供路由服务的页面,会将 URL 与一个 block 相绑定。block 中放入所需的初始化代码。能够在合适的地方将初始化 block 与路由 URL 绑定,好比在 +load 方法里:

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

复制代码

更多路由 URL 相关例子,能够参考 Bifrost 项目中的 Demo.

URL 自己是一种跨多端的通用协议。使用路由URL统跳方案的优点是动态性及多端统一 (H5, iOS,Android,Weex/RN); 缺点是能处理的交互场景偏简单。因此通常更适用于简单 UI 页面跳转。一些复杂操做和数据传输,虽然也能够经过此方式实现,但都不是很效率。

目前天猫和蘑菇街都有使用路由 URL 做为本身的页面统跳方案,达到解耦的目的。

2.2 Target-Action 方案

当没法 import 某个类的头文件但仍需调用其方法时,最常想到的就是基于反射来实现了。例:

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

复制代码

但这种方式存在大量的 hardcode 字符串。没法触发代码自动补全,容易出现拼写错误,并且这类错误只能在运行时触发相关方法后才能发现。不管是开发效率仍是开发质量都有较大的影响。

如何进行优化呢?这实际上是各端远程调用都须要解决的问题。移动端最多见的远程调用就是向后端接口发网络请求。针对这类问题,咱们很容易想到建立一个网络层,将这类“危险代码”封装到里面。上层业务调用时网络层接口时,不须要 hardcode 字符串,也不须要理解内部麻烦的逻辑。

相似的,我能够将模块间通信也封装到一个“网络层”中(或者叫消息转发层)。这样危险代码只存在某几个文件里,能够特别地进行 code review 和联调测试。后期还能够经过单元测试来保障质量。模块化方案中,咱们能够称这类“转发层”为 Mediator (固然你也能够起个别的名字)。同时由于 performSelector 方法附带参数数量有限,也没有返回值,因此更适合使用 NSInvocation 来实现。

//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
复制代码

而后各个业务模块依赖Mediator, 就能够直接调用这些方法了。

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

复制代码

这种方案的优点是调用简单方便,代码自动补全和编译时检查都仍然有效。 劣势是 category 存在重名覆盖的风险,须要经过开发规范以及一些检查机制来规避。同时 Mediator 只是收敛了 hardcode, 并未消除 hardcode, 仍然对开发效率有必定影响。

业界的 CTMediator 开源库,以及美团都是采用相似方案。

2.3 服务注册方案

有没有办法绝对的避免 hardcode 呢?若是接触事后端的服务化改造,会发现和移动端的业务模块化很类似。Dubbo 就是服务化的经典框架之一。它是经过服务注册的方式来实现远程接口调用的。即每一个模块提供本身对外服务的协议声明,而后将此声明注册到中间层。调用方能从中间层看到存在哪些服务接口,而后直接调用便可。例:

//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];  
...
复制代码

这种方式的优点也包括调用简单方便。代码自动补全和编译时检查都有效。实现起来也简单,协议的全部实现仍然在模块内部,因此不须要写反射代码了。同时对外暴露的只有协议,符合团队协做的“面向协议编程”的思想。劣势是若是服务提供方和使用方依赖的是公共模块中的同一份协议(protocol), 当协议内容改变时,会存在全部服务依赖模块编译失败的风险。同时须要一个注册过程,将 Protocol 协议与具体实现绑定起来。

业界里,蘑菇街的 ServiceManager 和阿里的 BeeHive 都是采用的这个方案。

2.4 通知广播方案

基于通知的模块间通信方案,实现思路很是简单, 直接基于系统的 NSNotificationCenter 便可。 优点是实现简单,很是适合处理一对多的通信场景。 劣势是仅适用于简单通信场景。复杂数据传输,同步调用等方式都不太方便。 模块化通信方案中,更多的是把通知方案做为以上几种方案的补充。

3、组件化开发必备工具

组件的存在方式是以每一个pod库的形式存在的。那么咱们组合组件的方法就是经过利用CocoaPods的方式添加安装各个组件,咱们就须要制做CocoaPods远程私有库,将其发不到公司的gitlab或GitHub,使工程可以Pod下载下来。

3.1 Git的基础命令:

echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master
复制代码

3.2 CocoaPods远程私有库制做:

  • 一、Create Component Project
pod lib create ProjectName
复制代码
  • 二、Use Git
echo "# test" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/c/test.git
git push -u origin master
复制代码

三、Edit podspec file

vim CoreLib.podspec
Pod::Spec.new do |s|
  s.name             = '组件工程名'
  s.version          = '0.0.1'
  s.summary          = 'summary'

  s.description      = <<-DESC
  description
                       DESC

  s.homepage         = '远程仓库地址'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { '做者' => '做者' }
  s.source           = { :git => '远程仓库地址', :tag => s.version.to_s }

  s.ios.deployment_target = '8.0'

  s.source_files = 'Classes/**/*.{swift,h,m,c}'
  s.resources = 'Assets/*'
  
  s.dependency 'AFNetworking', '~> 2.3'
end
复制代码
  • 四、Create tag
//create local tag
git tag '0.0.1'
或
git tag 0.0.1

//local tag push to remote
git push --tags
或
git push origin 0.0.1

//delete local tag
git tag -d 0.0.1

//delete remote tag
git tag origin :0.0.1
复制代码
  • 五、Verify Component Project
pod lib lint --allow-warnings --no-clean
复制代码
  • 六、Push To CocoaPods
pod repo add CoreLib git@git.test/CoreLib.git
pod repo push CoreLib CoreLib.podspec --allow-warnings
复制代码

4、各个组件该如何拆分

关于组件该如何拆分,这个没有一个完整的标准,由于每一个公司的业务场景不同,对应衍生出来的各个业务模块也就不同,因此业务组件间的拆分,这个根据本身公司的业务模块来进行合理的划分便可。这里咱们来讲下整个工程的组件大体的划分方向。

    1. 项目主工程:当咱们工程彻底使用组件化架构进行开发后,咱们会惊奇的发现咱们的主工程就成了一个空壳子工程。由于全部的主工程呈现出来的内容都被拆分红了各个独立的业务组件了,包括各个工具组件也是各自互相独立的。这样咱们发现开发一个完整的APP就像是搭建乐高积木同样,各个部件都有,任咱们随意的组合搭建,这样是否是感受很爽。
    1. 业务组件:业务组件就是咱们上面示例图所示的各个独立的产品业务功能模块,咱们将其封装成独立的组件。例如示例Demo中的电子发票业务组件,业务组件A,业务组件B。咱们经过组装各个独立的业务组件来搭建一个完整的APP项目。
    1. 基础工具类组件:基础工具类是各个互相独立,没有任何依赖的工具组件。它们和其它的工具组件、业务组件等没有任何依赖关系。这类组件例若有:对数组,字典进行异常保护的Safe组件,对数组功能进行扩展Array组件,对字符串进行加密处理的加密组件等等。
    1. 中间件组件:这个组件比较特殊,这个是咱们为了实现组件化开发而衍生出来的一个组件,上面示例图中的中间调度者就是一个功能独立的中间件组件。
    1. 基础UI组件:视图组件就比较常见了,例如咱们封装的导航栏组件,Modal弹框组件,PickerView组件等。
    1. 业务工具组件:这类组件是为各个业务组件提供基础功能的组件。这类组件可能会依赖到其余的组件。例如:网络请求组件,图片缓存组件,jspatch组件等等

至于组件的拆分颗粒度,这个着实很差去判定,因人而异,不一样的需求功能复杂度拆分出来的组件大小也不尽相同

总结

以上咱们只是讲解了简单的理论知识,若是你们要实战的话仍是要多查阅一些资料,不过目前咱们的app使用的是组件化开发的方式,目前各个模块解耦,能够快速开发新的app。优势仍是有不少的,但愿你们也能够敢于尝试。不过组件化模块化拆分也要考虑到本身的项目的,最终找到适合本身项目的方案才是你要作的事情,但愿本片文档能够为你提供一些思路。

参考

相关文章
相关标签/搜索