Android 中MVC、MVP和MVVM比较

项目中用到了MVP模式,所以就想在网上找找MVP和MVC的区别。找了一大圈,都感觉说的模模糊糊,甚至很多都前后矛盾,最后还是在歪果仁那里找到了一篇浅显易懂的文章,总算明白了MVP和MVC的区别。我下面写的基本是翻译原文。有兴趣的可以去看原文:https://realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android/

为了更好地帮助理解,通过一个例子程序来说明,程序是一个井字游戏(Tic Tac Toe)。https://github.com/ericmaxwell2003/ticTacToe

Android 中MVC、MVP和MVVM比较_第1张图片
QQ截图20170330170050.png

可以通过git命令来切换各个模式(git checkout mvc, git checkout mvp, git checkout mvvm)

MVC

MVC将程序分为三个模块Model,View和Controller

Model

Model包括了程序的数据、状态和业务逻辑。可以说它就是应用的大脑。它与view和controller都没有关系,所以它在各种情况下都是复用的。

View

View是Model的展现。View的职责是展示UI,并且当用户和应用交互时负责与controller进行通信。在MVC结构中,Views通常都好像一个“哑巴”,它对底层的模型和业务逻辑都一无所知,当用户通过按下按钮,输入文字等方式进行交互时,它也不知道如何响应。理想的情况是它知道的越少,与model的耦合就越少,它也就越容易修改。

Controller

Controller像胶水一样将app的各部分粘合在一起。它是app行为的控制器。当View告诉Controller用户点击了一个button,Controller会决定接下来如何与model进行交互。当model中的数据有变化时,controller会决定是否更新view。在Android应用中,controller通常是由Activity或Fragment充当的。

看看Tic Tac Toe的总体结构和各个类所扮演的角色

Android 中MVC、MVP和MVVM比较_第2张图片
MVC.png

让我们详细的看看controller的代码:

public class TicTacToeActivity extends AppCompatActivity {

    private Board model;

    /* View Components referenced by the controller */
    private ViewGroup buttonGrid;
    private View winnerPlayerViewGroup;
    private TextView winnerPlayerLabel;

    /**
     * In onCreate of the Activity we lookup & retain references to view components
     * and instantiate the model.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tictactoe);
        winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
        winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
        buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);

        model = new Board();
    }

    /**
     * Here we inflate and attach our reset button in the menu.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_tictactoe, menu);
        return true;
    }
    /**
     *  We tie the reset() action to the reset tap event.
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_reset:
                reset();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    /**
     *  When the view tells us a cell is clicked in the tic tac toe board,
     *  this method will fire. We update the model and then interrogate it's state
     *  to decide how to proceed.  If X or O won with this move, update the view
     *  to display this and otherwise mark the cell that was clicked.
     */
    public void onCellClicked(View v) {

        Button button = (Button) v;

        int row = Integer.valueOf(tag.substring(0,1));
        int col = Integer.valueOf(tag.substring(1,2));

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

    }

    /**
     * On reset, we clear the winner label and hide it, then clear out each button.
     * We also tell the model to reset (restart) it's state.
     */
    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很好的将model和view分离。model可以很容易的进行单元测试,因为它没有关联其他任何模块,view
不用进行什么单元测试,但Controller测试起来有一些问题。

Controller的问题

测试性:controller与Android API紧耦合,所以很难对它进行单元测试。
模块化&灵活性:controller与view紧耦合,如果我们修改了view,那么回头来还要修改controller。
可维护性:随着时间的推移,越来越多的代码将会加入Controller,导致它变得臃肿和脆弱。
如何才能解决这些问题?MVP来了!

MVP

MVP拆解了controller,所以view/activity这种天然的耦合就不会出现在controller的职责范围里。先来看看MVP通常的职责定义与MVC之间的区别。

Model

和MVC相同

View

唯一的改变就是 Activity/Fragment现在被视作view中的一部分。好的实践是让Activity实现view的接口,以便presenter调用。这就消除了presenter与某个具体view的耦合,并且可以通过mock一个view来做单元测试。

Presenter

基本上相当于MVC中的controller,除了它完全与view没有耦合这一点有所不同。这就解决了MVC中存在的测试性和模块化&灵活性这些问题。实际上纯粹的MVP倡导者更进一步的要求presenter不应该引用任何Android的API或者代码。
再来看看Tic Tac Toe的总体结构

Android 中MVC、MVP和MVVM比较_第3张图片
MVP.png

下面看看Presenter的细节。首先你会注意到每个action十分简洁和清晰。它们不是告诉view怎样展现一些东西,而只是告诉它要展现什么。

public class TicTacToePresenter implements Presenter {

