關於MVC/MVP/MVVM的一些錯誤認識

在 Android 開發中使用 MVP 和 MVVM 模式早已不是新鮮事了,各類 MVP/MVVM 相關的文章、開源庫也已家常便飯,甚至是讓人眼花撩亂,那麼我爲何還要在這個早已被畫滿塗鴉的黑板上再來塗塗畫畫呢?是想彰顯個人存在感嗎?那固然!啊不不不……不徹底是!我還想要警醒讀到這篇文章的各位:大家對於MVX的理解可能並不徹底正確!html

注:這篇文章裏我將使用 MVX 作爲 MVC 、MVP 以及 MVVM 的統稱。java

咱們都知道 MVX 的進化過程是從滾球獸進化到 MVC ,而後從 MVC 進化到 MVP,再從 MVP 超進化到 MVVM。那麼接下來,按照常規的套路,我應該要介紹什麼是 MVC,什麼是 MVP,以及什麼是 MVVM,而且分別介紹M、V、C/P/VM 各自的職責了。數據庫

達嘎,口頭瓦魯!後端

可是,我拒絕!

個人目的是想要糾正一些對 MVX 的錯誤認識,因此前提是你要對 MVX 有一些瞭解。爲了不有人在使用 MVX 時走上彎路,因此決定對我看到的一些關於 MVX 的錯誤認識進行總結以及糾正。會產生這些錯誤認知的緣由,經我分析,實際上是:沒有真正領會到 MVX 主義的核心價值觀!其實 MVX 的核心思想也很簡單,不要誤會,不是富強、民主、……而是 將表現層和業務層分離網絡

表現層和業務層分離

表現層和業務層分離,Matin Fowler 稱之爲 Separated Presentation。這裏的表現層就是 VX,業務層就是 M。若是有人看到這裏發現了和你認爲的 MVX 不同的話,那麼你對 MVX 的認識極可能就存在錯誤,嚴重者還多是走了修正主義路線!數據結構

從表現層和業務層分離的視角來看,M、V、X不是平等的身份,應該是 M 和 V-X。自始自終 M 的職責都沒變,變的是 V-X,隨着軟件開發技術的發展、交互形式或者交互媒介的不斷改變,表現層的邏輯也越來複雜,MVX 的進化過程就是一個不斷探尋處理表現層複雜邏輯的過程。固然從一個形態進化到另外一個形態,並不必定是爲了解決更復雜的交互邏輯,也多是有了一種「更優雅」的方式來處理表現層邏輯。架構

既然已經有表現層和業務層分離的概念了,那麼第一個錯誤觀點就很好解釋了。app

錯誤一:Presenter 或者 ViewModel 負責處理業務邏輯

這是一個很常見的錯誤觀點,不少介紹 MVP 或者 MVVM 的文章都這麼說過。正如前面所說,業務邏輯是屬於 M 層的,那 Presenter 或者 ViewModel 是幹什麼的,處理表現層邏輯的嗎?是的,或者說大部分表現層邏輯都是在 Presenter 或者 ViewModel 中處理的。以前我將業務層之上的這些邏輯稱之爲視圖邏輯,如今爲了統一就叫作表現層邏輯吧(加個吧字怎麼感受怪怪的)。dom

我在這裏就簡單說一下什麼是表現層邏輯,以及 View 和 Presenter/ViewModel 又是如何分工的。假設你的應用有一個我的資料的 profile 頁面,這個頁面有兩種狀態,一種是瀏覽狀態,一種是編輯狀態,經過一個編輯按鈕觸發狀態的轉換,編輯狀態時,部分信息項能夠進行編輯。那這裏就有一個明顯的表現層邏輯,那就是點擊按鈕切換瀏覽/編輯狀態。ide

如今的 MVP 的流行形態(或者變種)叫作 Passive View,它和 MVVM 同樣如今都傾向於將幾乎全部的表現層邏輯交給 Presenter 或者 ViewModel 處理,View 層須要作的事情不多,基本上就是接受用戶事件,而後將用戶事件傳遞給 Presenter 或者 ViewModel。以上面的 profile 頁面的例子來解釋的話就是,View 層負責接收編輯按鈕的點擊事件,而後通知 Presenter/ViewModel,而後 Presenter/ViewModel 通知 View 是顯示瀏覽狀態的視圖仍是編輯狀態的視圖。MVP 的示例代碼大概是這樣的:

