MVC、MVP、MVVM三雄爭霸
經過這些年的發展,對於如何將安卓應用合理地架構的探索有了很大的變化,安卓社區大部分都拋棄了原先的MVC架構而選擇了更加模塊化、有利於單元測試的架構。
MVP(Model Vier Prosenter) 和 MVVM(Model View ViewModel) 是當下兩個廣受歡迎的新架構方案,但一山不容二虎,開發者們常常因爲這兩個架構中哪個架構更適合安卓開發而產生分歧。在過去的幾年中,已經有許多的博文表達了針鋒相對的觀點,但它們往往都最終變成了對於判斷標準的爭論。
本文不會去爭論哪種架構更好,只會客觀地關注於它們各自的價值所在以及一些潛在的問題,這樣子讀者就可以自己做出明智的判斷
爲了幫助我們更好地理解每種架構,我們使用了一個井字遊戲APP作爲例子。
本文接下來會依次按照MVC、MVP、MVVM的順序講述,在每個部分最開始的時候我們會給出該架構中每個模塊的定義以及各自的職責,以及它們是如何應用於我們的井字遊戲的。
本文相關源碼可以在我們的GitHub倉庫中獲得
MVC
將代碼分割成Model、View以及Controller的方法將應用從宏觀層面上按職責分爲了三個部分
Model
Model層是數據(Data)、狀態(State)和商業邏輯(Business Logic) 的集合體,它是我們應用的“大腦”。這個模塊不依存於View或者Controller,所以它在多個不同的上下文中可以被複用。
View
View層是Model層的外在表現,它的職責是渲染UI界面,以及負責在用戶與應用交互時通知Controller層。
View層看起來似乎有些“笨拙”,因爲它不知道底層的模塊細節,並且對應用的狀態一無所知,甚至不知道當用戶點擊一個按鈕時該做些什麼。
這麼設計是因爲View層對其它的模塊瞭解越少,它們之間的耦合度越低,低耦合也就意味着當某個模塊發生變更時改變的細節也對外界屏蔽,整個應用更容易被修改。
Controller
Controller層更像是“膠水層”,用於將整個App組織起來。它是整個應用交互流程的控制着,當View層通知Controller層用戶交互時,它要決定如何作出響應。
取決於model層中數據的變化,Controller層會適當地更新View層的狀態。
Controller層在Android應用中通常作爲Activity類或Fragment類呈現。
從全局角度來看我們的應用分層以及各個模塊中的文件如下
讓我們更加詳細地來研究我們App中的Controller
public class TicTacToeActivity extends AppCompatActivity {
private Board model;
/* 控件的引用 */
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
/**
* 在onCreate方法中,我們查找控件並保留引用
* 以及實例化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();
}
/**
* 在這個方法中我們將重置按鈕添加到menu中
*/
@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);
}
}
/**
* 當一個格子被點擊時就會觸發這個方法
* 這個方法會去更新model並且根據返回值決定接下來的操作
* 如果玩家X或玩家0獲得勝利,就更新View
* 顯示結果,否則標記格子
*/
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);
}
}
}
/**
* 重置時會清除勝利標誌並且隱藏它,並且清除每個按鈕
* 同時通知model執行重置操作
*/
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層分開。在這種架構下Model層可以很容易地進行單元測試,因爲它不依存於其它層,View層因爲很薄所以不需要單元測試。
然而Controller層在單元測試方面有一些困難:
- 可測試性:Controller層與Android的API密切相關,所以難以進行單元測試
- 模塊化和靈活性:Controller層與View層的耦合度高,Controller也可以說是View層的延申。如果我們改變View層,我們必須跟着修改Controller層
- 可維護性:隨着時間的推移,Controller層的代碼會變得越來越多,使得Controller層越來越腫且可維護性變得很差
誰能解決這些困難?非MVP莫屬!
MVP
MVP架構將原先的Controller層拆散,使得原先Controller層中的Activity與原先的View層耦合在一起形成新的View層。
在講下面的內容之前,我們仍然先從各個層的定義開始介紹
Model
和MVC中的Model層完全一樣
View
相對MVC唯一的變化是原先Controller層中的Activity、Fragment現在歸到了View層中。
在編碼中,比較好的習慣是讓Activity(Fragment)去實現接口,Presenter引用的是接口的實現類。這麼做可以使得Presenter與View中具體類之間解耦,只要讓一個類去模擬實現這個接口即可對Presenter進行單元測試
Presenter
本質上這類似MVC中的Controller,只不過它不再與View層有多少聯繫,僅僅是引用了一個接口。這樣解決了原先MVC中可測試性、模塊性和靈活性的問題。
實際上,MVP的純粹主義者主張Presenter中不應該引用任何的Android API。(否則將難以進行單元測試)
再讓我們研究一下我們的APP中的MVP架構
public class TicTacToePresenter implements Presenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
// 這裏我們爲安卓Activity的生命週期實現了代理方法
// 這些方法定義在我們所實現的接口中
public void onCreate() { model = new Board(); }
public void onPause() { }
public void onResume() { }
public void onDestroy() { }
/**
* 當用戶選擇一個格子時,presenter會觸發這個方法
*/
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());
}
}
}
/**
* 當需要重置時,presenter需要指揮view和model執行相應操作
*/
public void onResetSelected() {
view.clearWinnerDisplay();
view.clearButtons();
model.restart();
}
}
爲了實現Activity(Fragment)與Presenter的解耦,我們創建一個接口,讓Activity(Fragment)來實現這個接口。
在單元測試中,我們會創建一個模擬類來實現這個接口去測試Presenter
public interface TicTacToeView {
void showWinner(String winningPlayerDisplayLabel);
void clearWinnerDisplay();
void clearButtons();
void setButtonText(int row, int col, String text);
}
架構評價
MVP架構明顯比MVC更加清晰,我們可以很容易地對Presenter層進行單元測試,因爲它不再與View層中的具體類有關聯,也不再涉及Android的API。
模塊化和靈活性也得到了提高,MVP允許我們對View中的具體實現類進行替換,只要新的類也實現了我們所定義的接口即可
Presenter層存在的問題
- 可維護性:就像MVC中的Controller一樣,傾向於隨着時間的推移集合過多的商業邏輯,從而變得非常的臃腫。在某些時刻,開發者們往往會發現Presenter層變得非常笨重,難以拆分。
當然,細心的開發者們可以小心謹慎地在應用迭代的過程中防止這種傾向。但MVVM給我們提供瞭解決這個問題更簡單的方法。
MVVM
MVVM架構搭配Android提供的Databinding有利於更簡單的測試以及模塊化,同時減少了用於連接View層和Model層的“膠水”代碼
Model
與MVC、MVP相同
View
View層會用一種靈活的方式與ViewModel層暴露出來的observable變量以及方法進行綁定,詳見後文。
ViewModel
ViewModel層的職責是將model層包裹起來,並準備好View層所需要的數據(Observable data)。
它還提供了View層向Model層傳遞事件的途徑。
從整體來看這個架構:
讓我們來看看變化的部分,從ViewModel開始
public class TicTacToeViewModel implements ViewModel {
private Board model;
/*
* 這些都是被觀察的變量,ViewModel會在合適的時候更新它們
* View層的控件直接綁定到這些被觀察的對象,控件會隨着它們的變化而改變
* 不需要ViewModel去主動調用更新
* 這些變量不一定要聲明爲public,也可以聲明爲private
* 只要有getter和setter
*/
public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>();
public final ObservableField<String> winner = new ObservableField<>();
public TicTacToeViewModel() {
model = new Board();
}
// 就像在MVP的Presenter一樣,我們實現了標準的生命週期方法
// 以防我們需要在這些時刻對model進行一些操作
public void onCreate() { }
public void onPause() { }
public void onResume() { }
public void onDestroy() { }
/**
* 一個方法,被綁定到View上,view被點擊時會觸發這個方法
* 這個方法會去操作model並且更新observable變量
*/
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());
}
/**
* 類似上面的方法
*/
public void onResetSelected() {
model.restart();
winner.set(null);
cells.clear();
}
}
以下是view層中佈局文件的部分摘錄,以展示如何將控件與可觀察變量綁定在一起
<!--
使用了Data Binding時,根元素是<layout>,它包含兩部分
1. <data> - 我們在此定義了我們希望在數據綁定表達式中使用到的變量,以及引入了其它我們會用到的類,比如android.view.View
2. <root layout> - 這是佈局中可見部分的根佈局,也是MVC、MVP中的根節點
-->
<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">
<!-- 我們接下來就可以用viewModel來引用TicTacToeViewModel,因爲這裏已經進行了定義 -->
<data>
<import type="android.view.View" />
<variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" />
</data>
<LinearLayout...>
<GridLayout...>
<!--每個格子都綁定了onClick方法,格子被點擊時會觸發onClickedCellAt方法並帶有行、列參數 -->
<!-- 要展示的值來自於ViewModel中定義的ObservableArrayMap-->
<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>
<!-- 展示獲勝者的控件是否展示取決於winner的值是否爲空
應該注意的是我們不應該在View層中加入展示的邏輯,然而在這裏我們這麼寫是有意義的,因爲它可以在合適的時候顯示出來
如果我們在winner值爲空的時候也去渲染這個控件就會顯得很奇怪 -->
<LinearLayout...
android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
tools:visibility="visible">
<!-- winner標籤的值會隨着viewModel中winner變量的值的變化而變化-->
<TextView
...
android:text="@{viewModel.winner}"
tools:text="X" />
...
</LinearLayout>
</LinearLayout>
</layout>
Tip: 要好好利用tools這個命名空間,在上面的例子中我們用到了tools:text這個屬性來設置winner標籤的值以及visibility的值,如果不設置它們的話,在預覽的時候就很難看出效果
注: 關於MVVM和Data Binding,以上只是冰山一角,強烈建議讀者去閱讀Android Data Binding文檔來學習這個非常有用的工具。在本文的底部也有一個Goodle Android Architecture Blueprints項目的鏈接,裏面有一些使用了MVVM和Data Binding的示範項目
架構評價
現在單元測試已經變得更簡單了,因爲別的層不再依賴於View層。當進行測試時,你只需要確保observable變量可以隨着Model層的變化恰當地改變,不需要再像MVP那樣模擬View層去測試。
MVVM存在的問題
- 可維護性 - 雖然View層的控件可以被綁定到ViewModel中的變量及方法,但隨着時間的流逝多餘的顯示邏輯可能會潛入XML文件中。爲了避免這種情況,我們應該直接從ViewModel中去拿數據,而不是試圖在View層中進行計算或邏輯控制。這樣才能將所有的計算以及邏輯集中到ViewModel中進行單元測試
結論
在模塊化、模塊職責單一化方面,MVP和MVVM都比MVC表現的要好,但是同時他們也讓你的App變得更加複雜。對於只有一兩個頁面的小應用來說,MVC還能夠勝任。MVVM搭配data binding的組合非常有吸引力,因爲它遵循了響應式編程的思想,代碼量也更少。
所以到底哪個架構最適合你?如果你正在MVP和MVVM中糾結的話,你可能會從一個主觀個人喜好的角度來抉擇,實際上看看他們在實際項目中的表現可以更好地幫助你理解它們各自的優點以及做出權衡。
如果你有興趣看看更多MVP和MVVM的實際項目的話,我推薦你看看Goodle Architecture Blueprints這個項目。也有許多其它博客鑽研於如何更好地實現MVP的架構。