Android 架構優化~MVP 架構改造

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

隨着業務的複雜化,咱們會發現傳統的 MVP 架構依然會有不少問題。數據庫

下面我將和你們一塊兒探討下在使用 MVP 架構過程當中遇到的比較大的問題以及解決方案。網絡

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

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

Presenter 臃腫的問題

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

  • 第一種:正如咱們上面說的 因爲 Presenter 有很是多的 業務回調方法,好比某個業務須要網絡請求,那麼成功後怎麼處理,對應一個方法,失敗了怎麼處理,對應一個方法,這樣的話基本上一個網絡請求至少對應兩個方法。若是某個界面業務比較複雜,請求的接口比較多的話,這樣的業務回調方法也就比較多
  • 第二種:除了業務的回調方法,可能還存在一些業務回調方法的 輔助方法 。何謂 輔助方法? 就是爲了實現業務回調方法而衍生出的一些方法。好比,某個接口請求成功後,邏輯比較多,可能咱們會把某段內聚強的邏輯單獨拿出來放在一個新方法裏供業務回調方法調用。

因此 Presenter 會有不少 業務回調方法 和它衍生的 輔助方法ide

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

這樣命名作有兩個好處:優化

  • 一是 後期維護的時候咱們只須要查詢 SuccessFailed 相關的方法便可,便於後期修改維護。
  • 二是 業務回調方法 和 輔助方法 從名字上就能夠區分。 《Android架構—MVP架構在Android中的實踐》 也有關於命名這方面的敘述,須要的能夠去看下。

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 中的 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

相關文章
相關標籤/搜索