一. 介绍
通过本文可以了解MVP+Clean架构那些事,以及如何使用此架构去构建和实现APP。
通常做一个APP,一般拿到需求构建项目的时候基本上采用的是mvc模式,可以不假思索,顺手拈来,且快而精简。
在编码的过程中基本上我们感觉不到MVC模式的层次感,除了数据层独立出来之外,UI和业务处理基本都是黏在一起的,虽然感觉不到,但是我们能很快的实现需求。
而MVP+Clean架构,区别于MVC模式,第一感就是你能体会到MVP+Clean模式带来的层次感。虽然在编码过程中多了很多步骤,但是它在单元测试以及后期需求变更的应对上,体现得更高大上。
二. 架构说明
1. 传统的MVC
界面和业务逻辑黏在一起,功能实现即是展示
2. MVP+Clean
图一为clean架构在Android上的体现,里层的不依赖于外层,里层核心功能部分可以完全脱离系统,当实现另一套UI时,只需要修改UI层即可
图二为mvl-clean具体的体现和实现,对于数据层、功能逻辑层、交互逻辑层、展示层可以独立进行测试。一般编写交互逻辑层测试用例即可,可以兼顾测试到交互以及业务功能逻辑。
图一
图二
3. MVP+Clean优缺点
通过上面架构图可以看得出,clean架构层次清晰,一层套一层,里层核心逻辑完全不依赖于外层的表现。
优点如下:
①. 易于测试
在MVC模式下,因为逻辑和UI黏在一起,想针对功能逻辑进行测试,都得从编写Android UI测试用例开始,我们知道编写Android UI测试是一件很烦的事情,在考虑各种交互的同时,还要兼顾各种逻辑数据处理的情况,而且需要在真机或者是模拟器上进行测试。
而在MVP-Clean模式下,我们可以抛开Android UI层的测试(毕竟UI层是经常变动的,编写UI测试工作量太大),可以直接编写功能逻辑的测试用例,而且不需要真机或模拟器。
②. UI及功能模块独立
模块之间耦合度低,添加和删除单独UI或功能模块更加容易
③. 代码复用性高
功能模块独立并单一,可直接复用。
④. 易于应对需求变更
UI层的改动,修改UI;功能变更,修改功能模块,对于需求变更的修改更具有针对性。
缺点如下:
①. 代码量增多
每一层之间基本上都是根据接口进行适配访问,需要定义很多接口以及适配器。
②. 需要考虑接口的通用性
编写层级之间的接口时,对于数据的传递以及适配,需要考虑充分,谨慎修改。因为修改接口,牵扯到他下一层的实现,定义接口时尽量抽象。
③. 相对而言开发进度慢
对于一个简单的app,考虑事情过多,影响开发进度;不适合开发一些尝试想法的app。
三. 实现
最好的体现和表达就是拿实践说话,下面就是针对于上面的架构,从需求分析到最终实现,以及后期需求变更的应对,做了一个demo。
其实现过程基本上是由里而外。
下面以网易云视频上传及相关信息为例:
需求
展示通过ShareREC c2d demo录制的本地视频文件列表
展示已经上传到网易云端的视频列表3. 上传本地视频文件到网易云端
架构设计
根据上面需求,采用MVP+Clean架构,来设计我们所需要实现的三个核心功能:获取本地视频、获取网络视频
、上传视频。
另外对于上传视频功能,为了体验更友好,我们需要显示上传进度,并且需要实现断点续传,那么我们就需要
缓存视频上传过程中的相关信息。
通过上面的分析,我们定义两个数据源获取类:本地视频数据源LocalVideoRepo、云端视频数据源
NetVideoRepo
对应需要的实体对象:本地视频信息LocalVideoInfo、网络视频信息NetVideoInfo、视频上传信息UploadInfo
功能逻辑事件处理:获取本地视频GetLocalVideoCase、获取云端视频GetNetVideoCase、上传视频
UploadVideoCase(此步骤均在线程池中操作)
交互逻辑层:处理本地视频相关交互及逻辑处理LocalVideoPresenter、云端视频LocalVideoPresenter、
视频上传UploadVideoPresenter(针对此步骤编写单元测试用例)
界面:MainActivity、LocalVideoFragment、NetVideoFragment
根据上面说明,其核心内容结构设计图如下
3. 数据层开发
由于数据源来自于网络和本地、并且还需要缓存上传信息(这里我们将上传信息缓存到本地)
将具体实现部分独立出来(方便后面修改和测试等,后面测试部分会提及到),所以定义了两个基本功能接口NetInterface和DBFileInterface
//网络请求 public interface NetInterface { int HTTP_METHOD_GET = 1; int HTTP_METHOD_POST = 2; //http请求get、post void request(String url, HashMap<String, Object> request, HashMap<String, String> headers, int httpMethod, HttpCallback callback); //上传文件 void requestUpload(String url, byte[] buf, HashMap<String, String> headers, HttpCallback callback); } //文件缓存接口 public interface DBFileInterface { //保存数据 boolean put(String key, Object object); //获取数据 Object get(String key); //删除数据 boolean del(String key); }
定义数据返回统一接口
本地视频数据源LocalVideoRepo,实现三个接口:获取本地视频、获取视频上传信息、保存视频上传信息(见附件demo中代码)
云端视频数据源NetVideoRepo,实现获取网络视频接口、以及相关上传相关的接口(见附件demo中代码)
4.功能逻辑层开发
实现本地/云端视频获取、上传视频功能。
5.交互逻辑层开发
定义用户交互接口,例如显示本地视频界面的交互设计到加载状态和列表显示两部分,对应的接口定义如下
对应的LocalVideoPresenter则实现LocalVideoContract.Presenter接口,并进行view交互处理
public class LocalVideoPresenter implements LocalVideoContract.Presenter { private GetLocalVideoCase getLocalVideoCase; private LocalVideoContract.View localVideoView; private int loadLocalVideoStatus = LocalVideoContract.LOAD_STATUS_IDLE; private CaseHandler caseHandler; public LocalVideoPresenter(LocalVideoContract.View localVideoView) { this(localVideoView, new GetLocalVideoCase(), CaseHandler.getInstance()); } public LocalVideoPresenter(LocalVideoContract.View localVideoView, GetLocalVideoCase getLocalVideoCase, CaseHandler caseHandler) { this.localVideoView = localVideoView;//对应的交互view this.localVideoView.setPresenter(this);//委托view的交互为当前presenter this.getLocalVideoCase = getLocalVideoCase;//功能实现类 this.caseHandler = caseHandler;//处理线程以及返回数据的回调控制 } public void loadVideos() { if (loadLocalVideoStatus == LocalVideoContract.LOAD_STATUS_ING) { return; } //设置view为正在加载状态 setLoadVideoStatus(LocalVideoContract.LOAD_STATUS_ING); //执行加载任务 caseHandler.execute(getLocalVideoCase, new GetLocalVideoCase.RequestValues(null), new BaseCase.CaseCallback<GetLocalVideoCase.ResponseValue,GetLocalVideoCase.ErrorValue>() { public void onSuccess(GetLocalVideoCase.ResponseValue response) { if (response.localVideoInfoList == null || response.localVideoInfoList.isEmpty()) { //设置view为加载数据为空的状态 setLoadVideoStatus(LocalVideoContract.LOAD_STATUS_NO_DATA); return; } //设置view为加载成功的状态 setLoadVideoStatus(LocalVideoContract.LOAD_STATUS_SUCC); //显示视频列表view localVideoView.showLocalVideoList(response.localVideoInfoList); } public void onError(GetLocalVideoCase.ErrorValue error) { if (error.t != null) { error.t.printStackTrace(); } //设置view为加载失败的状态 setLoadVideoStatus(LocalVideoContract.LOAD_STATUS_FAILED); } }); } private void setLoadVideoStatus(int status) { loadLocalVideoStatus = status; localVideoView.showLocalVideoLoadStatus(loadLocalVideoStatus); } }
原则上,定义的View和Presenter接口尽量最小功能集,将不同view的交互定义不同的presenter,便于代码复用,可控性高。
这样当一个界面可以多个不同的view自行组合时,只需要实现相应的几个View接口即可;或者是多个界面可以共享使用同一个View接口。
6.编写测试用例
上面完成后,基本上除了界面外,我们的功能和交互都已经完成了。这个时候,我们就可以编写测试用例,进行测试了。
这里我们使用测试框架:junit和mockito
因为是junit测试用例,是在纯java的环境下跑测试的,所以如果代码中有android.*时,会报错。
因此我们编码过程中尽量避免使用到android.*,当然在子线程中更新view无法避免会使用到Handler。
不用担心,在实现的地方,我们尽量使用接口去定义,然后在测试中实现此接口而不使用Handler,然后替换实现即可,因为测试中实现回调接口即可,而不需要使用handler
例如,启线程池的ThreadPoolCaseScheduler中我们会使用到Handler,在测试时,我们可以编写TestCaseScheduler实现BaseCaseScheduler接口替代ThreadPoolCaseScheduler
public class ThreadPoolCaseScheduler implements BaseCaseScheduler { private Executor executor; private final Handler mHandler = new Handler(); public ThreadPoolCaseScheduler() { executor = Executors.newCachedThreadPool(); } public void execute(Runnable runnable) { executor.execute(runnable); } public <R extends BaseCase.ResponseValue, E extends BaseCase.ErrorValue> void notifyResponse(final R response, final BaseCase.CaseCallback<R, E> callback) { mHandler.post(new Runnable() { public void run() { callback.onSuccess(response); } }); } public <R extends BaseCase.ResponseValue, E extends BaseCase.ErrorValue> void notifyError(final E error, final BaseCase.CaseCallback<R, E> callback) { mHandler.post(new Runnable() { public void run() { callback.onError(error); } }); } } public class TestCaseScheduler implements BaseCaseScheduler { public void execute(Runnable runnable) { runnable.run(); } public <R extends BaseCase.ResponseValue, E extends BaseCase.ErrorValue> void notifyResponse(final R response, final BaseCase.CaseCallback<R, E> callback) { callback.onSuccess(response); } public <R extends BaseCase.ResponseValue, E extends BaseCase.ErrorValue> void notifyError(final E error, final BaseCase.CaseCallback<R, E> callback) { callback.onError(error); } }
对应的LocalVideoPresenterTest测试用例代码如下:
public class LocalVideoPresenterTest { @Mock private GetLocalVideoCase getLocalVideoCase; @Captor private ArgumentCaptor<BaseCase.CaseCallback> callbackCaptor; @Mock private LocalVideoContract.View localVideoView;//模拟一个view对象 private LocalVideoPresenter localVideoPresenter; @Before public void setupLocalVideoPresenter() { MockitoAnnotations.initMocks(this); //使用TestCaseScheduler替换包含Handler的ThreadPoolCaseScheduler CaseHandler caseHandler = new CaseHandler(new TestCaseScheduler()); localVideoPresenter = new LocalVideoPresenter(localVideoView, getLocalVideoCase, caseHandler); } @Test public void loadVideos_success() throws Exception { localVideoPresenter.loadVideos();//加载视频 verify(getLocalVideoCase).setCaseCallback(callbackCaptor.capture());//捕获getLocalVideoCase的数据回调参数 List<LocalVideoInfo> list = new ArrayList<LocalVideoInfo>(); list.add(new LocalVideoInfo("test1")); //设置回调数据为list callbackCaptor.getValue().onSuccess(new GetLocalVideoCase.ResponseValue(list)); //验证localVideoView会调用两次showLocalVideoLoadStatus方法,并且一次为加载中状态,一次为加载成功 verify(localVideoView).showLocalVideoLoadStatus(LocalVideoContract.LOAD_STATUS_ING); verify(localVideoView).showLocalVideoLoadStatus(LocalVideoContract.LOAD_STATUS_SUCC); } }
更多的测试部分,可以查看demo中的源码。
在编写NetVideoPresenterTest时,也出现一个问题,这里使用的NetworkHelper中会用到org.apache.*,而找不到此类,我们同样采用了上面的方式,测试时实现了NetInterface接口取代NetworkHelper的实现。
注:在编写前面步骤的代码时,尽量避免使用到非jdk的api;如果无法避免,就得抽象成统一接口,然后再实现,方便后面的修改和测试。
7.UI开发
在实现的UI界面,我们需要实现上面的交互View接口体现到UI上,并指定其Presenter。
public class LocalVideoFragment extends Fragment implements UploadVideoContract.View,
LocalVideoContract.View
public class NetVideoFragment extends Fragment implements NetVideoContract.View
我们可以看到本地视频界面会有两个View组成,1.显示视频列表的View,2.上传视频的View,所以需要继承这两个View;
而云端视频界面不用上传视频,则只需继承列表View即可
8.编写UI测试用例
由于UI变动性比较大,并且编写UI测试细节多,个人觉得没有编写UI测试用例的必要,手工点点更快很准;如果是压力测试编写monkey脚本进行测试即可。
9.针对各种变更的修改
界面上的变更,如果只是UI修改,而功能不变,则直接修改UI,实现相应的View接口即可,不用改变非UI层的
代码;如果涉及到UI功能变更,添加新的View接口和Presenter从新实现即可。
数据机构改变,需要修改UI、Presenter的实现、以及功能逻辑层XXXCase
缓存、网络框架策略的修改,添加对应的接口代理实现,并应用即可。
四. 总结
对于MVP+Clean架构在Android app上的应用,个人觉得不用分的那么清楚,可以根据每个功能模块的复杂和易变性进行区别对待。
功能复杂或者UI易变的模块,可以采用mvp+clean模式,而那些简单且容易修改的模块,直接在采用mvc模式。
没有万能的框架,所有的架构归根结底就是抽象的深度不一样,越抽象越容易扩展。
往往在设计的时候想得越多越容易深陷,最终可能白白浪费了很多时间和精力在这个上面而达不到预期的效果,所以一开始不要想太多太深,在后面实现和扩展的过程中,慢慢地就会发现问题,再进行调整;考虑再多,重构也都是必经之路。