之前我寫過一篇關於 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 層等
無論任何架構,在業務不斷髮展的過程當中,可能都須要在某個架構基礎上,根據咱們的實際業務狀況,來作相應的改造和優化。
下面是個人公衆號,乾貨文章不錯過,有須要的能夠關注下,有任何問題能夠聯繫我: