项目中用到了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 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的数据绑定很吸引力,它是更灵活的编程模式并且可以写更少的代码。
哪一个模式最适合你呢?那就是选择更适合项目的模式。要模式,但不要模式过度了。(这一句是我自己的体会)