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

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的总体结构和各个类所扮演的角色

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的总体结构

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的总体结构

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<String, String> cells = new ObservableArrayMap<>();
    public final ObservableField<String> 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是怎么被绑定的

<!-- 
    With Data Binding, the root element is <layout>.  It contains 2 things.
    1. <data> - We define variables to which we wish to use in our binding expressions and 
                import any other classes we may need for reference, like android.view.View.
    2. <root layout> - This is the visual root layout of our view.  This is the root xml tag in the MVC and MVP view examples.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- We will reference the TicTacToeViewModel by the name viewModel as we have defined it here. -->
    <data>
        <import type="android.view.View" />
        <variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" />
    </data>
    <LinearLayout...>
        <GridLayout...>
            <!-- onClick of any cell in the board, the button clicked will invoke the onClickedCellAt method with its row,col -->
            <!-- The display value comes from the ObservableArrayMap defined in the ViewModel  -->
            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}"
                android:text='@{viewModel.cells["00"]}' />
            ...
            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> viewModel.onClickedCellAt(2,2)}"
                android:text='@{viewModel.cells["22"]}' />
        </GridLayout>

        <!-- The visibility of the winner view group is based on whether or not the winner value is null.
             Caution should be used not to add presentation logic into the view.  However, for this case
             it makes sense to just set visibility accordingly.  It would be odd for the view to render
             this section if the value for winner were empty.  -->
        <LinearLayout...
            android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
            tools:visibility="visible">

            <!-- The value of the winner label is bound to the viewModel.winner and reacts if that value changes -->
            <TextView
                ...
                android:text="@{viewModel.winner}"
                tools:text="X" />
            ...
        </LinearLayout>
    </LinearLayout>
</layout>

评价

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

MVVM的问题

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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容