MVC vs. MVP vs. MVVM on Android

在過去的幾年裏,將Android應用程序轉變成邏輯組件的方法已經逐漸成熟。很大程度上擺脫了MVC模式,轉而採用更模塊化、可測試的模式。

Model View Presenter (MVP) & Model View ViewModel (MVVM)是最廣泛被採用的兩種替代方案。本文不去討論哪種方式更適合於Android應用開發,只是通過案例來看到每種模式是如何編寫的。

本文通過實現一個井字遊戲,分別通過MVC、MVP、MVVM三種模式實現遊戲效果。源代碼已經上傳到Github倉庫中。

這裏寫圖片描述

MVC

Model, View, Controller將應用程序在宏觀層面分爲3中職責。

Model

Model模型是應用程序中的數據+狀態+業務邏輯。可以說是應用程序的大腦,不受View視圖和Controller控制器的束縛,因此很多情況下是可以複用的。

View

View視圖是Model的展現,負責呈現UI並在用戶與應用程序交互時與Controller通信。在MVC架構中,視圖通常很“愚蠢”,因爲他們不瞭解底層模型,也沒有對狀態的理解,或者當用戶通過單擊按鈕,鍵入值等進行交互時要做什麼。這個想法是越少他們知道他們對模型的耦合越鬆散,因此他們要改變的就越靈活。

Controller

Controller是將app粘在一起的膠水,它是應用程序中的主控制器。當View告訴Controller用戶單擊按鈕時,Controller決定如何與Model進行相應的交互。根據Model中的數據更改, Controller可以根據需要更新View的狀態。在Android應用程序中,Controller幾乎總是由Activity或Fragment來擔任的。

這就是我們的井字遊戲中每個類扮演的角色

這裏寫圖片描述

讓我們更詳細地檢查Controller。

public class MVCTicTacToeActivity extends AppCompatActivity {
    private static String TAG = MVCTicTacToeActivity.class.getName();

    private Board model;

    private ViewGroup buttonGrid;
    private View winnerPlayerViewGroup;
    private TextView winnerPlayerLabel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc_tictactoe);
        winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
        winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
        buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);

        model = new Board();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_tictactoe, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_reset:
                reset();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    public void onCellClicked(View v) {

        Button button = (Button) v;

        String tag = button.getTag().toString();
        int row = Integer.valueOf(tag.substring(0,1));
        int col = Integer.valueOf(tag.substring(1,2));
        Log.i(TAG, "Click Row: [" + row + "," + col + "]");

        Player playerThatMoved = model.mark(row, col);

        if(playerThatMoved != null) {
            button.setText(playerThatMoved.toString());
            if (model.getWinner() != null) {
                winnerPlayerLabel.setText(playerThatMoved.toString());
                winnerPlayerViewGroup.setVisibility(View.VISIBLE);
            }
        }

    }

    private void reset() {
        winnerPlayerViewGroup.setVisibility(View.GONE);
        winnerPlayerLabel.setText("");

        model.restart();

        for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
            ((Button) buttonGrid.getChildAt(i)).setText("");
        }
    }
}

評估

MVC在分離模型和視圖方面做得很好。當然,該模型可以很容易地進行測試,因爲它不依賴於任何東西,並且視圖在單元測試級別上沒有什麼可測試的。然而,控制器有一些問題。

Controller關注

  • 可測試性 - Controller與Android API緊密聯繫,難以進行單元測試。
  • 模塊化和靈活性 - Controller與View緊密耦合。它也可能是View的擴展。如果我們更改View,我們必須返回並更改Controller。
  • 維護 - 隨着時間的推移,越來越多的代碼開始轉移到Controller中,使它們變得臃腫和脆弱。

我們如何解決這個問題?MVP來拯救!

MVP

MVP將Controller斷開,以便自然View/Activity耦合可以發生,而不會與其他“Controller”責任相關聯。讓我們再次從MVC對比開始。

Model

與MVC相同/無變化

View

這裏唯一的變化是Activity/Fragment現在被視爲View的一部分。讓Activity實現一個視圖界面,以便Presnenter有一個可以編碼的界面。這消除了將它耦合到任何特定的頁面,並允許使用視圖的mock實現進行簡單的單元測試。

Presenter

這實質上是MVC的Controller,除了它完全不依賴於View,只是一個接口。這解決了可測試性問題以及我們在MVC中遇到的模塊化/靈活性問題。實際上,MVP純粹主義者認爲Presenter不應該對任何Android API或代碼有任何引用。

我們再來看看在我們的應用程序中在MVP中的分解。

這裏寫圖片描述

在下面更詳細地看看Presenter,你會注意到的第一件事是每個動作的意圖是多麼簡單和清晰。它不是告訴View如何顯示某些東西,而只是告訴它要顯示什麼。

public class TicTacToePresenter implements Presenter {
    private TicTacToeView view;
    private Board model;

    public TicTacToePresenter(TicTacToeView view){
        this.view = view;
    }

    @Override
    public void onCreate() {
        model = new Board();
    }

    @Override
    public void onPause() {

    }

    @Override
    public void onResume() {

    }

    @Override
    public void onDestroy() {

    }


    public void onButtonSelected(int row, int col) {
        Player playerThatMoved = model.mark(row, col);

        if(playerThatMoved != null) {
            view.setButtonText(row, col, playerThatMoved.toString());

            if (model.getWinner() != null) {
                view.showWinner(playerThatMoved.toString());
            }
        }
    }

