最近完成了我司iOS项目的重构,把总体的代码架构都梳理了一遍,主要按照MVP的架构模式,并综合考虑了重构的难度和效果。在这个过程当中也积累了一些代码重构方面的经验,在这里总结一下。数据库
首先简单介绍一下项目状况。咱们原有项目的架构是比较标准的MVC模式,也是苹果官方推荐的架构模式。Model层用来表示实体类,View层负责界面展现和传递UI事件,Controller层负责大部分的业务逻辑。除此以外,对一部分公共的可复用的逻辑,咱们抽象出Service层,提供给Controller使用,另外网络层也独立出来。下图比较清楚地展现了总体架构 编程
MVC架构做为苹果官方推荐的架构模式,把数据Model和展示View经过Controller层隔离开,在项目规模较小的时候是一个不错的选择。随着项目复杂性的提升,咱们也渐渐感受到MVC模式的弊端,主要体如今下面几个方面设计模式
Controller处理业务逻辑,处理UI更新,处理UI事件,同步Model层,咱们几乎全部的代码都写在了Controller层。设计模式里有单一模式原则,你看这里的Controller层已经至少有四种职责了。bash
这一点一方面是由于Cocoa框架里的Controller层,就是咱们最熟悉的UIViewController
和View是自然耦合的,不少view的生命周期方法如viewWillAppear
都存在于VC,另外一方面咱们不少时候也习惯于把UI操做甚至初始化操做放在VC里,致使UI和业务逻辑混杂在一块儿。当你想对业务逻辑编写单元测试的时候,看着业务逻辑代码里混杂的UI操做,就知道什么叫举步维艰——数据能够Mock,UI是不可能被Mock的。网络
当一个界面功能比较复杂的时候,咱们全部的逻辑代码都会堆积在Controller中,好比咱们原有的WebViewController
的代码就多达5000行,在这种状况下维护代码简直是如履薄冰。架构
对于Controller层过于臃肿的问题,MVP模式则能较好地解决这个问题——既然UIViewController
和UIView
是耦合的,索性把这二者都归为View层,业务逻辑则独立存在于Presenter层,Model层保持不变。下图比较清除得展现了MVP模式的结构 app
在MVP模式下,Controller层和View层已经合并为View层,专门负责处理UI更新和事件传递,Model层仍是做为实体类。本来写在ViewController层的业务逻辑已经迁移到Presenter中。MVP模式较好地解决了Controller层职责过多的问题。框架
Presenter层主要处理业务逻辑,ViewController层实现Presenter提供的接口,Presenter经过接口去更新View,这样就实现了业务逻辑和UI解耦。若是咱们要编写单元测试的话,只须要Mock一个对象实现Presenter提供的接口就行了。MVP模式较好地解决了UI和逻辑的解耦。单元测试
经过把业务逻辑迁移到Presenter层,Controller层的困境彷佛获得了解决,可是若是某个需求逻辑较为复杂,单纯的把业务逻辑迁移解决不了根本的问题,Presenter层也会存在大量业务逻辑代码,维护困难。这个问题,咱们下面会讨论如何解决。学习
这里主要是考虑界面间跳转的代码如何重构,这一点我在以前的文章里已经有提到了,这里给个连接iOS重构之面向协议编程实践,另外附图一张
前面咱们提到,MVP模式虽然能解决许多MVC模式下存在的问题,但对于比较复杂的需求,仍是会存在逻辑过于复杂,Presenter层也出现难以维护的问题。下面咱们就经过一个实际的例子,来看看面对复杂的业务逻辑,咱们应该如何去设计和实现。
不少复杂的需求,在最初都是从一个简单的场景,一步步往上增长功能。在这个过程当中,若是不持续的进行优化和重构,到最后就成了所谓的"只有上帝能看懂的代码"。说了这么多,进入正题,来看这个需求。
实现一个简单的单文件上传,文件的索引存储在数据库中,文件存储在App的沙箱里面。这个应该对于有经验的客户端开发者来讲是小菜一碟,比较简单也容易实现。咱们能够把这个需求大体拆分红如下几个子需求
以上几项若是使用传统的MVC模式,实现起来以下图所示
UploadViewController
中实现,目前需求仍是比较简单的情形下面,仍是勉强可以接受,也不须要更多的思考。若是使用MVP的模式进行优化,以下图所示
如今UploadPresenter
负责处理上传逻辑了,而UploadViewController
专一于UI更新和事件传递,总体的结构更加清晰,之后维护代码也会比较方便。
需求来了!须要在原来的基础上支持多文件上传,意味着咱们多了一个子需求
很显然,咱们须要在UploadPresenter
中增长一个维护上传队列的功能,最初我也确实是这样实现的,可是因为文件上传须要监听的事件比较多,回调也比较频繁,直接在Presenter中继续写这样的逻辑代码,已经成倍增长了代码的复杂性。
因此通过一番思考,我考虑把文件上传这部分的逻辑单独提取出一层FileUploader
,而UploadPresenter
只负责维护FileUploader
的队列以及检查网络状态。具体的实现以下所示。
原来咱们的上传文件的来源是存在于App沙箱里的,咱们经过数据库查询能够找到这个文件的索引和路径,进而获取到这个文件进行上传。如今万恶的需求又来了,须要支持上传系统相册中获取的图片/视频。
到这里可能有些读者已经有点头大了,若是没有仔细思考,极可能从这里就走向了代码质量崩溃的道路。
这个时候,咱们就要思考,他们是多来源,可是对于FileUploader来讲,它其实不关心模型的来源,它只须要获取到模型的二进制流。因而,咱们能够抽象出一个BaseModel
,提供一个stream
只读属性,两种来源分别继承BaseModel
,各自重载stream
只读属性,实现本身的构造文件stream
的方法。对于FileUploader
来讲,它只持有BaseModel
便可,这就是继承和多态的一个典型的使用场景。
若是后续还有更多来源的文件,好比网络文件(先下载再上传?),也只须要继续继承BaseModel
,重载stream
便可,对于FileUploader
和它的全部上层来讲,一切都是透明的,无需进行修改。通过这样的设计,咱们的代码的可维护性和可扩展性又好了。下面是架构图。
在HTTP文件上传中,咱们能够直接上传文件的二进制流,这种就须要服务端作特定的支持。但更为经常使用和支持普遍的作法是使用HTTP表单文件传输,即组装HTTP请求的body时采用multipart/form-data
的标准组装,传输数据。因而,咱们又多了一个需求:
思路和刚才的多来源上传差很少,咱们把上面的两种来源的模型,即FSBaseM
和ABaseM
抽象为父类,父类含有各自的文件二进制数据的抽象,子类分别实现二进制直接组装流,和按multipart/form-data
格式组装流,实现以下图。
刚才咱们的文件上传,底层的协议是基于Http,此时咱们须要支持FTP/Socket协议的传输,应该怎么办?
通过上面的思考,相信你必定知道该怎么作了。这里留个思考,答案请戳这里MVP_V5架构
最后,咱们把目前的需求全都整理一下
咱们看看,若是分别采用MVC、MVP_V一、MVP_V二、MVP_V三、MVP_V四、MVP_V5,来实现目前的十个需求,咱们的代码大体会分布在哪几层。
孰优孰劣一目了然。若是采用最原始的MVC模式的话,保守估计ViewController
代码量至少3K行以上。
在此次的项目重构中,我也总结了一些重构方面的技巧和贴士,但愿能帮助到想开始进行代码重构的同窗
大段重复的代码出现了三次或以上 ——提取成一个公共的方法 这一点是最多见也最容易作到的,只要在平时的编码过程当中养成这种习惯,对于出现过三次以上重复代码段,提取成一个公共方法。
一个类的职责有三种或以上 ——经过合理分层的方式,减小职责 这一点在上面的例子中已经阐述地比较清楚了,经过职责的分层,上层持有下层,下层经过接口与上层通信。其实这也是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 "皇帝柑"
}
}
复制代码
Notification
便可。最后我想谈谈设计模式。其实重构的过程其实也就是灵活运用设计模式对代码进行优化和改进。不少人设计模式也看了不少,学习了不少,但真正在工做中能合理使用的却不多。因此关键还在灵活运用四个字上,能作到这一点,你的水平就会上一个台阶。
因此在平时的工做中,咱们要有对代码的Taste,知道什么样的是好代码,什么样的是脏代码,尽早发现可优化可改进的地方,持续产出高质量代码,而不是实现功能就万事大吉,不然早晚要为你之前偷的懒买单。 以上就是我在我司项目重构过程当中的的一些总结和分享,水平有限,但愿对你们有所帮助。