之前我写过一篇关于 MVP 架构的文章《Android架构—MVP架构在Android中的实践》。android
随着业务的复杂化,咱们会发现传统的 MVP 架构依然会有不少问题。数据库
下面我将和你们一块儿探讨下在使用 MVP 架构过程当中遇到的比较大的问题以及解决方案。网络
随着业务逻辑复杂化,咱们可能会遇到下面几个比较大的问题:架构
Prenseter 臃肿的表现形式有两种:app
因此 Presenter 会有不少 业务回调方法
和它衍生的 辅助方法
。ide
我通常将业务回调方法命名为:XXXSuccess() 和 XXXFailed(),XXXSuccess() 对应业务请求成功对应的方法, XXXFailed() 对应业务请求失败的方法。fetch
这样命名作有两个好处:优化
Presenter 臃肿的问题,致使 Presenter 维护成本变高,可读性变差。由于充斥各类业务回调方法,和一些衍生的辅助方法 。this
若是用普通的 MVP 架构来实现,代码 "糟糕" 地本身都不肯意维护了spa
这个问题不太好描述。为了更好的描述这个问题,咱们先来看下我对业务的划分:
不论是 简单业务 仍是 复杂业务 咱们都是放到 Presenter 中。
对于 复杂业务,尽管可能调用了多个接口,咱们可使用 RxJava 将这些请求经过链式的方式进行组装, 避免 Callback Hell
举一个 复杂业务 的例子:
// 业务接口一:根据用户 id 获取用户的基本信息 userApi.fetchUserInfo("userId") .flatMap(new Func1<User, Observable<User>>() { @Override public Observable<User> call(User user) { // 业务接口二:获取用户的好友列表 return fetchFriendsInfo(user); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<User>() { @Override public void call(User user) { // 在界面展现 用户的基本信息 和 用户的好友列表 mView.loadUserSuccess(user); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { throwable.printStackTrace(); // 在界面提示 对应的错误提示 mView.loadUserFailed(); } });
上面的 复杂业务逻辑 的例子主要逻辑为:根据用户 id 获取用户 基本信息
,成功后获取用户的 好友列表
,最后将这些信息展现在界面上。为了实现这个业务逻辑,请求了两个网络接口。
可是,上面的业务逻辑若是外在 Presenter 中是没法复用的。由于 MVP 中的 View 和 Presenter 是一一对应的关系
假设 A 界面对应的 Presenter 中实现了一个复杂的业务链, 此时 B 页面也须要这个 复杂业务链,
B 的 Presenter 又没法直接使用 A 界面的 Presenter, 这就出现业务没法重用的问题,B 界面的 Presenter 还得要把业务链从新写一遍,而后对成功失败的回调进行处理。
需求描述:扫二维码、条形码,把商品直接接入购物车
在手机上实现扫一扫二维码、条形码,直接把商品加入购物车,这个功能已经实现。
可是并非全部的 Android 设备上都会有摄像头,好比一些定制的硬件上可能就没有 ,不过会有外接设备(扫码枪) 来支持扫一扫
因此须要为有扫码枪的系统上支持 扫二维码、条形码,将商品加入购物车 的功能
此时也会出现须要重用业务逻辑的状况。业务流程以及业务重用的状况,以下图所示:
通常来讲咱们都会将手机摄像头的扫一扫功能,封装到一个 Activity 中,好比:BaseScanActivity。
假设手机设备上实现的这个业务逻辑的类名为 GoodsScanActivity 该类继承了 BaseScanActivity
如今须要针对扫码枪的设备也实现相同的功能, 可是该业务逻辑 在 GoodsScanActivity 对应的 Presenter 中, 该业务逻辑很难重用
需求描述:咱们的 App 是 to B 的,用户若是有多个店铺会用到 切换店铺
的功能:进入 店铺列表界
面,点击某个店铺,而后调用 切店接口
,成功后调用 初始化接口
这个功能已经在用户 `个人` 模块中实现了:个人店铺列表 --> 切店
最近须要开发一个 开店功能
,这个功能之前是在其余 App 中的,开店成功后也须要 切换店铺
这个时候也会出现须要重用业务逻辑的状况。业务流程以及业务重用的状况,以下图所示:
好比某些硬件内置 Android 系统, 可是弱化屏幕展现功能,或者根本就没有屏幕。这个时候咱们就不能直接使用之前的 Module 了
对于 复杂的业务链,咱们也没法重用。 这个时候出现业务须要重用的状况会更多
经过上面案例的分析,咱们发现随着业务不断的复杂化,对复杂业务的重用性变得更加紧迫
为了可以将复杂业务重用,咱们将其抽取到新的一层中:Engine 层,Presenter 不直接和 Model 交互,改为和 Engine 层交互, 再由 Engine 层和 Model 层进行交互
下面是常规的 MVP 和咱们基于MVP改造后的架构对比图:
以第一个业务逻辑重用的案例,咱们来实现下:
interface IMenuScanGunEngine : IEngine { //二维码 fun getMenuByUrl(param: MenuScanGunEngine.Param, logic: IMenuByUrlLogic?) //条形码 fun getMenuByCode(param: MenuScanGunEngine.Param, logic: IMenuByCodeLogic?) }
getMenuByUrl() 与之对应的逻辑回调:
interface IMenuByUrlLogic { fun scanFailed(errorCode: String?, errorMessage: String?) fun gotoComboMenuDetail(menuId: String?, baseMenuVo: BaseMenuVo?) fun gotoNormalMenuDetail(baseMenuVo: BaseMenuVo?) fun menuTookOff() fun menuSoldOut() fun addCartSuccess(menuName: String?, dinningTableVo: DinningTableVo?) }
getMenuByCode() 与之对应的逻辑回调
interface IMenuByCodeLogic : IMenuByUrlLogic { fun showMenuList(list: ArrayList<BoMenu>) }
在 View 层实现全部的业务回调
//View 继承了上面两个业务回调接口 interface View : BaseView<Presenter>, IMenuByCodeLogic, IMenuByUrlLogic{ }
Activity/Fragment 实现业务回调方法,也就是 View 层的实现类,省略具体的实现逻辑:
class MenuScanGunActivity:MenuScanGunContract.View{ //扫码失败 fun scanFailed(errorCode: String?, errorMessage: String?){ //ignore... } //进入套餐详情 fun gotoComboMenuDetail(menuId: String?, baseMenuVo: BaseMenuVo?){ //ignore... } //进入普通商品详情 fun gotoNormalMenuDetail(baseMenuVo: BaseMenuVo?){ //ignore... } //商品下架 fun menuTookOff(){ //ignore... } //商品售罄 fun menuSoldOut(){ //ignore... } //加入购物车成功 fun addCartSuccess(menuName: String?, dinningTableVo: DinningTableVo?){ //ignore... } //一个码对应多个商品,展现一个列表让用户选择 fun showMenuList(list: ArrayList<BoMenu>){ //ignore... } }
interface Presenter : BasePresenter{ fun processResultCode(resultCode: String?) fun processMenuDetail(menuId: String) } class MenuScanGunPresenter(private var mOrderId: String?, private var mSeatCode: String?, private var mView: MenuScanGunContract.View?) : MenuScanGunContract.Presenter { private val mEngine = MenuScanGunEngine() override fun processResultCode(resultCode: String?) { if (mEngine.isURL(resultCode)) { mEngine.getMenuByUrl(createParam(resultCode), mView) } else { mEngine.getMenuByCode(createParam(resultCode), mView) } } override fun processMenuDetail(menuId: String) { mEngine.handleMenuDetail(menuId, mView, createParam(menuId = menuId)) } private fun createParam(readCode: String? = null, menuId: String? = null): MenuScanGunEngine.Param { return MenuScanGunEngine.Param().apply { this.readCode = readCode this.menuId = menuId this.orderId = mOrderId this.seatCode = mSeatCode } } override fun subscribe() { } override fun unsubscribe() { mView = null mEngine.destroy() } }
经过这个例子咱们知道,若是要复用业务逻辑只须要在 Presenter
中使用须要的 Engine
便可。
上面列举的三个案例,都是 复杂业务
(复杂业务多是接口请求、数据库操做的组合),可是在项目中一样会存在不少的 简单业务
(一个网络请求或者数据库操做)
在这种状况下,咱们是否还须要 Engine 层呢?若是再加上 Engine 是否复杂了一点呢?
笔者以为仍是有加上 Engine 层的必要的:
简单业务
,能够更灵活的处理由 简单业务 产生的业务分支下面咱们再举一个实际的案例:
上面的简单的业务:查询桌位状态,成功后根据不一样的状态处理不一样的逻辑
上面这个业务逻辑在 桌位列表
页用到了,在 订单搜索
页也用到了,咱们须要在两个不一样的地方进行 status 判断,而后走不一样的逻辑分支
若是咱们在 Engine 中在封装一层,就不须要在多个地方进行 if 判断了,这些逻辑判断均可以写在 Engine 中,而后对外暴露几个须要关心的业务接口方法便可
Google 在 android-architecture
中的 MVP 架构中,会把 Model 中的 DataSource 在抽象一层 Repository ,而后 Presenter 调用 Repository ,以下所示:
View -> Presenter -> Repository -> RemoteDataSource/LocalDataSource
读者可能会问,你这个 Engine 和这个 Repository 不差很少吗?
其实不同! Repository
更多的是组合多个 DataSource
,好比是操做本地数据源,仍是调用远程接口,充当的是一个 底层数据
提供者的角色
而咱们这个 Engine
层主要是对顶层业务的封装,而不是对数据的封装
另外,在实际的开发过程当中,我的以为 Repository
的做用并非很大。 固然每一个 App 的性质不同,有些 App 可能对本地数据操做比较多,对 Model
层的依赖比较大
若是本地数据操做比较多,其实均可以放到 Engine 层在处理,根据业务逻辑的不一样,对本地 Dao 层 和 远程数据层进行组合便可
若是不须要 Repository
层的话,那么咱们最终的流程是这样的:
View -> Presenter -> Engine -> RemoteDataSource/LocalDataSource
下面是个人公众号,干货文章不错过,有须要的能够关注下,有任何问题能够联系我:
基于 MVP 架构基础上,咱们在 Presenter 和 Model 之间加了一个 Engine 层,使得业务逻辑变得可重用,避免模板代码和逻辑的不一致性问题
同时也解决 Presenter 层代码过于臃肿的问题
View 层的业务回调方法也更加清晰,不一样的业务回调,放在不一样接口里,也保证了业务回调方法命名的统一
固然,Engine 层只是笔者取的名字,也能够叫作 Business 层等
无论任何架构,在业务不断发展的过程当中,可能都须要在某个架构基础上,根据咱们的实际业务状况,来作相应的改造和优化。
下面是个人公众号,干货文章不错过,有须要的能够关注下,有任何问题能够联系我: