iOS组件化方案(二)

概述

这是iOS组件化方案-总结的第二篇,在本文中我实现了Target-Action方案的Demo,并与第一篇介绍的protocol方案做出对比。

如果没有看过我第一篇protocol组件化方案的同学,可以先去下载我那篇文章中提供的Demo,方便理解我本文的详述以及了解我Demo中实现的业务场景,传送门iOS组件化方案-总结的第一篇

Target-Action方案

国际惯例先上Demo(下载主工程就好了哈,如果不能理解可以把所有业务模块都下载下来,Casa也提供了官方Demo,我第一篇文章中提供了传送门)

以下链接可到原文查看。

  • Target-Action方案主工程
  • Target-Action方案商品详情业务Category组件地址
  • Target-Action方案商品详情业务模块地址
  • Target-Action方案确认订单Category组件地址
  • Target-Action方案确认订单业务模块地址
  • Target-Action方案CTMediator地址因Casa没有把CTMediator做成公有库,所以我是直接拷贝过来做成我的私有库了。

实施

如何把模块做成私有pods我这里就不介绍了,想知道的可以看我第一篇组件化介绍文章。我这里只拿确认订单模块举例

确认订单模块是个单独的project,为了避免其他模块调用确认订单模块需引入整个模块,这里又做了一个确认订单业务Category的私有组件如下图

iOS组件化方案(二)

TAConfirmOrderBusinessCategory即是确认订单模块对外提供服务的入口,我们的业务场景是商品详情模块立即购买进入确认订单模块,确认订单模块提交订单后返回商品详情模块,同时得到通知下单成功,所以上图中入参提供了ConfirmComplete的Block,下图是TAConfirmOrderBusinessCategory.m中的实现


  1. #import "CTMediator+TAConfirmOrder.h" 
  2.   
  3. @implementation CTMediator (TAConfirmOrder) 
  4.   
  5. - (UIViewController *)confirmOrderViewControllerWithGoodsId:(NSString *)goodsId goodsName:(NSString *)goodsName ConfirmComplete:(dispatch_block_t)confirmComplete 
  6.     NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; 
  7.     params[@"goodsId"] = goodsId; 
  8.     params[@"goodsName"] = goodsName; 
  9.     params[@"completeBlock"] = confirmComplete; 
  10.     return [self performTarget:@"TAConfirmOrder" action:@"ConfirmOrderViewController" params:params shouldCacheTarget:NO]; 
  11.   
  12. @end  

OK,TAConfirmOrderBusinessCategory实现完了,我们来看下TAConfirmOrder模块,模块中定义一个Target_TAConfirmOrder具体实现如下图


  1. @interface Target_TAConfirmOrder : NSObject 
  2.   
  3. - (UIViewController *)Action_ConfirmOrderViewController:(NSDictionary *)params; 
  4.   
  5. @end 
  6.  
  7. @implementation Target_TAConfirmOrder 
  8.   
  9. - (UIViewController *)Action_ConfirmOrderViewController:(NSDictionary *)params 
  10.     TAConfirmOrderViewController *confirmOrderVC = [[TAConfirmOrderViewController alloc] init]; 
  11.     confirmOrderVC.goodsId = params[@"goodsId"]; 
  12.     confirmOrderVC.goodsName = params[@"goodsName"]; 
  13.     confirmOrderVC.confirmComplete = params[@"completeBlock"]; 
  14.     return confirmOrderVC; 
  15.   
  16. @end  

既然TAConfirmOrderBusinessCategory和TAConfirmOrder是2个project,那category是如何调用到Target_TAConfirmOrder的呢?其实很简单,我想看这篇文章的人大部分都知道,无非就是NSClassFromString ,performSelector这些方法,不知道的可以阅读源码

到这里我都没有贴过架构图或者讲过原理,只是贴了一部分代码和讲述如何实现,为什么?其实组件化原理很简单,简单到比当初学习UITableView容易多了,我的Demo即原理,如果还是看不明白可以自行google一轮或者在评论区提问.

Target_Action VS Protocol方案

1.是否需要注册?

  • Target_Action方案不需要注册
  • Protocol方案需要在启动的时候向CRProtocolManager注册

Target_Action很好的利用了runtime特性,减少注册这一步,不过对于即将切到Swift的同学就有点尴尬了。

在上篇提供的Protocol方案Demo中,在向CRProtocolManager注册服务的是实例对象而非Class,这样确实会造成内存常驻,但是无伤大雅,熟悉runtime的同学应该都知道第一次调用某个类或对象的方法,会构建出类对象,所以无论你用Class注册还是实例对象注册类对象都在,抛开类对象对于一个不挂任何property的实例对象所占用的内存是很小的。当然你可能会问既然都差不多你为什么注册实例而不是注册Class,注册的ServiceProvider实例对象在有些情况下可以记录一些状态,当然这只是极少数情况下出现的,你如果真要把ServiceProvider当单例对象用,我还是强烈建议注册Class

