關(guān)于MVC/MVP/MVVM的一些錯誤認(rèn)識
在Android開發(fā)中使用MVP和MVVM模式早已不是新鮮事了,各種MVP/MVVM相關(guān)的文章、開源庫也已屢見不鮮,甚至是讓人眼花撩亂,那么我為什么還要在這個早已被畫滿涂鴉的黑板上再來涂涂畫畫呢?是想彰顯我的存在感嗎?那當(dāng)然!啊不不不……不完全是!我還想要警醒讀到這篇文章的各位:你們對于MVX的理解可能并不完全正確!
注:這篇文章里我將使用 MVX 做為MVC、MVP以及MVVM的統(tǒng)稱。
我們都知道MVX的進(jìn)化過程是從滾球獸進(jìn)化到MVC,然后從MVC進(jìn)化到MVP,再從MVP超進(jìn)化到MVVM。那么接下來,按照常規(guī)的套路,我應(yīng)該要介紹什么是MVC,什么是MVP,以及什么是MVVM,并且分別介紹M、V、C/P/VM各自的職責(zé)了。
我的目的是想要糾正一些對MVX的錯誤認(rèn)識,所以前提是你要對MVX有一些了解。為了避免有人在使用MVX時走上彎路,所以決定對我看到的一些關(guān)于MVX的錯誤認(rèn)識進(jìn)行總結(jié)以及糾正。會產(chǎn)生這些錯誤認(rèn)知的原因,經(jīng)我分析,其實(shí)是:沒有真正領(lǐng)會到MVX主義的核心價值觀!其實(shí)MVX的核心思想也很簡單,不要誤會,不是富強(qiáng)、民主、……而是 將表現(xiàn)層和業(yè)務(wù)層分離 。
表現(xiàn)層和業(yè)務(wù)層分離
表現(xiàn)層和業(yè)務(wù)層分離,Matin Fowler稱之為Separated Presentation。這里的表現(xiàn)層就是VX,業(yè)務(wù)層就是M。如果有人看到這里發(fā)現(xiàn)了和你認(rèn)為的MVX不一樣的話,那么你對MVX的認(rèn)識很可能就存在錯誤,嚴(yán)重者還可能是走了修正主義路線!
從表現(xiàn)層和業(yè)務(wù)層分離的視角來看,M、V、X不是平等的身份,應(yīng)該是M和V-X。自始自終M的職責(zé)都沒變,變的是V-X,隨著軟件開發(fā)技術(shù)的發(fā)展、交互形式或者交互媒介的不斷改變,表現(xiàn)層的邏輯也越來復(fù)雜,MVX的進(jìn)化過程就是一個不斷探尋處理表現(xiàn)層復(fù)雜邏輯的過程。當(dāng)然從一個形態(tài)進(jìn)化到另一個形態(tài),并不一定是為了解決更復(fù)雜的交互邏輯,也可能是有了一種“更優(yōu)雅”的方式來處理表現(xiàn)層邏輯。
既然已經(jīng)有表現(xiàn)層和業(yè)務(wù)層分離的概念了,那么第一個錯誤觀點(diǎn)就很好解釋了。
錯誤一:Presenter或者ViewModel負(fù)責(zé)處理業(yè)務(wù)邏輯
這是一個很常見的錯誤觀點(diǎn),很多介紹MVP或者M(jìn)VVM的文章都這么說過。正如前面所說,業(yè)務(wù)邏輯是屬于M層的,那Presenter或者ViewModel是干什么的,處理表現(xiàn)層邏輯的嗎?是的,或者說大部分表現(xiàn)層邏輯都是在Presenter或者ViewModel中處理的。之前我將業(yè)務(wù)層之上的這些邏輯稱之為視圖邏輯,現(xiàn)在為了統(tǒng)一就叫做表現(xiàn)層邏輯吧(加個吧字怎么感覺怪怪的)。
我在這里就簡單說一下什么是表現(xiàn)層邏輯,以及View和Presenter/ViewModel又是如何分工的。假設(shè)你的應(yīng)用有一個個人資料的profile頁面,這個頁面有兩種狀態(tài),一種是瀏覽狀態(tài),一種是編輯狀態(tài),通過一個編輯按鈕觸發(fā)狀態(tài)的轉(zhuǎn)換,編輯狀態(tài)時,部分信息項(xiàng)可以進(jìn)行編輯。那這里就有一個明顯的表現(xiàn)層邏輯,那就是點(diǎn)擊按鈕切換瀏覽/編輯狀態(tài)。
現(xiàn)在的MVP的流行形態(tài)(或者變種)叫做Passive View,它和MVVM一樣現(xiàn)在都傾向于將幾乎所有的表現(xiàn)層邏輯交給Presenter或者ViewModel處理,View層需要做的事情很少,基本上就是接受用戶事件,然后將用戶事件傳遞給Presenter或者ViewModel。以上面的profile頁面的例子來解釋的話就是,View層負(fù)責(zé)接收編輯按鈕的點(diǎn)擊事件,然后通知Presenter/ViewModel,然后Presenter/ViewModel通知View是顯示瀏覽狀態(tài)的視圖還是編輯狀態(tài)的視圖。MVP的示例代碼大概是這樣的:
- public class ProfileView {
- void initView() {
- // 負(fù)責(zé)注冊點(diǎn)擊事件監(jiān)聽器,并將點(diǎn)擊事件通知給presenter
- editStateButton.setOnClickListener(new OnClickListener() {
- presenter.onEditStateButtonClicked();
- })
- ...
- }
- // 顯示瀏覽狀態(tài)視圖,想不到好名字,就叫showNormalState吧
- public void showNormalState() {
- // 瀏覽狀態(tài)下編輯按鈕提示文字為“編輯”,所有項(xiàng)不可編輯
- editStateButton.setText("編輯");
- nickName.setEditable(false);
- ...
- }
- public void showEditState() {
- // 瀏覽狀態(tài)下編輯按鈕提示文字為“完成”,部分項(xiàng)要設(shè)置為可編輯
- editStateButton.setText("完成");
- nickName.setEditable(true);
- ...
- }
- }
- public class ProfilePresenter {
- private State curState = State.NORMAL;
- public void onEditStateButtonClicked() {
- // 按鈕被點(diǎn)擊時,根據(jù)當(dāng)前狀態(tài)判斷View應(yīng)該切換顯示的狀態(tài)
- // 這就是表現(xiàn)層邏輯
- 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;
- }
- }
注:這個示例代碼只是為了展示表現(xiàn)層邏輯,沒有涉及到Model層,編譯也不會通過的!
能感受到我想表達(dá)的意思嗎?就是Presenter/ViewModel根據(jù)當(dāng)前交互狀態(tài)決定該顯示什么,而View要做的是如何顯示它們。再比如說下拉刷新的場景,由View告訴Presenter/ViewModel,它接收到了下拉事件,然后Presenter/ViewModel再告訴View,讓它去顯示刷新提示視圖,至于這個刷新提示長什么樣就由View來決定。當(dāng)然Presenter/ViewModel也可能會判斷當(dāng)前網(wǎng)絡(luò)不可用,而讓View顯示一個網(wǎng)絡(luò)不可用的提示視圖。
為什么要讓Presenter/ViewModel處理幾乎所有的表現(xiàn)層邏輯呢?主要是為了提高可測試性,將盡可能多的表現(xiàn)層邏輯納入到單元測試的范圍內(nèi)。因?yàn)閷σ晥D控件的顯示等等進(jìn)行單元測試太難了,所以View是基本上沒法進(jìn)行單元測試的,但是Presenter/ViewModel是完全可以進(jìn)行單元測試的:
- public class ProfilePresenterTest {
- private ProfilePresenter presenter;
- private ProfileView view;
- @Test
- public void testShowEditStateOnButtonClick() {
- // 瀏覽狀態(tài)下點(diǎn)擊編輯按鈕,驗(yàn)證View是否顯示了編輯狀態(tài)視圖
- // 也就是驗(yàn)證view.showEditState()方法是否被調(diào)用了
- presenter.setState(State.NORMAL);
- presenter.onEditStateButtonClicked();
- Mockito.verify(view).showEditState();
- }
- @Test
- public void testShowNormalStateOnButtonClick() {
- // 編輯狀態(tài)下點(diǎn)擊完成按鈕,驗(yàn)證View是否顯示了瀏覽狀態(tài)視圖
- // 也就是驗(yàn)證view.showNormalState()方法是否被調(diào)用了
- presenter.setState(State.EDIT);
- presenter.onEditStateButtonClicked();
- Mockito.verify(view).showNormalState();
- }
- }
你看,這些表現(xiàn)層邏輯就都能進(jìn)行單元測試了吧!大概懂我意思了吧?
OK,現(xiàn)在你已經(jīng)知道表現(xiàn)層了,那業(yè)務(wù)層又是干什么用的呢?現(xiàn)在我們就要開始談到M了。
M是什么?M是指那些喜歡從受虐中獲得性……哎呀,不好意思,搞混了!哎~學(xué)識淵博就是麻煩!M者,Model也,再長一點(diǎn)就是Domain Model,中文名字叫領(lǐng)域模型。我們看一下維基百科上對Domain model的定義:
- In software engineering, a domain model is a conceptual model of the domain that incorporates both behaviour and data.
怎么樣,是不是很通俗易懂呀?當(dāng)然不是!剛剛開始有點(diǎn)理解Model層是處理業(yè)務(wù)邏輯的,現(xiàn)在又來了個抖MMM……Domain,我都不知道該往哪里去想了!Domain,簡單點(diǎn)就把它理解成業(yè)務(wù),我覺得都沒啥問題。我這里引用這句話,主要是想強(qiáng)調(diào),Model層包含了業(yè)務(wù)數(shù)據(jù)以及對業(yè)務(wù)數(shù)據(jù)的操作(behaviour and data),也是為了引出第二個錯誤觀點(diǎn)。
錯誤二:Model就是靜態(tài)的業(yè)務(wù)數(shù)據(jù)
我們做業(yè)務(wù)模塊開發(fā)時,會經(jīng)常定義一些數(shù)據(jù)結(jié)構(gòu)類,比如個人資料可能會對應(yīng)一個UserProfile類,一條訂單數(shù)據(jù)可能會對應(yīng)一個Order類,這些類沒有任何邏輯,只有一些簡單的getter、setter方法。有些人會認(rèn)為像UserProfile或者Order這樣的數(shù)據(jù)結(jié)構(gòu)類就是Model。
我們已經(jīng)強(qiáng)調(diào)了,Model層包含了業(yè)務(wù)數(shù)據(jù)以及對業(yè)務(wù)數(shù)據(jù)的操作。像UserProfile或者Order這樣的數(shù)據(jù)結(jié)構(gòu)類的實(shí)例甚至都不能稱之為對象,可以看一下Uncle Bob的Classes vs. Data Structures這篇文章,對象是有行為的,一個數(shù)據(jù)結(jié)構(gòu)實(shí)例沒有行為,連對象都稱不上,怎么能代表Model層呢!
靜態(tài)的業(yè)務(wù)數(shù)據(jù)不能代表Model層,業(yè)務(wù)數(shù)據(jù)以及針對業(yè)務(wù)數(shù)據(jù)的操作共同構(gòu)成了Model層,這也就是業(yè)務(wù)邏輯。再舉個例子說一下吧,假設(shè)你在做一個叫“掘鐵”的app,這個app現(xiàn)在只有一個頁面,用來展示推薦的博客列表。OK,我們?nèi)绻肕VP的形式該怎么寫呢?我們就先不管和Model層完全沒有交互的View了,Presenter層除了處理表現(xiàn)層邏輯外,還要向Model層發(fā)出業(yè)務(wù)指令,注意,Presenter并不處理業(yè)務(wù)邏輯,真正的業(yè)務(wù)邏輯還是由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()很可能是耗時操作,所以實(shí)際寫的時候會在非主線程執(zhí)行,這里只是示例
- callback.onLoaded(repo.fetch("recommend"));
- }
- }
- public interface BlogFeedRepository {
- List<Blog> fetch(String tag);
- }
什么?你這個BlogModelImpl里就這一行代碼,你跟我說這是業(yè)務(wù)邏輯?大家冷靜一下,把手里的板磚、砍刀、狼牙棒先放下來。BlogModelImpl類里面的邏輯雖然簡單,但是它的確是業(yè)務(wù)邏輯,也正是因?yàn)闃I(yè)務(wù)邏輯比較簡單,所以BlogModelImpl類才會很簡潔。
再從Presenter的角度看一下,為什么loadRecommendBlogs()屬于業(yè)務(wù)邏輯。博客這個概念毫無疑問屬于業(yè)務(wù)概念,根據(jù)前面的解釋應(yīng)該可以判斷出來“獲取推薦的博客列表”不屬于表現(xiàn)層邏輯,那么這個邏輯的實(shí)現(xiàn)就不是Presenter需要關(guān)心的,那就應(yīng)該是Model層的職責(zé),既然是Model層的那就應(yīng)該是業(yè)務(wù)邏輯了;再者,既然博客是業(yè)務(wù)概念,那么Blog就是業(yè)務(wù)數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu),loadRecommendBlogs()涉及到對業(yè)務(wù)數(shù)據(jù)Blog的創(chuàng)建及組裝等操作,所以也應(yīng)該是業(yè)務(wù)邏輯。
看到這里,可能有些人會產(chǎn)生一些誤解:所謂的業(yè)務(wù)邏輯處理就是網(wǎng)絡(luò)請求、數(shù)據(jù)庫查詢等數(shù)據(jù)獲取邏輯,即Model層就是負(fù)責(zé)數(shù)據(jù)獲取的,這也是我要說的第三個錯誤觀點(diǎn)。稍等,我先寫個標(biāo)題⬇
錯誤三:Model層就是負(fù)責(zé)數(shù)據(jù)獲取的
產(chǎn)生這種錯誤認(rèn)識的,說白了還是沒有搞懂業(yè)務(wù)邏輯。當(dāng)然了業(yè)務(wù)邏輯本身就是很抽象的概念,難理解,也很難區(qū)分,我也不敢往細(xì)了去說,因?yàn)檎f多了怕被你們發(fā)現(xiàn)其實(shí)我也是在裸泳。
業(yè)務(wù)邏輯層并不負(fù)責(zé)數(shù)據(jù)的獲取,數(shù)據(jù)的獲取職責(zé)還要在Model層的更下層,這也是為什么我要把的BlogModel的實(shí)現(xiàn)邏輯寫得如此簡單,因?yàn)閿?shù)據(jù)獲取的職責(zé)全部交給了BlogFeedRepository類,Model層只處理業(yè)務(wù)邏輯。BlogFeedRepository是博客列表的倉儲類,BlogModel通過BlogFeedRepository的fetch()方法獲取標(biāo)簽為recommend的博客列表,也就是推薦的博客列表。BlogModel不關(guān)心BlogFeedRepository是如何獲取對應(yīng)博客數(shù)據(jù)的,它可以是從通過網(wǎng)絡(luò)請求獲取的,也可以是從本地數(shù)據(jù)庫中獲取的,數(shù)據(jù)源有任何改變也不應(yīng)該影響到BlogModel中的業(yè)務(wù)邏輯。
那么既然BlogModel中的業(yè)務(wù)邏輯如此簡單,為什么要強(qiáng)行增加這么一個Model層,而不是讓Presenter直接使用BlogFeedRepository類去獲取數(shù)據(jù)呢?
當(dāng)然是有原因的!假設(shè)我們剛才介紹的“掘鐵”app,在僅有一個博客列表頁面的情況下,依然吸引了很多用戶去使用,產(chǎn)品經(jīng)理此時決定嘗試探索變現(xiàn)手段,首先是在博客推薦列表中添加廣告數(shù)據(jù)。再假設(shè),由于廣告數(shù)據(jù)和博客數(shù)據(jù)分屬不同的后端團(tuán)隊(duì),兩邊數(shù)據(jù)尚未整合打通,暫時由客戶端負(fù)責(zé)把廣告數(shù)據(jù)添加到博客列表中。這個時候,BlogModel終于凸顯了它存在的必要性。表現(xiàn)層不負(fù)責(zé)廣告數(shù)據(jù)的獲取與整合,BlogFeedRepository也不能負(fù)責(zé)廣告數(shù)據(jù)的獲取與整合。廣告數(shù)據(jù)的整合是業(yè)務(wù)邏輯,由BlogModel負(fù)責(zé),廣告數(shù)據(jù)的獲取由專門的數(shù)據(jù)倉儲類負(fù)責(zé)。示例代碼如下:
- 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");
- // 在這里把廣告數(shù)據(jù)整合到博客列表中
- 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);
- }
考慮到廣告和博客可能有不同的整合策略,可以按需替換不同的實(shí)現(xiàn),所以把整合策略封裝到了BlogAdComposeStrategy接口中。整合策略也屬于業(yè)務(wù)邏輯,但是因?yàn)檎喜呗缘膶?shí)現(xiàn)細(xì)節(jié)這里不需要關(guān)注,所以我覺得不寫出來也行,反正都是我編的。
這里我想表達(dá)的是,獲取廣告數(shù)據(jù)并將廣告數(shù)據(jù)整合到博客列表中也是業(yè)務(wù)邏輯的一部分,如果省略Model層將會造成得把廣告的整合邏輯放到Presenter或者Repository層,這必然都是不合適的。將業(yè)務(wù)邏輯放到了錯誤的層次里,勢必會造成后續(xù)的維護(hù)性和擴(kuò)展性問題。
錯誤四:Model層依賴Presenter/ViewModel層
還有一些人沒有搞清楚Model層和上層的依賴關(guān)系,依賴關(guān)系寫成了雙向的,這是不對的,業(yè)務(wù)層不應(yīng)該依賴表現(xiàn)層,而是應(yīng)該反過來。
實(shí)際上應(yīng)該是Presenter/ViewModel通過接口的形式依賴Model層,Model層完全不依賴Presenter/ViewModel。就像我前面的示例代碼里一樣,Model層必然不會出現(xiàn)任何presenter這樣的單詞,上層通過觀察者模式來監(jiān)聽Model層的數(shù)據(jù)變化(LoadCallback接口也算是一種),Model層也不用關(guān)心上層是Presenter還是ViewModel。
最后
讀到這里,不知道你們對MVX的理解是不是更深了些呢?對表現(xiàn)層邏輯、業(yè)務(wù)邏輯是不是也有了更清晰的認(rèn)識了呢?
其實(shí)關(guān)于MVX還有更多可以討論的,比如有些人認(rèn)為Model層并不是真正處理業(yè)務(wù)邏輯的地方,它只是業(yè)務(wù)模塊的一個上層封裝層,我覺得也不無道理,在復(fù)雜業(yè)務(wù)模塊中,業(yè)務(wù)是存在層次的,MVX中的Model層是所有業(yè)務(wù)層中的最上層。
還有我剛剛提到的業(yè)務(wù)層之下還有數(shù)據(jù)層,這是典型的三層架構(gòu)的概念,即表現(xiàn)層、業(yè)務(wù)層和數(shù)據(jù)層。邏輯存在分層,所以架構(gòu)也必然要進(jìn)行分層,MVX可以做為我們從代碼到業(yè)務(wù)甚至到架構(gòu)的探索的開端。