iOS组件化辨析
iOS开发相对于其他开发语言来说是个很年轻的开发方式,很多开发理念是与其他语言不谋而合的。随着客户端软件规模和复杂性的不断增加,在软件设计中,软件的局部和整体的系统结构显得越来越重要,对此人们提出了软件体系结构的概念,一种方式即我们所熟知的组件化开发方案。
针对于组件化的技术方案,Limboy分享了两篇关于蘑菇街的组件化的实现,分别是《蘑菇街 App 的组件化之路》、《蘑菇街 App 的组件化之路·续》。基于Limboy的分享,Casa提出了一些意见,并在博客上分享出来,即《iOS应用架构谈 组件化方案》。最后,Bang在二人的基础上结合自己的思考对三种方案做了一番梳理,即《iOS 组件化方案探索》。通过这些文章,对于移动应用开发中的组件化方案基本上就有了一个大体的理念了。本文将在前人的基础上系统的梳理一遍,以满足日常开发的需要。
本文包括:
- 组件化的历史
- 组件化的优势
- 组件化的系统实现
组件化的历史
这一段完全可以跳过去不读。不过读读也无妨。 -- 刘心武《钟鼓楼》
自从1948年6月21日,软件模式在曼切斯特大学诞生以来,软件体系结构经历了四个阶段:无结构、萌芽、初级和高级阶段。自上世纪 90 年代步入高级阶段以来,软件开发的目标是使软件具备较好的自适应性、互操作性、可扩展性和可重用性,软件开发强调采用构件化技术和体系结构技术。
在项目的实施阶段,体系结构是建立开发人员的组织、分工、协调开发人员关系和配合的依据。在项目的维护升级阶段,对软件的任何扩充和修改都要在体系结构的指导下进行,以维护整体设计的合理性和正确性,并为维护升级的复杂性和代价分析提供依据。
在日常开发中,程序员一直考虑的一个问题是如何让代码变的简单。抛开技术大牛和大神程序员这条路(毕竟开发人员是一种金字塔结构),最后自然而然形成的一套思路就是大团队的协同合作。经过长时间的积累,软件开发人员借鉴了硬件组成原理(如CPU从提升主频到多核的改变),基于组件式程序设计思想,提出了组件式软件体系结构,这一理论给软件开发工程注入了无限的活力。进而牵涉到的两个原则就是:内聚性和耦合性,也就是架构设计中所谓的高内聚、低耦合。通俗来讲就是:一个模块实现所需要的全部功能(内聚性),而不需要其他人辅助实现;并且代码不会影响到别的模块(低耦合性)。
组件化一定程度上可以约等于模块化,调用者只需关注输入和输出,相互之间没有影响,组件化本质的一点就是封装。每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;每个组件相对独立,界面只不过是组件的容器,组件自由组合形成功能完整的界面;当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换。组件化只是所有GUI开发的一种思路,总思想就是分而治之、重复利用。
组件化通常有以下几个要素:
1)组件是对逻辑的封装,不限于UI元素。
2)组件具备单个可移植性,即“随加载随用”,不需要为其准备复杂的基础条件。
组件是一个广义上的概念,并不一定都是页面跳转,还可以是其他不具备UI属性的服务提供者,比如日志服务,VOIP服务,内存管理服务和其他帮助服务等,即在更高的维度去封装功能单元。组件化开发一个最重要的特点就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式。对这些功能单元进行进一步的分类,才能在具体的业务场景下做更合理的设计。这些在MrPeakTeach的《iOS组件化方案》中也做了相应的讨论。
组件可以分为以下几类:
1)带UI属性的独立业务模块。
这些组件有很具体的业务场景,如App的主页模块、登录注册模块、分享模块等。这类模块一般有个入口Controller,可以通过Push或Present的方式作为入口接入。Controller作为页面的基本单位和Web Page有很高的相似度,蘑菇街采取URL注册的实现方式或是天猫的统跳协议等,采用类URL的方式标记本地的每一个Controller,不仅方便本地的跳转,还能支持Server下发跳转指令,对于业务的兼容性有很大的帮助。
从理论上来说,组件化和URL本身并没有什么联系,URL只是接入组件的方式之一。
2)不具备UI属性的独立业务模块。
这类模块不具备UI场景,但却和具体的业务相关,如日志上报模块、埋点统计等。组件被调用分为远程和本地,这种日志服务的调用是本地类型的调用,用URL来标这类记本地服务多有不便。
3)不具备业务场景的功能模块。
这类模块和具体的业务场景无关,如数据模块(提供数据的读写服务,包含多线程的处理等)、Network模块和图片处理类等。这些模块可以被任意模块使用,但不和任何业务相关。这种组件属于我们app的基础服务提供者,更像是一个个SDK,或是帮助类。通过Pods使用的很多著名第三方库都属于这一类,像FMDB,SDWebImage等。
组件其实不是那么好进行抽象设计的,组件可以横向组件,但也要考虑纵向复用的问题,不得不牵涉的问题就是耦合问题。还有的就是组件的粒度问题,组件要细分到何种程度,都是在架构设计中需要考虑的,这些问题我们在具体使用时都需要考虑。
组件化的优势
组件化编程的关键目的是为了将程序模块化,使各个模块之间可以单独开发,单独测试。组件式体系结构把程序的功能分散在各个不同的组件中来完成,组件是可独立开发的程序模块,它能够动态地插入到系统中,并且可以被自由地删除和替换。
组件化能够提高软件开发的并行性和开发效率,降低设计开发难度,缩短开发周期,增强应用程序的可运行性、可测试性和可维护性。归纳起来就是:
-
组件化能够提高软件的复用度。
-
降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,以满足用户不断变化的需求,缩短项目交付周期。
-
组件化因为强大的独立性,可以提高软件开发的并行性,为软件产业的大规模生产提供支持,便于协同开发。
-
提高可维护性,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单。
由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级。例如某个组件负责处理异步请求,与业务无关,我们添加缓存机制,序列化兼容,编码修正等功能,一来整个系统中的每个使用到这个组件的模块都会受惠;二来可以使这个组件更具健壮性。由于代码中的耦合度降低了,每个模块都可以分拆为一个组件,团队中每个人发挥所长维护各自组件,对整个应用来说是精细的打磨。
组件化的系统实现
如果您接触过早期的项目,OC中的代码都是写在一起的,里面会分成各种Tools、Helper等,现在看起来很不优雅。不过,这也是程序的发展过程,我想如果您见多识广,即使是现在仍会碰到一些认为移动端就是显示显示UI的领导,大千世界各有不同罢了。
我始终认为,即使是一个普通的开发人员也是需要一些架构观的,从项目的整体考虑对一个技术人的成长是有很大帮助的。本次关于架构方面的内容不会涉及很多,重点还是在架构中的组件化这个方面。
在我们早期的组件化实现中,会实现一个Manager类,里面封装着所有的跳转逻辑,外界调用组件都通过这个类实现,因此Manager对其他类也不得不产生耦合,因为需要#import相关的类。即Bang所说的中间件耦合。
图1当组件里的类被移除或是实现类改变了都需要在Manager类里做出改变。
和Bang的思路相同,通过Runtime反射调用可以解除耦合。
+ (UIViewController *)TestComponent_viewController:(NSString *)some {
Class cls = NSClassFromString(@"TestComponent");
return [cls performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"some":some}];
}
如此便可用调用组件里的TestComponent类里的detailViewController方法
+ (UIViewController *)detailViewController:(NSString *) some {
DetailViewController *detailVC = [[DetailViewController alloc] initWithSome: some];
return detailVC;
}
图2
由此,便解除了import头文件的耦合问题。import头文件算一种耦合,因为头文件缺失会导致编译出错。业务耦合是另一种维度的耦合,这种方式对业务耦合并没有多大的改观。如果组件方修改了业务接口,即使你能编译通过,你所调用的组件也无法正常工作了。此时便需要做一些容错处理了,当业务无法实现时,相应的做一些处理,这些需要针对不同的业务流进行操作。如Hybrid跳转,如果不能跳转就跳到web,因为web加载只需要一个URL。但是本地的实现逻辑呢,这是一个问题。
针对这个问题Mrpeak给出了一些建议,在组件化上更倾向于采用Distributed Design方式(如图3)。各个组件”自扫门前雪“,用规范的protocol声明,加上严格的版本控制来提供组件服务,称之为Protocol+Version方案。
图3因为采用Centralized Design(图1),Centralized设计在Node增加的情形下会增加中央节点的负担。Mrpeak以IP协议的路由寻址算法(Distributed Design Vs Centralized Design的一个经典场景)进行了举例分析。这种方法会有耦合,在业务改动后编译时会报错。但是,回到我们上面使用的Manager类,同样可以实现。针对Mrpeak的分析,反革命攻城狮CasaTaloyum对其做了一些回应,具体可以参考《关于MrPeak的组件化文章的回应》。
基于上面的分析,我们在具体的项目中也实现了Protocol的相关逻辑,但更多使用的是在一个模块内的逻辑,如首页复杂的实现逻辑等,采用工厂模式+代理模式实现。
结合Casa和Limboy的解决方案,Bang的实现方式对于不具备业务场景的功能模块的实现有很好的通用性。但是,如果要实现统跳协议,这种实现逻辑就有些捉襟见肘了。统跳协议的内容可以参考天猫的《解耦神器 —— 统跳协议和Rewrite引擎》。我们的目的是合理的实现功能,所以在具体的使用中并不局限于一种方式。相反的,结合不同处理逻辑的优缺点,在项目的构建时都做了对应的处理。