    private TicTacToeView view;
    private Board model;

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

    // Here we implement delegate methods for the standard Android Activity Lifecycle.
    // These methods are defined in the Presenter interface that we are implementing.
    public void onCreate() { model = new Board(); }
    public void onPause() { }
    public void onResume() { }
    public void onDestroy() { }

    /** 
     * When the user selects a cell, our presenter only hears about
     * what was (row, col) pressed, it's up to the view now to determine that from
     * the Button that was pressed.
     */
    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());
            }
        }
    }

    /**
     *  When we need to reset, we just dictate what to do.
     */
    public void onResetSelected() {
        view.clearWinnerDisplay();
        view.clearButtons();
        model.restart();
    }
}

为了presenter不引用Activity就能工作起来,我们创建了一个接口并让Activity实现。在测试中,我们可以创建一个mock对象实现这个接口,从而可以测试presenter和view的交互

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

评价

现在已经很干净了。我们可以轻松地对presenter的逻辑进行单元测试了,因为它已经没有和任何的Android的API有关系了,并且它还允许我们实现各种view,唯一的条件就是这些view实现TicTacToeView接口。

Presenter的问题

可维护性:presenter和controller一样,有集成业务逻辑的趋势。在某个时间点上,开发者常常会发现他们面对庞大笨拙的presenter已经很难去拆分了。把业务逻辑加进来有时会很便利,这是种很诱人的诱惑,当然,仔细的开发者可以通过努力来阻止这种诱惑,不过MVVM可以通过更少的付出帮助解决这种问题。

MVVM

MVVM通过 Data Binding on Android拥有容易测试和模块化的好处,同时减少了连接view和model的代码。让我们看看MVVM的每个部分。

Model

和 MVC一样

View

view绑定ViewModel 暴露出来的可观察(observable )的变量和action,只用几分钟时间。

ViewModel

ViewModel 的职责是封装model并且准备好view需要的数据(observable )。它也提供了一些hook供view传递事件到model。ViewModel完全和view没有关联。
Tic Tac Toe的总体结构

Android 中MVC、MVP和MVVM比较_第4张图片
MVVM.png

让我们看看变化了的部分,先从ViewModel开始

public class TicTacToeViewModel implements ViewModel {

    private Board model;

    /* 
     * These are observable variables that the viewModel will update as appropriate
     * The view components are bound directly to these objects and react to changes
     * immediately, without the ViewModel needing to tell it to do so. They don't
     * have to be public, they could be private with a public getter method too.
     */
    public final ObservableArrayMap cells = new ObservableArrayMap<>();
    public final ObservableField winner = new ObservableField<>();

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

    // As with presenter, we implement standard lifecycle methods from the view
    // in case we need to do anything with our model during those events.
    public void onCreate() { }
    public void onPause() { }
    public void onResume() { }
    public void onDestroy() { }

    /**
     * An Action, callable by the view.  This action will pass a message to the model
     * for the cell clicked and then update the observable fields with the current
     * model state.
     */
    public void onClickedCellAt(int row, int col) {
        Player playerThatMoved = model.mark(row, col);
        cells.put("" + row + col, playerThatMoved == null ? 
                                                     null : playerThatMoved.toString());
        winner.set(model.getWinner() == null ? null : model.getWinner().toString());
    }

    /**
     * An Action, callable by the view.  This action will pass a message to the model
     * to restart and then clear the observable data in this ViewModel.
     */
    public void onResetSelected() {
        model.restart();
        winner.set(null);
        cells.clear();
    }

}

摘录一部分view,看看变量和action是怎么被绑定的




    
    
        
        
    
    
        
            
            
            

评价

单元测试更容易了,因为你完全不依赖view了。当测试时,你只需要验证当model发生变化时,可观察的变量都被正确的设置。不用mock一个view来进行测试了。

MVVM的问题

可维护性:因为view可以绑定变量和表达式,无关的显示逻辑会渐渐滋生,实际的表现形式就是XML中的代码会增多。要防止这个问题,可以通过直接从ViewModel中获取变量而不是通过绑定表达式的方式。这种方法可以让计算过程得到适当的测试(XML是不能进行单元测试的)。

总结

MVP和MVVM在分解app使其模块化方面都比MVC更好,但它们也带来了复杂性。对于一个只有一两个界面的app,MVC可以很好地工作。MVVM的数据绑定很吸引力,它是更灵活的编程模式并且可以写更少的代码。
哪一个模式最适合你呢?那就是选择更适合项目的模式。要模式,但不要模式过度了。(这一句是我自己的体会)

你可能感兴趣的:(Android 中MVC、MVP和MVVM比较)