    public void onResetSelected() {
        view.clearWinnerDisplay();
        view.clearButtons();
        model.restart();
    }
}

爲了在不將Activity與Presenter綁定的情況下進行這項工作,我們創建了一個Activity實現的接口。在測試中,我們將基於此接口創建一個mock來測試與Presenter和View的交互。

public interface TicTacToeView {
    void showWinner(String winnerLabel);
    void clearWinnerDisplay();
    void clearButtons();
    void setButtonText(int row,int col,String text);
}

評估

我們可以很容易地對Presenter邏輯進行單元測試,因爲它沒有綁定到任何Android特定的View和API,並且只要View實現*TicTacToeView*界面,我們也可以使用任何其他View。

Presenter的關注

  • 維護 - 與Controller一樣,Presenter隨着時間的推移,傾向於收集額外的業務邏輯。在某些時候,開發者經常會發現自己擁有難以分開的笨重的Presenter。

當然,開發人員可以謹慎的防止這種情況發生。但是,MVVM可以更好的解決這個問題。

MVVM

Android上具有數據綁定功能的 MVVM具有易於測試和模塊化的優點,同時還減少了我們必須編寫的連接View +Model的代碼數量。

我們來看看MVVM的各個部分。

Model

與MVC相同/無變化

View

該View以一種靈活的方式綁定到由viewModel來監控可觀察的變量和操作。

ViewModel

ViewModel負責包裝Model並準備View所需的可觀察數據。它還爲View提供了將事件傳遞給Model的hocks。然而,ViewModel並不依賴於View。

我們的程序早MVVM模式中分解。

這裏寫圖片描述

讓我們仔細看看這裏的代碼,從ViewModel開始。

public class TicTacToeViewModel implements ViewModel {
    private Board model;
    public final ObservableArrayMap<String,String> cells = new ObservableArrayMap<>();
    public final ObservableField<String> winner = new ObservableField<>();

    public TicTacToeViewModel(){
        model = new Board();
    }

    @Override
    public void onCreate() {

    }

    @Override
    public void onPause() {

    }

    @Override
    public void onResume() {

    }

    @Override
    public void onDestroy() {

    }

    public void onResetClick(){
        model.restart();
        winner.set(null);
        cells.clear();
    }

    public void onCellClick(int row, int col){
        Player player = model.mark(row,col);
        cells.put(""+row+col,player == null?null:player.toString());
        winner.set(model.getWinner()==null?null:model.getWinner().toString());
    }
}

查看xml文件,看看這些變量和事件是如何綁定的。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <import type="android.view.View" />
        <variable
            name="player"
            type="com.shijc.mvx.mvvm.viewmodel.TicTacToeViewModel"/>
    </data>

    <LinearLayout
        android:id="@+id/tictactoe"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context="com.acme.tictactoe.view.TicTacToeActivity">

        <GridLayout
            android:id="@+id/buttonGrid"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:columnCount="3"
            android:rowCount="3">

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(0,0)}"
                android:text='@{player.cells["00"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(0,1)}"
                android:text='@{player.cells["01"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(0,2)}"
                android:text='@{player.cells["02"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(1,0)}"
                android:text='@{player.cells["10"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(1,1)}"
                android:text='@{player.cells["11"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(1,2)}"
                android:text='@{player.cells["12"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(2,0)}"
                android:text='@{player.cells["20"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(2,1)}"
                android:text='@{player.cells["21"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(2,2)}"
                android:text='@{player.cells["22"]}' />

        </GridLayout>


        <LinearLayout
            android:id="@+id/winnerPlayerViewGroup"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="@{player.winner == null ? View.GONE:View.VISIBLE}"
            tools:visibility="visible">

            <TextView
                android:id="@+id/winnerPlayerLabel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="20dp"
                android:textSize="40sp"
                android:text="@{player.winner}"
                tools:text="X" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Winner"
                android:textSize="30sp" />

        </LinearLayout>

    </LinearLayout>

</layout>

評估

單元測試現在更容易,因爲你真的不依賴於View。測試時,您只需要驗證Model更改時是否正確設置了可觀察變量。沒有必要mock測試View,因爲有MVP模式。

MVVM關注

  • 維護 - 由於View可以綁定到變量和表達式,因此無關的表示邏輯可能會隨着時間的推移而變動,從而有效地將代碼添加到我們的XML中。爲了避免這種情況,總是直接從ViewModel獲取值,而不是試圖在視圖綁定表達式中計算或派生它們。這樣計算可以適當地進行單元測試。

結論

MVP和MVVM在將應用程序分解爲模塊化單一用途組件方面比MVC做得更好,但它們也增加了應用程序的複雜性。對於只有一個或兩個屏幕的非常簡單的應用程序,MVC可能工作得很好。帶有數據綁定的MVVM具有吸引力,因爲它遵循更加反應式的編程模型,並且生成的代碼更少。

如果您有興趣在實踐中看到MVP和MVVM的更多示例,我鼓勵您查看Google Architecture Blueprints項目。還有很多博客文章深入探討這幾種模式

參考

https://academy.realm.io/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android

源碼地址

本文作者: shijiacheng
本文鏈接: http://shijiacheng.studio/2018/07/01/mvx/
版權聲明: 轉載請註明出處!

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