背景
可能大多数项目在最初开发时期,都是处于一个”一锅乱炖“的状态,UI、功能等代码糅合在一起。随着项目的发展,重构就成了必经之路。其实无论是重构还是一开始开发,我们在设计一个项目时,都需要考虑到其灵活性、复用性、复杂度、耦合度和可维护性等。
模块化顾名思义就是将一个复杂的系统分解为多个模块。降低复杂性,降低代码的耦合度,部署方便,提高效率。目前市面上已经有许多优质的模块化方案,本文将对主流的几个模块化方案进行分析探究。
- 模块化的意义
- 适用于基础功能稳定、项目规模较大
- 项目变大,编译时间长,基础模块的产品间复用
- 多团队发布、集成、测试不变,协同开发互相依赖和冲突
设计模式原则
在了解模块化方案之前,还是先回顾一下设计模式的六大原则。
-
单一原则:一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化。(业务对象、业务逻辑拆分)
-
里氏替换原则:子类可以扩展父类的功能,但不能改变原有父类的功能。(目的:增强程序的健壮性)实际项目中,每个子类对应不同的业务含义,使父类作为参数,传递不同的子类完成不同的业务逻辑。
-
依赖倒置原则:面向接口编程;(通过接口或抽象类作为参数实现应用场景)
1. 上层模块不应该依赖下层模块,两者应依赖其抽象;
2. 抽象不应该依赖实现类,实现类应该依赖抽象
- 接口隔离原则:建立单一接口;(扩展为类也是一种接口,一切皆接口。复杂的接口,根据业务拆分成多个简单接口)
1. 客户端不应该依赖它不需要的接口;
2. 类之间依赖关系应该建立在最小的接口上;
-
迪米特原则:最少知道原则,尽量降低类与类之间的耦合;一个对象应该对其他对象有最少的了解
-
开闭原则:用抽象构建架构,用实现扩展原则。
模块划分
从整体架构层面和业务解耦层面考虑,基本上工程后期都会发展成如下结构:(该图仅展示基础结构)

如上图所示,组件化拆分层次如下
-
通用模块:通用工具。如网络,文件处理等
-
基础功能模块:按功能分库,不涉及产品业务需求
-
业务模块 + 接口:业务功能间相对独立,相互之间没有依赖;业务之间的逻辑Action调用只能通过中间件提供;(中间件方案下面会详细探究)
-
基础UI组件:各个业务模块依赖使用,需要保持好定制扩展的设计
-
App
中间件方案
常见中间件方案
-
基于路由 URL 的 UI 页面统跳管理
-
基于反射的远程接口调用封装
-
基于面向协议思想的服务注册方案
-
基于通知的广播方案
上述4点参考自iOS 组件化/模块化架构设计实践
| URL Scheme | Target Action | Protocol - Class | NSNotificationCenter |
| --- | --- | --- | --- |
| - 使URL处理本地的跳转
-
通过中间层进行注册&调用
-
注册表无需使用反射
-
非懒加载/注册表的维护/参数 | - 抽离业务逻辑
-
通过中间层进行调用
-
中间层使用runtime反射 | 增加Prototol Wrapper层
-
中间件返回Protocol对应的Class
-
解决硬编码的问题 | - 基于系统的 NSNotificationCenter- 作为前面几种方案的补充 |
基于路由 URL 的 UI 页面统跳管理
-
基本原理:将请求、功能实现等放至web界面
-
应用场景:VC解耦
-
优势
- 便于实现多端统一
- 动态性
- 劣势
- 交互场景偏简单
- 复杂参数难以传递
// example
let urlString = "<https://www.baidu.com>"
if let urlRequest = URLRequest(urlString: urlString) {
view.load(urlRequest)
}
基于反射的远程接口调用封装
-
我们知道OC是一门动态语言,Runtime是我们可以动态获取一个类的方法和属性。当然swift也是有Mirror来实现反射,此处主要以OC的反射机制来举例。
-
当我们需要调用某个类的方法,但无法直接import其头文件时,就会用到反射机制。如下所示:
Class module = NSClassFromString(@"GoodsModule");
NSArray *list = [manager performSelector:@selector(getGoodsList)];
-
如果直接这样调用,容易出现拼写错误,且难以排查,所以常见的解决方案是写一个消息转发层来统一封装方法调用。如大名鼎鼎的CTMediator就是用了这种方案。
基于面向协议思想的服务注册方案
-
方案一:通过服务注册的方式来实现远程接口调用。每个模块提供自己对外服务的协议声明,然后将此声明注册到中间层。调用方能从中间层看到存在哪些服务接口,然后直接进行调用即可。
-
方案二:将功能协议和回调协议暴露给调用方,调用方可通过注册回调的方式监听数据的变化。如下所示
@objc public protocol UserContext: NSObjectProtocol {
func getLocalUserInfo() -> UserInfo
func registerUserEventHandler(_ handler: UserHandler)
func unregisterUserEventHandler(_ handler: UserHandler)
}
@objc public protocol UserHandler: NSObjectProtocol {
@objc optional func onRemoteUserJoined(user: UserInfo)
}
-
优势:协议的所有实现仍然在模块内部,所以不需要写反射代码。同时对外暴露的只有协议,符合团队协作的“面向协议编程”的思想。
-
劣势:
- 如果服务提供方和使用方依赖的是公共模块中的同一份协议(protocol), 当协议内容改变时,会存在所有服务依赖模块编译失败的风险。(方案一)
- 需要一个注册过程,将 Protocol 协议与具体实现绑定起来。(方案一)
基于通知的广播方案
-
基于系统的 NSNotificationCenter。
-
模块化通讯方案中,更多的是把通知方案作为以上几种方案的补充。
-
优势
- 实现简单,非常适合处理一对多的通讯场景。
- 劣势
- 仅适用于简单通讯场景。
- 复杂数据传输,同步调用等方式都不太方便。