public class ProfileView {

	void initView() {
		// 負責註冊點擊事件監聽器,並將點擊事件通知給presenter
		editStateButton.setOnClickListener(new OnClickListener() {
			presenter.onEditStateButtonClicked();
		})
		...
	}

    // 顯示瀏覽狀態視圖,想不到好名字,就叫showNormalState吧
	public void showNormalState() {
		// 瀏覽狀態下編輯按鈕提示文字爲「編輯」,全部項不可編輯
		editStateButton.setText("編輯");
		nickName.setEditable(false);
		...
	}

	public void showEditState() {
		// 瀏覽狀態下編輯按鈕提示文字爲「完成」,部分項要設置爲可編輯
		editStateButton.setText("完成");
		nickName.setEditable(true);
		...
	}
}
複製代碼
public class ProfilePresenter {

	private State curState = State.NORMAL;

	public void onEditStateButtonClicked() {
		// 按鈕被點擊時,根據當前狀態判斷View應該切換顯示的狀態
		// 這就是表現層邏輯
		if (isInEditState()) {
			curState = State.NORMAL;
			view.showNormalState();
		} else {
			curState = State.EDIT;
			view.showEditState();
		}
	}

	private boolean isInEditState() {
		return curState == State.EDIT;
	}

	@VisibleForTest
	void setState(State state) {
		curState = state;
	}
}
複製代碼

注:這個示例代碼只是爲了展現表現層邏輯,沒有涉及到Model層,編譯也不會經過的!

能感覺到我想表達的意思嗎?就是 Presenter/ViewModel 根據當前交互狀態決定該顯示什麼,而 View 要作的是如何顯示它們。再好比說下拉刷新的場景,由 View 告訴 Presenter/ViewModel,它接收到了下拉事件,而後 Presenter/ViewModel 再告訴 View,讓它去顯示刷新提示視圖,至於這個刷新提示長什麼樣就由 View來決定。固然 Presenter/ViewModel 也可能會判斷當前網絡不可用,而讓 View 顯示一個網絡不可用的提示視圖。

爲何要讓 Presenter/ViewModel 處理幾乎全部的表現層邏輯呢?主要是爲了提升可測試性,將盡量多的表現層邏輯歸入到單元測試的範圍內。由於對視圖控件的顯示等等進行單元測試太難了,因此 View 是基本上無法進行單元測試的,可是 Presenter/ViewModel 是徹底能夠進行單元測試的:

public class ProfilePresenterTest {

	private ProfilePresenter presenter;
	private ProfileView view;

	@Test
	public void testShowEditStateOnButtonClick() {
		// 瀏覽狀態下點擊編輯按鈕,驗證View是否顯示了編輯狀態視圖
		// 也就是驗證view.showEditState()方法是否被調用了
		presenter.setState(State.NORMAL);
		presenter.onEditStateButtonClicked();
		Mockito.verify(view).showEditState();
	}

	@Test
	public void testShowNormalStateOnButtonClick() {
		// 編輯狀態下點擊完成按鈕,驗證View是否顯示了瀏覽狀態視圖
		// 也就是驗證view.showNormalState()方法是否被調用了
		presenter.setState(State.EDIT);
		presenter.onEditStateButtonClicked();
		Mockito.verify(view).showNormalState();
	}
}
複製代碼

你看,這些表現層邏輯就都能進行單元測試了吧!大概懂我意思了吧?

懂我意思吧

OK,如今你已經知道表現層了,那業務層又是幹什麼用的呢?如今咱們就要開始談到 M 了。

M 是什麼?M 是指那些喜歡從受虐中得到性……哎呀,很差意思,搞混了!哎~學識淵博就是麻煩!M 者,Model 也,再長一點就是 Domain Model,中文名字叫領域模型。咱們看一下維基百科上對 Domain model 的定義:

In software engineering, a domain model is a conceptual model of the domain that incorporates both behaviour and data.

怎麼樣,是否是很通俗易懂呀?固然不是!剛剛開始有點理解Model層是處理業務邏輯的,如今又來了個抖MMM…… Domain,我都不知道該往哪裏去想了!Domain,簡單點就把它理解成業務,我以爲都沒啥問題。我這裏引用這句話,主要是想強調,Model 層包含了業務數據以及對業務數據的操做 (behaviour and data),也是爲了引出第二個錯誤觀點。

