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

Chiclaim發表於2019-03-01

以前我寫過一篇關於 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 層等

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

聯絡我

下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我:

公眾號:  chiclaim

相關文章