不过我不认为ServiceProvider需要向中间件注册有逻辑上的问题,区别只是可省可不省

2.依赖关系

Target_Action方案中商品详情模块依赖TAConfirmOrderBusinessCategory组件来获取确认订单模块的服务

Protocol方案中商品详情模块需要依赖CRConfirmOrderServiceProtocol通过CRProtocolManager组件获取提供服务的实例对象,同时确认订单模块也依赖CRConfirmOrderServiceProtocol来注册服务

乍一看Protocol方案依赖关系好像对业务产生了侵入因为调用方和实现方都同时依赖了CRConfirmOrderServiceProtocol,其实CRConfirmOrderServiceProtocol应当属于确认订单模块的一部分,把他独立出来只是为了避免调用方需要直接引用实现方,这个依赖在架构图中体现的应该是虚线而不是实线。试想如果Target_Action方案不用runtime,那BusinessCategory也需要直接依赖Target。利用runtime中NSProtocolFromString也可以解决对CRConfirmOrderServiceProtocol的依赖,只是造成一定量的硬编码不够优雅。(提一下,虽然runtime在一些特定场景给我们开发带来一些意想不到的奇效,但是runtime跳过了编译器检查,有时候排除bug比较艰难,所以还是慎用)

另Protocol方案中商品详情模块同时依赖CRConfirmOrderServiceProtocol和CRProtocolManager而Target_Action方案中商品详情模块仅依赖TAConfirmOrderBusinessCategory,依赖关系如下图

iOS组件化方案(二)

Protocol方案横向依赖了2者,Target_Action方案纵向依赖。Target_Action设计的更优异

3.可读性、硬编码

Target_Action在Category中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这造成了一定量的硬编码,不过在现实开发中,一个模块一个模块提供的category通常是一个人写的,所以造成的影响微乎其微,但是给其他阅读代码的人带来一些不便,甚至同一个人写Cagetory、Target的时候也需要在2个project不停切换查看之前在Target中定义的函数名

Protocol方案中0硬编码,可读性更高。

在这里提一下Url注册方案,Url注册方案我觉得最大的问题是大量的硬编码,可读性很差,维护性也很差,对文档的依赖度很高,而且需要有人不停督促文档的更新。我想很多同学对此都深有体会,每个项目第一版的接口文档相对比较详细、全面,随着版本迭代更新,某个接口增加了一些字段,通常后台开发人员都是忘记去更新文档也许是因为忙,甚至有些同学懒的更新文档,一般这时候都是通过qq或者其他通讯工具告知客户端开发人员增加了哪些个字段,字段含义是什么。待时间长了,客户端开发人员忘记字段含义或者换了另一个开发人员接手,不知道这个字段含义是什么,先去翻看以前聊天记录,找不到去看接口文档,文档还是1.0版本。。。。我去。。。

总结

综合以上3点,Target_Action更优,我们公司目前也采用的Target_Action方案,如果有同学有计划切到Swift语言开发,我建议Protocol方案。事实上没有哪个方案是万能的,具体的采用还得结合自己的业务以及开发人员的整体素质,如果你还是拿不定主意,阿里开源了一个模块解耦框架BeeHive(protocol注册),你就向着大厂靠拢吧。Url注册方案Demo我就不提供了,因为它的可读性,维护性以及常规参数传递的缺点让我放弃了它,不过Url注册方案配合服务器下发路由能够很好的解决bug,前提是你们的模块得Native开发一套 H5开发一套(或者RN和Weex)

补 业务模块的划分

有不少同学知道了组件化,但是不知道如何去划分业务模块,我大致拿京东App某几个业务举例见下图

iOS组件化方案(二)

图片每个Module组件化后就是一个单独的project,也许很多project里面只有一个ViewController,这也是合理的划分,比如商品详情,很多模块(服装城,京东超市,全球购。。。)会调用到商品详情模块,那把商品详情模块中的业务强行塞到(服装城,京东超市,全球购。。。)任何一个project都是不合理的,确认订单同理。组件化是把业务纵切,具体到某个业务模块中network模块,database模块的划分是横切

预告

发现很多同学理解MVC的姿势不对,导致controller很臃肿难以维护,下一篇我会把自己理解的MVC写成一个Demo,这个Demo会是一个业务比较庞大的模块(所以时间会长一点。毕竟我白天要忙公司的项目,还有几个个人项目需要维护)在这个Demo中职责划分会很清楚,敬请期待哈。




本文作者:佚名
来源:51CTO
上一篇:js设计模式--行为型--状态模式


下一篇:iOS:UIView视图与组件控件