项目中用到了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
可以通过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的总体结构和各个类所扮演的角色
让我们详细的看看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的总体结构
下面看看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的总体结构
让我们看看变化了的部分,先从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的数据绑定很吸引力,它是更灵活的编程模式并且可以写更少的代码。
哪一个模式最适合你呢?那就是选择更适合项目的模式。要模式,但不要模式过度了。(这一句是我自己的体会)