錯誤二:Model 就是靜態的業務數據

咱們作業務模塊開發時,會常常定義一些數據結構類,好比我的資料可能會對應一個 UserProfile 類,一條訂單數據可能會對應一個 Order 類,這些類沒有任何邏輯,只有一些簡單的 getter、setter 方法。有些人會認爲像 UserProfile 或者 Order 這樣的數據結構類就是 Model。

咱們已經強調了,Model 層包含了業務數據以及對業務數據的操做。像 UserProfile 或者 Order 這樣的數據結構類的實例甚至都不能稱之爲對象,能夠看一下 Uncle Bob 的 Classes vs. Data Structures 這篇文章,對象是有行爲的,一個數據結構實例沒有行爲,連對象都稱不上,怎麼能表明 Model 層呢!

靜態的業務數據不能表明 Model 層,業務數據以及針對業務數據的操做共同構成了 Model 層,這也就是業務邏輯。再舉個例子說一下吧,假設你在作一個叫「掘鐵」的 app,這個 app 如今只有一個頁面,用來展現推薦的博客列表。OK,咱們若是用 MVP 的形式該怎麼寫呢?咱們就先無論和 Model 層徹底沒有交互的 View 了,Presenter 層除了處理表現層邏輯外,還要向 Model 層發出業務指令,注意,Presenter 並不處理業務邏輯,真正的業務邏輯仍是由 Model 層完成。示例代碼大概是下面這樣:

public class RecommendBlogFeedPresenter {

	private RecommendBlogFeedView view;
	private BlogMode model;

	public void onStart() {
		view.showLoadWait();
		model.loadRecommendBlogs(new LoadCallback<>() {

			@Override
			public void onLoaded(List<Blog> blogs) {
				view.showBlogs(blogs);
			}
		})
	}

}
複製代碼
public interface BlogModel {

	void loadRecommendBlogs(LoadCallback<List<Blog>> callback);
}

public class BlogModelImpl implements BlogModel {

	private BlogFeedRepository repo;

	@Override
	public void loadRecommendBlogs(LoadCallback<List<Blog>> callback) {
		// BlogFeedRepository.fetch()極可能是耗時操做,因此實際寫的時候會在非主線程執行,這裏只是示例
		callback.onLoaded(repo.fetch("recommend"));
	}
}

public interface BlogFeedRepository {

	List<Blog> fetch(String tag);
}
複製代碼

什麼?你這個 BlogModelImpl 裏就這一行代碼,你跟我說這是業務邏輯?你們冷靜一下,把手裏的板磚、砍刀、狼牙棒先放下來。BlogModelImpl 類裏面的邏輯雖然簡單,可是它的確是業務邏輯,也正是由於業務邏輯比較簡單,因此 BlogModelImpl 類纔會很簡潔。

再從 Presenter 的角度看一下,爲何 loadRecommendBlogs() 屬於業務邏輯。博客這個概念毫無疑問屬於業務概念,根據前面的解釋應該能夠判斷出來「獲取推薦的博客列表」不屬於表現層邏輯,那麼這個邏輯的實現就不是 Presenter 須要關心的,那就應該是 Model 層的職責,既然是 Model 層的那就應該是業務邏輯了;再者,既然博客是業務概念,那麼 Blog 就是業務數據的數據結構,loadRecommendBlogs() 涉及到對業務數據 Blog 的建立及組裝等操做,因此也應該是業務邏輯。

看到這裏,可能有些人會產生一些誤解:所謂的業務邏輯處理就是網絡請求、數據庫查詢等數據獲取邏輯,即Model層就是負責數據獲取的,這也是我要說的第三個錯誤觀點。稍等,我先寫個標題⬇

錯誤三:Model 層就是負責數據獲取的

產生這種錯誤認識的,說白了仍是沒有搞懂業務邏輯。固然了業務邏輯自己就是很抽象的概念,難理解,也很難區分,我也不敢往細了去說,由於說多了怕被大家發現其實我也是在裸泳。

業務邏輯層並不負責數據的獲取,數據的獲取職責還要在 Model 層的更下層,這也是爲何我要把的 BlogModel 的實現邏輯寫得如此簡單,由於數據獲取的職責所有交給了 BlogFeedRepository 類,Model 層只處理業務邏輯。BlogFeedRepository 是博客列表的倉儲類,BlogModel 經過 BlogFeedRepositoryfetch() 方法獲取標籤爲 recommend 的博客列表,也就是推薦的博客列表。BlogModel 不關心 BlogFeedRepository 是如何獲取對應博客數據的,它能夠是從經過網絡請求獲取的,也能夠是從本地數據庫中獲取的,數據源有任何改變也不該該影響到 BlogModel 中的業務邏輯。

