目录
MVC模式的问题
下图是比较标准的MVC模式,也是苹果官方推荐的架构模式。
Model层用来表示实体类,View层负责界面展示和传递UI事件,Controller层负责大部分的业务逻辑。
除此之外,对一部分公共的可复用的逻辑,我们抽象出Service层,提供给Controller使用,另外网络层也独立出来。下图比较清楚地展示了整体架构
根据上篇对MVC模式的介绍,网络层,公共服务,持久化,也可以放在Model层.
MVC架构作为苹果官方推荐的架构模式,把数据Model和展现View通过Controller层隔离开,在项目规模较小的时候是一个不错的选择。随着项目复杂性的提高,MVC模式的弊端就会显现
Controller层职责过多,Model和View层太简单
Controller处理业务逻辑,处理UI更新,处理UI事件,同步Model层,我们几乎所有的代码都写在了Controller层。 设计模式里有单一模式原则,你看这里的Controller层已经至少有四种职责了。
业务逻辑(网络请求,数据持久化)和UI(UI点击事件监听,UI更新)混杂在一起,难以编写单元测试
这一点一方面是因为Cocoa框架里的Controller层,就是我们最熟悉的UIViewController和View是天然耦合的,很多view的生命周期方法如viewWillAppear都存在于VC,另一方面我们很多时候也习惯于把UI操作甚至初始化操作放在VC里,导致UI和业务逻辑混杂在一起。当你想对业务逻辑编写单元测试的时候,看着业务逻辑代码里混杂的UI操作,就知道什么叫举步维艰——数据可以Mock,UI是不可能被Mock的。
业务逻辑代码大量存在于Controller层,维护困难
当一个界面功能比较复杂的时候,我们所有的逻辑代码都会堆积在Controller中,比如有的ViewController的代码就多达5000行,在这种情况下维护代码简直是如履薄冰。
MVP模式
一 介绍
对于Controller层过于臃肿的问题,MVP模式则能较好地解决这个问题
既然UIViewController和UIView是耦合的,索性把这两者都归为View层,业务逻辑则独立存在于Presenter层,Model层保持不变。下图比较清除得展示了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层也会存在大量业务逻辑代码,维护困难。这个问题,我们可以通过合理分层的方式解决,下面会讨论如何解决。
二 通过一个实例来讲解
前面我们提到,MVP模式虽然能解决许多MVC模式下存在的问题,但对于比较复杂的需求,还是会存在逻辑过于复杂,Presenter层也出现难以维护的问题。下面我们就通过一个实际的例子,来看看面对复杂的业务逻辑,我们应该如何去设计和实现。 很多复杂的需求,在最初都是从一个简单的场景,一步步往上增加功能。在这个过程中,如果不持续的进行优化和重构,到最后就成了所谓的”只有上帝能看懂的代码”。说了这么多,进入正题,来看这个需求。
V1.0 单文件上传
实现一个简单的单文件上传,文件的索引存储在数据库中,文件存储在App的沙箱里面。这个应该对于有经验的客户端开发者来说是小菜一碟,比较简单也容易实现。我们可以把这个需求大致拆分成以下几个子需求
1.初始化上传View
2.更新上传View
3.点击上传按钮事件
4.数据库中获取上传模型
5.发起HTTP请求上传文件
6.检查网络状态
以上几项如果使用传统的MVC模式,实现起来如下图所示
我们可以看到上述需求基本都直接在UploadViewController中实现,目前需求还是比较简单的情形下面,还是勉强能够接受,也不需要更多的思考。如果使用MVP的模式进行优化,如下图所示
现在UploadPresenter负责处理上传逻辑了,而UploadViewController专注于UI更新和事件传递,整体的结构更加清晰,以后维护代码也会比较方便。
V2.0 多文件上传
需求来了!需要在原来的基础上支持多文件上传,意味着我们多了一个子需求
7.维护上传模型队列
很显然,我们需要在UploadPresenter中增加一个维护上传队列的功能,最初也确实是这样实现的,但是由于文件上传需要监听的事件比较多,回调也比较频繁,直接在Presenter中继续写这样的逻辑代码,已经成倍增加了代码的复杂性。 所以经过一番思考,我考虑把文件上传这部分的逻辑单独提取出一层FileUploader,而UploadPresenter只负责维护FileUploader的队列以及检查网络状态。具体的实现如下所示。
我们可以看到,分层之后的结构又更加清晰了,每一层的职责都比较单一,目前看起来一切OK!
V3.0 多来源上传
原来我们的上传文件的来源是存在于App沙箱里的,我们通过数据库查询可以找到这个文件的索引和路径,进而获取到这个文件进行上传。现在万恶的需求又来了,需要支持上传系统相册中获取的图片/视频。
8.支持系统相册和App沙箱中获取文件
到这里可能有些读者已经有点头大了,如果没有仔细思考,很可能从这里就走向了代码质量崩溃的道路。 这个时候,我们就要思考,他们是多来源,但是对于FileUploader来说,它其实不关心模型的来源,它只需要获取到模型的二进制流。于是,我们可以抽象出一个BaseModel,提供一个stream只读属性,两种来源分别继承BaseModel,各自重载stream只读属性,实现自己的构造文件stream的方法。对于FileUploader来说,它只持有BaseModel即可,这就是继承和多态的一个典型的使用场景。 如果后续还有更多来源的文件,比如网络文件(先下载再上传?),也只需要继续继承BaseModel,重载stream即可,对于FileUploader和它的所有上层来说,一切都是透明的,无需进行修改。经过这样的设计,我们的代码的可维护性和可扩展性又好了。下面是架构图。
V4.0 多方式上传
在HTTP文件上传中,我们可以直接上传文件的二进制流,这种就需要服务端做特定的支持。但更为常用和支持广泛的做法是使用HTTP表单文件传输,即组装HTTP请求的body时采用multipart/form-data的标准组装,传输数据。于是,我们又多了一个需求:
支持表单传输和流传输
思路和刚才的多来源上传差不多,我们把上面的两种来源的模型,即FSBaseM和ABaseM抽象为父类,父类含有各自的文件二进制数据的抽象,子类分别实现二进制直接组装流,和按multipart/form-data格式组装流,实现如下图。
V5.0 支持FTP/Socket上传
刚才我们的文件上传,底层的协议是基于Http,此时我们需要支持FTP/Socket协议的传输,应该怎么办?
支持HTTP/FTP/Socket
经过上面的思考,相信你一定知道该怎么做了。
对比
最后,我们把目前的需求全都整理一下
1.初始化上传View
2.更新上传View
3.点击上传按钮事件
4.数据库中获取上传模型
5.发起HTTP请求上传文件
6.检查网络状态
7.维护上传模型队列
8.支持系统相册和App沙箱中获取文件
9.支持表单传输和流传输
10.支持HTTP/FTP/Socket
我们看看,如果分别采用MVC、MVP_V1、MVP_V2、MVP_V3、MVP_V4、MVP_V5,来实现目前的十个需求,我们的代码大致会分布在哪几层。
总结
运用MVP的设计模式,逻辑和UI操作解耦
分层模式,上层拥有下层,下层通过接口与上层通信,达到解耦。
利用继承和多态,屏蔽底层实现的细节,达到职责分离和高扩展性
行者常至,为者常成!