Android MVP 架構改造 ~ 如何重用頂層業務

以前我寫過一篇關於 MVP 架構的文章《Android架構—MVP架構在Android中的實踐》

隨着業務的複雜化,我們會發現傳統的 MVP 架構依然會有很多問題。

下面我將和大家一起探討下在使用 MVP 架構過程中遇到的比較大的問題以及解決方案。

隨着業務邏輯複雜化,我們可能會遇到下面幾個比較大的問題:

  1. Presenter 中充斥着非常多的業務回調方法,Presenter 非常臃腫
  2. 頂層業務邏輯無法重用

Presenter 臃腫的問題

Prenseter 臃腫的表現形式有兩種:

  • 第一種:正如我們上面說的 由於 Presenter 有非常多的 業務回調方法,比如某個業務需要網絡請求,那麼成功後怎麼處理,對應一個方法,失敗了怎麼處理,對應一個方法,這樣的話基本上一個網絡請求至少對應兩個方法。如果某個界面業務比較複雜,請求的接口比較多的話,這樣的業務回調方法也就比較多

  • 第二種:除了業務的回調方法,可能還存在一些業務回調方法的 輔助方法 。何謂 輔助方法? 就是爲了實現業務回調方法而衍生出的一些方法。比如,某個接口請求成功後,邏輯比較多,可能我們會把某段內聚強的邏輯單獨拿出來放在一個新方法裏供業務回調方法調用。

所以 Presenter 會有很多 業務回調方法 和它衍生的 輔助方法

我一般將業務回調方法命名爲:XXXSuccess()XXXFailed()XXXSuccess() 對應業務請求成功對應的方法, XXXFailed() 對應業務請求失敗的方法。

這樣命名做有兩個好處:

  • 一是 後期維護的時候我們只需要查詢 SuccessFailed 相關的方法即可,便於後期修改維護。

  • 二是 業務回調方法 和 輔助方法 從名字上就可以區分。 《Android架構—MVP架構在Android中的實踐》 也有關於命名這方面的敘述,需要的可以去看下。

Presenter 臃腫的問題,導致 Presenter 維護成本變高,可讀性變差。因爲充斥各種業務回調方法,和一些衍生的輔助方法 。

如果用普通的 MVP 架構來實現,代碼 “糟糕” 地自己都不願意維護了

業務邏輯無法重用問題

這個問題不太好描述。爲了更好的描述這個問題,我們先來看下我對業務的劃分:

  • 簡單業務:簡單業務只由一個 “操作” 組成。比如網絡請求、數據庫操作等

  • 複雜業務 :一個複雜業務由多個簡單業務組成,它像一個業務鏈。比如一個複雜業務需要多個網絡請求然後再把數據呈現給用戶。

不管是 簡單業務 還是 複雜業務 我們都是放到 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 中的 ViewPresenter 是一一對應的關係

假設 A 界面對應的 Presenter 中實現了一個複雜的業務鏈, 此時 B 頁面也需要這個 複雜業務鏈,

BPresenter 又無法直接使用 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改造後的架構對比圖:

MVP架構對比

使用基於MVP改造的架構來優化上面的案例一

以第一個業務邏輯重用的案例,我們來實現下:

1) Engine 層,省略其實現類:

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>)
}

2) View 層:

在 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...
    }
}

3) Presenter 層:

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 是否複雜了一點呢?

筆者覺得還是有加上 Engine 層的必要的:

  1. 在業務不斷迭代的過程中,都是由簡單變得複雜
  2. Engine 層封裝 簡單業務 ,可以更靈活的處理由 簡單業務 產生的業務分支

下面我們再舉一個實際的案例:

案例4

上面的簡單的業務:查詢桌位狀態,成功後根據不同的狀態處理不同的邏輯

上面這個業務邏輯在 桌位列表 頁用到了,在 訂單搜索 頁也用到了,我們需要在兩個不同的地方進行 status 判斷,然後走不同的邏輯分支

如果我們在 Engine 中在封裝一層,就不需要在多個地方進行 if 判斷了,這些邏輯判斷都可以寫在 Engine 中,然後對外暴露幾個需要關心的業務接口方法即可

Engine 層 和 Repository 的區別

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 層等

不管任何架構,在業務不斷髮展的過程中,可能都需要在某個架構基礎上,根據我們的實際業務情況,來做相應的改造和優化。


如果你覺得本文幫助到你,給我個關注和讚唄!

另外,我爲 Android 程序員編寫了一份:超詳細的 Android 程序員所需要的技術棧思維導圖

如果有需要可以移步我的 GitHub -> AndroidAll,裏面包含了最全的目錄和對應知識點鏈接,幫你掃除 Android 知識點盲區。 由於篇幅原因只展示了 Android 思維導圖:
超詳細的Android技術棧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章