那麼既然 BlogModel 中的業務邏輯如此簡單,爲何要強行增長這麼一個 Model 層,而不是讓 Presenter 直接使用 BlogFeedRepository 類去獲取數據呢?

固然是有緣由的!假設咱們剛纔介紹的「掘鐵」 app,在僅有一個博客列表頁面的狀況下,依然吸引了不少用戶去使用,產品經理此時決定嘗試探索變現手段,首先是在博客推薦列表中添加廣告數據。再假設,因爲廣告數據和博客數據分屬不一樣的後端團隊,兩邊數據還沒有整合打通,暫時由客戶端負責把廣告數據添加到博客列表中。這個時候,BlogModel 終於凸顯了它存在的必要性。表現層不負責廣告數據的獲取與整合,BlogFeedRepository 也不能負責廣告數據的獲取與整合。廣告數據的整合是業務邏輯,由 BlogModel 負責,廣告數據的獲取由專門的數據倉儲類負責。示例代碼以下:

public class BlogModelImpl implements BlogModel {

	private BlogFeedRepository blogRepo;
	private AdRepository adRepo;
	private BlogAdComposeStrategy composeStrategy;
	private AdBlogTransform transform;

	@Override
	public void loadRecommendBlogs(LoadCallback<List<Blog>> callback) {
		List<BlogAd> ads = adRepo.fetch("recommend");
		List<Blog> blogs = blogRepo.fetch("recommend");
		// 在這裏把廣告數據整合到博客列表中
		blogs = composeStrategy.compose(blogs, ads, transform);
		callback.onLoaded(blogs);
	}
}

public interface AdRepository {
	List<BlogAd> fetch(String tag);
}

public interface BlogAdComposeStrategy {
	List<Blog> compose(List<Blog> blogs, List<BlogAd> ads, AdBlogTransform transoform);
}

public interface AdBlogTransform {
	Blog transform(BlogAd ad);
}
複製代碼

考慮到廣告和博客可能有不一樣的整合策略,能夠按需替換不一樣的實現,因此把整合策略封裝到了 BlogAdComposeStrategy 接口中。整合策略也屬於業務邏輯,可是由於整合策略的實現細節這裏不須要關注,因此我以爲不寫出來也行,反正都是我編的。

這裏我想表達的是,獲取廣告數據並將廣告數據整合到博客列表中也是業務邏輯的一部分,若是省略 Model 層將會形成得把廣告的整合邏輯放到 Presenter 或者 Repository 層,這必然都是不合適的。將業務邏輯放到了錯誤的層次裏,勢必會形成後續的維護性和擴展性問題。

錯誤四:Model 層依賴 Presenter/ViewModel 層

還有一些人沒有搞清楚 Model 層和上層的依賴關係,依賴關係寫成了雙向的,這是不對的,業務層不該該依賴表現層,而是應該反過來。

實際上應該是 Presenter/ViewModel 經過接口的形式依賴 Model 層,Model 層徹底不依賴 Presenter/ViewModel。就像我前面的示例代碼裏同樣,Model 層必然不會出現任何 presenter 這樣的單詞,上層經過觀察者模式來監聽 Model 層的數據變化( LoadCallback 接口也算是一種),Model 層也不用關心上層是 Presenter 仍是 ViewModel。

最後

讀到這裏,不知道大家對MVX的理解是否是更深了些呢?對錶現層邏輯、業務邏輯是否是也有了更清晰的認識了呢?

其實關於 MVX 還有更多能夠討論的,好比有些人認爲 Model 層並非真正處理業務邏輯的地方,它只是業務模塊的一個上層封裝層,我以爲也不無道理,在複雜業務模塊中,業務是存在層次的,MVX 中的 Model 層是全部業務層中的最上層。

還有我剛剛提到的業務層之下還有數據層,這是典型的三層架構的概念,即表現層、業務層和數據層。邏輯存在分層,因此架構也必然要進行分層,MVX 能夠作爲咱們從代碼到業務甚至到架構的探索的開端。

相關文章
相關標籤/搜索