最近完成了我司 iOS 项目的重构, 把整体的代码架构都梳理了一遍, 主要按照 MVP 的架构模式, 并综合考虑了重构的难度和效果在这个过程中也积累了一些代码重构方面的经验, 在这里总结一下
项目简介和 MVP 模式重构
项目简介
首先简单介绍一下项目情况我们原有项目的架构是比较标准的 MVC 模式, 也是苹果官方推荐的架构模式 Model 层用来表示实体类, View 层负责界面展示和传递 UI 事件, Controller 层负责大部分的业务逻辑除此之外, 对一部分公共的可复用的逻辑, 我们抽象出 Service 层, 提供给 Controller 使用, 另外网络层也独立出来下图比较清楚地展示了整体架构
整体架构
MVC 模式的问题
MVC 架构作为苹果官方推荐的架构模式, 把数据 Model 和展现 View 通过 Controller 层隔离开, 在项目规模较小的时候是一个不错的选择随着项目复杂性的提高, 我们也渐渐感觉到 MVC 模式的弊端, 主要体现在下面几个方面
Controller 层职责过多, Model 和 View 层太简单
Controller 处理业务逻辑, 处理 UI 更新, 处理 UI 事件, 同步 Model 层, 我们几乎所有的代码都写在了 Controller 层设计模式里有单一模式原则, 你看这里的 Controller 层已经至少有四种职责了
业务逻辑和 UI 混杂在一起, 难以编写单元测试
这一点一方面是因为 Cocoa 框架里的 Controller 层, 就是我们最熟悉的 UIViewController 和 View 是天然耦合的, 很多 view 的生命周期方法如 viewWillAppear 都存在于 VC, 另一方面我们很多时候也习惯于把 UI 操作甚至初始化操作放在 VC 里, 导致 UI 和业务逻辑混杂在一起当你想对业务逻辑编写单元测试的时候, 看着业务逻辑代码里混杂的 UI 操作, 就知道什么叫举步维艰数据可以 Mock,UI 是不可能被 Mock 的
业务逻辑代码大量存在于 Controller 层, 维护困难
当一个界面功能比较复杂的时候, 我们所有的逻辑代码都会堆积在 Controller 中, 比如我们原有的 webViewController 的代码就多达 5000 行, 在这种情况下维护代码简直是如履薄冰
MVP 模式的重构
对于 Controller 层过于臃肿的问题, MVP 模式则能较好地解决这个问题既然 UIViewController 和 UIView 是耦合的, 索性把这两者都归为 View 层, 业务逻辑则独立存在于 Presenter 层, Model 层保持不变下图比较清除得展示了 MVP 模式的结构
MVP 模式简介
我们来看一下 MVP 模式能否解决 MVC 模式存在的问题
Controller 层职责过多, Model 和 View 层太简单
在 MVP 模式下, Controller 层和 View 层已经合并为 View 层, 专门负责处理 UI 更新和事件传递, Model 层还是作为实体类原本写在 ViewController 层的业务逻辑已经迁移到 Presenter 中 MVP 模式较好地解决了 Controller 层职责过多的问题
业务逻辑和 UI 混杂在一起, 难以编写单元测试
Presenter 层主要处理业务逻辑, ViewController 层实现 Presenter 提供的接口, Presenter 通过接口去更新 View, 这样就实现了业务逻辑和 UI 解耦如果我们要编写单元测试的话, 只需要 Mock 一个对象实现 Presenter 提供的接口就好了 MVP 模式较好地解决了 UI 和逻辑的解耦
业务逻辑代码大量存在于 Controller 层, 维护困难
通过把业务逻辑迁移到 Presenter 层, Controller 层的困境似乎得到了解决, 但是如果某个需求逻辑较为复杂, 单纯的把业务逻辑迁移解决不了根本的问题, Presenter 层也会存在大量业务逻辑代码, 维护困难这个问题, 我们下面会讨论如何解决
MVC 模式改进 Router 模式
这里主要是考虑界面间跳转的代码如何重构, 这一点我在之前的文章里已经有提到了, 这里给个链接 iOS 重构之面向协议编程实践, 另外附图一张
Router 模式
实例分析
前面我们提到, MVP 模式虽然能解决许多 MVC 模式下存在的问题, 但对于比较复杂的需求, 还是会存在逻辑过于复杂, Presenter 层也出现难以维护的问题下面我们就通过一个实际的例子, 来看看面对复杂的业务逻辑, 我们应该如何去设计和实现
很多复杂的需求, 在最初都是从一个简单的场景, 一步步往上增加功能在这个过程中, 如果不持续的进行优化和重构, 到最后就成了所谓的 "只有上帝能看懂的代码" 说了这么多, 进入正题, 来看这个需求
V1.0 单文件上传
实现一个简单的单文件上传, 文件的索引存储在数据库中, 文件存储在 App 的沙箱里面这个应该对于有经验的客户端开发者来说是小菜一碟, 比较简单也容易实现我们可以把这个需求大致拆分成以下几个子需求
初始化上传 View
更新上传 View
点击上传按钮事件
数据库中获取上传模型
发起 HTTP 请求上传文件
检查网络状态
以上几项如果使用传统的 MVC 模式, 实现起来如下图所示
MVC
我们可以看到上述需求基本都直接在
UploadViewController
中实现, 目前需求还是比较简单的情形下面, 还是勉强能够接受, 也不需要更多的思考如果使用 MVP 的模式进行优化, 如下图所示
MVP.png
现在 UploadPresenter 负责处理上传逻辑了, 而
UploadViewController
专注于 UI 更新和事件传递, 整体的结构更加清晰, 以后维护代码也会比较方便
V2.0 多文件上传
需求来了! 需要在原来的基础上支持多文件上传, 意味着我们多了一个子需求
维护上传模型队列
很显然, 我们需要在 UploadPresenter 中增加一个维护上传队列的功能, 最初我也确实是这样实现的, 但是由于文件上传需要监听的事件比较多, 回调也比较频繁, 直接在 Presenter 中继续写这样的逻辑代码, 已经成倍增加了代码的复杂性
所以经过一番思考, 我考虑把文件上传这部分的逻辑单独提取出一层 FileUploader, 而 UploadPresenter 只负责维护 FileUploader 的队列以及检查网络状态具体的实现如下所示
MVP2.png
我们可以看到, 分层之后的结构又更加清晰了, 每一层的职责都比较单一, 目前看起来一切 OK!
V3.0 多来源上传
原来我们的上传文件的来源是存在于 App 沙箱里的, 我们通过数据库查询可以找到这个文件的索引和路径, 进而获取到这个文件进行上传现在万恶的需求又来了, 需要支持上传系统相册中获取的图片 / 视频
支持系统相册和 App 沙箱中获取文件
到这里可能有些读者已经有点头大了, 如果没有仔细思考, 很可能从这里就走向了代码质量崩溃的道路
这个时候, 我们就要思考, 他们是多来源, 但是对于 FileUploader 来说, 它其实不关心模型的来源, 它只需要获取到模型的二进制流于是, 我们可以抽象出一个 BaseModel, 提供一个 stream 只读属性, 两种来源分别继承 BaseModel, 各自重载 stream 只读属性, 实现自己的构造文件 stream 的方法对于 FileUploader 来说, 它只持有 BaseModel 即可, 这就是继承和多态的一个典型的使用场景
如果后续还有更多来源的文件, 比如网络文件 (先下载再上传?), 也只需要继续继承 BaseModel, 重载 stream 即可, 对于 FileUploader 和它的所有上层来说, 一切都是透明的, 无需进行修改经过这样的设计, 我们的代码的可维护性和可扩展性又好了下面是架构图
MVP3.png
V4.0 多方式上传
在 HTTP 文件上传中, 我们可以直接上传文件的二进制流, 这种就需要服务端做特定的支持但更为常用和支持广泛的做法是使用 HTTP 表单文件传输, 即组装 HTTP 请求的 body 时采用 multipart/form-data 的标准组装, 传输数据于是, 我们又多了一个需求:
支持表单传输和流传输
思路和刚才的多来源上传差不多, 我们把上面的两种来源的模型, 即 FSBaseM 和 ABaseM 抽象为父类, 父类含有各自的文件二进制数据的抽象, 子类分别实现二进制直接组装流, 和按 multipart/form-data 格式组装流, 实现如下图
MVP4.png
V5.0 支持 FTP/Socket 上传
刚才我们的文件上传, 底层的协议是基于 Http, 此时我们需要支持 FTP/Socket 协议的传输, 应该怎么办?
支持 HTTP/FTP/Socket
经过上面的思考, 相信你一定知道该怎么做了这里留个思考, 答案请戳这里 MVP_V5 架构
对比
最后, 我们把目前的需求全都整理一下
初始化上传 View
更新上传 View
点击上传按钮事件
数据库中获取上传模型
发起 HTTP 请求上传文件
检查网络状态
维护上传模型队列
支持系统相册和 App 沙箱中获取文件
支持表单传输和流传输
支持 HTTP/FTP/Socket
我们看看, 如果分别采用 MVCMVP_V1MVP_V2MVP_V3MVP_V4MVP_V5, 来实现目前的十个需求, 我们的代码大致会分布在哪几层
优化后的架构模式之间的比较
孰优孰劣一目了然如果采用最原始的 MVC 模式的话, 保守估计 ViewController 代码量至少 3K 行以上
总结
运用 MVP 的设计模式, 逻辑和 UI 操作解耦
分层模式, 上层拥有下层, 下层通过接口与上层通信, 达到解耦
利用继承和多态, 屏蔽底层实现的细节, 达到职责分离和高扩展性
代码优化和重构的技巧
在这次的项目重构中, 我也总结了一些重构方面的技巧和贴士, 希望能帮助到想开始进行代码重构的同学
事不过三
大段重复的代码出现了三次或以上
提取成一个公共的方法
这一点是最常见也最容易做到的, 只要在平时的编码过程中养成这种习惯, 对于出现过三次以上重复代码段, 提取成一个公共方法
一个类的职责有三种或以上
通过合理分层的方式, 减少职责
这一点在上面的例子中已经阐述地比较清楚了, 通过职责的分层, 上层持有下层, 下层通过接口与上层通讯其实这也是 MVP 模式的本质
同类的 if/else 出现了三次或以上
考虑使用抽象类和多态代替 if/else
如果相同的 if/else 判断在你的代码中出现了很多次的话, 则应该考虑设计一个抽象类去替代这些判断这里可能有点难以理解, 举个例子就好懂很多
比如, 现在我们有一个水果类, 有三种水果, 水果有颜色价钱和品种
- class Fruit {
- var name: String = ""func getColor() - >UIColor ? {
- if name == "apple" {
- return UIColor.red
- } else if name == "banana" {
- return UIColor.yellow
- } else if name == "orange" {
- return UIColor.orange
- }
- return nil
- }
- func getPrice() - >Float ? {
- if name == "apple" {
- return 10
- } else if name == "banana" {
- return 20
- } else if name == "orange" {
- return 30
- }
- return nil
- }
- func getType() - >String ? {
- if name == "apple" {
- return "红富士"
- } else if name == "banana" {
- return "芭蕉"
- } else if name == "orange" {
- return "皇帝"
- }
- return nil
- }
- }
这里的对名称 name 的相同的 if/else 判断出现了三次, 如果此时我们多了一种水果梨, 我们得修改上述所有的 if/else 判断, 这样就会非常难维护
这种场景我们可以考虑抽象出一个 Fruit 的抽象类 / 接口 / 协议, 通过实现水果类 / 接口 / 协议的方式, 此时如果多了一种水果, 让这种水果继续实现 Fruit 协议就行, 这样我们就通过新增的方式替代修改, 提高了代码的可维护性
- protocol Fruit {
- func getPrice() - >Float ? func getType() - >String ? func getColor() - >UIColor ?
- var name: String {
- get
- }
- }
- class Apple: Fruit {
- var name: String = "apple"func getColor() - >UIColor ? {
- return UIColor.red
- }
- func getPrice() - >Float ? {
- return 10
- }
- func getType() - >String ? {
- return "红富士"
- }
- }
- class Banana: Fruit {
- var name: String = "banana"func getColor() - >UIColor ? {
- return UIColor.yellow
- }
- func getPrice() - >Float ? {
- return 20
- }
- func getType() - >String ? {
- return "芭蕉"
- }
- }
- class Orange: Fruit {
- var name: String = "orange"func getColor() - >UIColor ? {
- return UIColor.orange
- }
- func getPrice() - >Float ? {
- return 30
- }
- func getType() - >String ? {
- return "皇帝柑"
- }
- }
合理分层
纵向分层层级之间有关联
上层持有下层, 下层通过接口与上层通信这里为什么不让下层也持有上层呢? 主要还是为了能够解耦, 下层设计的目的是为上层服务的, 它不应该依赖上层这种设计模式在计算机科学中是很常见的, 比如计算机网络中的网络分层设计
横向分层层级之间无关联
适用于功能相对独立的模块, 简单划分即可我们的 iOS 项目的首页就是由好几个部分组成, 这个部分之间无太多的关联, 我们简单划分成几个模块就行如果出现了少数需要通讯的场景, 使用 Notification 即可
避免过度设计
越简单的越是有效的
复杂的架构设计往往在客户端高速迭代开发中意义不大 (相比服务端)
没有银弹!
软件开发是工程化的, 没有完美的架构模式, 很多时候需要具体问题具体分析, 灵活运用设计模式, 得到局部的最优解比如前面提到的 MVP 模式, 如果生搬硬套, 同样无法解决 Presenter 层复杂的问题
如何判断过度设计?
胶水代码过多
大量文件的行数小于 100
想了一天, 没写出代码, 也没写出架构方案
重构的时机和对象
时机
单文件代码行数开始超过 500 行的时候
Code Review 是重构的好帮手
对象
需求经常变化或增加的功能, 一定要注意设计, 避免走向质量不可控
稳定且不变的功能, 不重构
总结
最后我想谈谈设计模式其实重构的过程其实也就是灵活运用设计模式对代码进行优化和改进很多人设计模式也看了很多, 学习了很多, 但真正在工作中能合理使用的却很少所以关键还在灵活运用四个字上, 能做到这一点, 你的水平就会上一个台阶
所以在平时的工作中, 我们要有对代码的 Taste, 知道什么样的是好代码, 什么样的是脏代码, 尽早发现可优化可改进的地方, 持续产出高质量代码, 而不是实现功能就万事大吉, 否则迟早要为你以前偷的懒买单 以上就是我在我司项目重构过程中的的一些总结和分享, 水平有限, 希望对大家有所帮助
来源: http://www.jianshu.com/p/61dc0586874f