【iOS进阶】模块化方案探究

407 阅读6分钟

背景

可能大多数项目在最初开发时期,都是处于一个”一锅乱炖“的状态,UI、功能等代码糅合在一起。随着项目的发展,重构就成了必经之路。其实无论是重构还是一开始开发,我们在设计一个项目时,都需要考虑到其灵活性、复用性、复杂度、耦合度和可维护性等。

模块化顾名思义就是将一个复杂的系统分解为多个模块。降低复杂性,降低代码的耦合度,部署方便,提高效率。目前市面上已经有许多优质的模块化方案,本文将对主流的几个模块化方案进行分析探究。

  • 模块化的意义

    - 适用于基础功能稳定、项目规模较大

    - 项目变大,编译时间长,基础模块的产品间复用

    - 多团队发布、集成、测试不变,协同开发互相依赖和冲突

设计模式原则

在了解模块化方案之前,还是先回顾一下设计模式的六大原则。

  1. 单一原则:一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化。(业务对象、业务逻辑拆分)

  2. 里氏替换原则:子类可以扩展父类的功能,但不能改变原有父类的功能。(目的:增强程序的健壮性)实际项目中,每个子类对应不同的业务含义,使父类作为参数,传递不同的子类完成不同的业务逻辑。

  3. 依赖倒置原则:面向接口编程;(通过接口抽象类作为参数实现应用场景)

    1. 上层模块不应该依赖下层模块,两者应依赖其抽象;

    2. 抽象不应该依赖实现类,实现类应该依赖抽象

  1. 接口隔离原则:建立单一接口;(扩展为类也是一种接口,一切皆接口。复杂的接口,根据业务拆分成多个简单接口)

    1. 客户端不应该依赖它不需要的接口;

    2. 类之间依赖关系应该建立在最小的接口上;

  1. 迪米特原则:最少知道原则,尽量降低类与类之间的耦合;一个对象应该对其他对象有最少的了解

  2. 开闭原则:用抽象构建架构,用实现扩展原则。

模块划分

从整体架构层面和业务解耦层面考虑,基本上工程后期都会发展成如下结构:(该图仅展示基础结构)

https://oscimg.oschina.net/oscnet/up-dd3a4bfffac5f97624e9e0bb3529cfe4dab.png

如上图所示,组件化拆分层次如下

  • 通用模块:通用工具。如网络,文件处理等

  • 基础功能模块:按功能分库,不涉及产品业务需求

  • 业务模块 + 接口:业务功能间相对独立,相互之间没有依赖;业务之间的逻辑Action调用只能通过中间件提供;(中间件方案下面会详细探究)

  • 基础UI组件:各个业务模块依赖使用,需要保持好定制扩展的设计

  • App

中间件方案

常见中间件方案

  1. 基于路由 URL 的 UI 页面统跳管理

  2. 基于反射的远程接口调用封装

  3. 基于面向协议思想的服务注册方案

  4. 基于通知的广播方案

上述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就是用了这种方案。

基于面向协议思想的服务注册方案

  1. 方案一:通过服务注册的方式来实现远程接口调用。每个模块提供自己对外服务的协议声明,然后将此声明注册到中间层。调用方能从中间层看到存在哪些服务接口,然后直接进行调用即可。

  2. 方案二:将功能协议和回调协议暴露给调用方,调用方可通过注册回调的方式监听数据的变化。如下所示


@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。

  • 模块化通讯方案中,更多的是把通知方案作为以上几种方案的补充。

  • 优势

    - 实现简单,非常适合处理一对多的通讯场景。

  • 劣势

    - 仅适用于简单通讯场景。

    - 复杂数据传输,同步调用等方式都不太方便。