這是一個有關安卓MVC框架模式的短系列,目的是思索和分析安卓中MVC模式更為真實的一面。
隨著安卓開發這些年的發展,項目開發相關的框架模式一直是個比較火的話題,一度從傳統忠實的MVC,聊到熱火朝天的MVP,再到看似終極的MVVM,真有些百家爭鳴的味道,是個好現象。不過呢,網上太多文章要么觀點大同小異(ctrl+c+v),要么模棱兩可,要么是鐵桿粉絲不屑其它,而具有個人深度思考的文章卻是較少。筆者呢,其實是MVC模式的支持者,感覺呢,現在安卓中MVC被人誤解偏多,故經過長期思考,寫成此文,以表我MVC真實面目,希望能對讀者有所啟發。而若有MVP或者MVVM堅定的支持者,或許不適合閱讀此文,不過還是建議讀一下,起碼能看到些不一樣的東西。
轉載請標明出處:http://www.lxweimin.com/p/08e461201cd4
1. MVC的常見認識
首先,來段MVC框架模式的簡介:MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟件設計典范,用一種業務邏輯、數據、界面顯示分離的方法組織代碼,將業務邏輯聚集到一個部件里面,在改進和個性化定制界面及用戶交互的同時,不需要重新編寫業務邏輯。MVC被獨特的發展起來用于映射傳統的輸入、處理和輸出功能在一個邏輯的圖形化用戶界面的結構中。
MVC在包括后端、前端和移動端的多個領域都用使用,不同領域的理解和應用都有些差別。對于上述這種界定,很多安卓開發童鞋,大體是這么認為的:
- View:對應于布局等展示類文件
- Model:實體模型 ,存取數據
- Controllor:對應于Activity 和Fragment,業務邏輯+路由功能+視圖邏輯
這種觀點暫且不論,但基于上述觀點的一個衍生觀點是值得懷疑的,即View對應于布局文件,能做的事情特別少,關于該布局文件中的數據綁定的操作,事件處理的代碼都放在Activity/Fragment中,造成了Activity既像View又像Controller,且容易臃腫;這種觀點整體基于一個實踐,即activity完全負責view的數據接入甚至交互,可是這個實踐未必是個得當操作。比如,一個綠色進度條,當進度超過50%,將會變成紅色,這個過程伴隨的是數據變化,體現的是個交互過程,不過這個顏色轉變過程,完全可在進度條這個View內部實現,而不必讓controller控制太多--by CysionLiu。
列舉上述例子,實際上是對網上很多有關MVC說法的一種懷疑:controller由于管這管那,職責太多,并且隨著業務一多,其自身就會臃腫,難以維護。然而,這是MVC自身的問題嗎?
2. MVC怎樣一步步被玩壞的
為了說明這個問題,接下來,我們用==偽代碼==的方式,看看controller是如何一步步被玩壞的。
1.首先來個實體類Person.class和數據獲取接口ICall;
public class Person{
public String name;//偽代碼,僅為說明問題
public int age;
public String address;
}
public interface ICall{
void act(Person person);
}
2.DemoActivity的初始代碼,其實現ICall;
public class DemoActivity extends Activity implement ICall{
//省略了一些邏輯
private TextView mTextShow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextShow = (TextView) findViewById(R.id.text_show);
getData(this);//也可以由其它方式觸發,例如點擊button,最后回調act方法
}
//ICall方法
public void act(Person person){
//...
}
}
3.第一份需求來了,讓mTextShow展示一個Person的名稱,act()為DemoActivity回調ICall的方法;
public void act(Person person){
mTextShow.setText(person.name);
}
4.第二份需求來了,讓mTextShow展示一個Person的年齡,大于60歲,則顯示紅色;
public void act(Person person){
String ageStr = String.valueOf(person.age);
mTextShow.setText(ageStr);
if(person.age>=60){
mTextShow.setTextColor(Color.Red);
}
}
5.發現個問題,person的年齡不能小于0或者大于150;且鑒于顏色太少,來第三份需求,讓mTextShow展示一個Person的年齡,大于50歲且小于60歲,則顯示黃色;大于60歲,顯示紅色;非正常年齡,顯示灰色的0歲;
public void act(Person person){
String ageStr = String.valueOf(person.age);
mTextShow.setText(ageStr);
if(person.age>=50&&person.age<=60){
mTextShow.setTextColor(Color.Yellow);
}else if(person.age>60){
mTextShow.setTextColor(Color.Red);
}else if(person.age<0||person.age>150){
mTextShow.setTextColor(Color.Grey);
mTextShow.setText("0");
}
}
6.現在顯示上基本沒問題了,可是數據有點不適宜的地方,就是act的參數有時可能是null,針對這種情況,希望能顯示之前不為null的數據或者占位數據;
Map<String,Person> recordMap;//偽代碼,省略一些語句
public void act(Person person){
if(person==null){
person = recordMap.get("lastPerson");
if(person==null){
person = new Person("占位");
}
}else{
recordMap.put("lastPerson",person);
}
String ageStr = String.valueOf(person.age);
mTextShow.setText(ageStr);
if(person.age>=50&&person.age<=60){
mTextShow.setTextColor(Color.Yellow);
}else if(person.age>60){
mTextShow.setTextColor(Color.Red);
}else if(person.age<0||person.age>150){
mTextShow.setTextColor(Color.Grey);
mTextShow.setText("0");
}
}
通過上述過程,使得原本一行的act方法工作量,赫然增長到17行。對于不同心態和技術階段的開發童鞋來說,以上過程可能都能見到縮影。一個簡單的View配上一個簡單的業務,就能使代碼量變大10倍以上,那么對于頁面復雜邏輯也復雜些的頁面,按照上述方式,作為中間調節者的controller,可以預見,定會變得臃腫且不易維護--by CysionLiu。
假使上述過程數據的獲取是由點擊一個button引發,從網絡得到數據Person并返回。則一個經典的MVC調用鏈就形成了,即V->C->M:M->C:C->V;這里,箭頭->代表調用;冒號:代表內部處理。
3. MVP簡單介紹以及相關思考
由于上述問題的存在,MVP興起了,打著解耦、清晰的旗號,招攬了不少支持者。其字面定義什么的就不再此處說了,就列出幾點和MVC有主要差別的,并配上大家基本都見過的圖。
MVC和MVP的差別主要有以下三個方面。
- 各部分之間的通信,都是雙向的。
- View 與 Model 不發生聯系,都通過 Presenter 傳遞。
-
View 非常薄,不部署任何業務邏輯,稱為"被動視圖",即沒有任何主動性,而 Presenter非常厚,所有邏輯都部署在那里。
image
這里用MVP來粗略實現上述Person的例子,來看看MVP的效果。
- 其它大體同上,定義IView接口和Presenter,DemoActivity實現IView,如需要,Presenter也要實現IView;
public class Presenter implement ICall{
//省略某些邏輯
private IView callback;
public Presenter(IView aCallBack){
callback = aCallBack;
}
public void getData(){
Model.get(this);//模擬從model獲取數據,傳入ICall回調
}
public void act(Person person){
//...
}
}
public interface IVew{
void showAge(String ageStr,Color color);
}
2.DemoActivity的代碼
private TextView mTextShow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextShow = (TextView) findViewById(R.id.text_show);
Presenter presenter = new Presneter(this);
presenter.getData();
}
3.接下來就是Presenter中act代碼
public void act(Person person){
if(person==null){
person = recordMap.get("lastPerson");
if(person==null){
person = new Person("占位");
}
}else{
recordMap.put("lastPerson",person);
}
String ageStr = String.valueOf(person.age);
if(person.age>=50&&person.age<=60){
callback.showAge(ageStr,Color.Yellow);
}else if(person.age>60){
callback.showAge(ageStr,Color.Red);
}else if(person.age<0||person.age>150){
callback.showAge("0",Color.Grey);
}
}
4.DemoActivity的設置代碼
public void showAge(String ageStr,Color color){
mTextShow.setText(ageStr);
mTextShow.setTextColor(color);
}
經過上述過程,可以看出,Activiity的確輕量不少,而且由于多了P層,代碼也清晰了不少。由于筆者對MVP不是很熟悉,僅是寫過幾個demo,研究過github上那幾個國內外的MVP-Retrofit-RxJava等類似技術的開源項目的源碼;這里只說幾點疑問和感受吧,未必正確,僅拋磚引玉:
解耦,基本就是解耦V和M吧,那么也就是這么認為,M變化,V可能不變;V變,M也可能不變。但此處,注意,只是 可能而已;例如,原來V是個progressbar,現在成了ratebar或者imageview+textView,那么IView要變,則M不變也不大可能,順帶著,作為中間者的presenter也難保持不變,實現那個IView接口的那些子類--activity也要跟著變。也就是說,這個解耦并不完全,只是更多的從小粒度的變化實現了解耦,對于大的視圖/業務邏輯的改變,解耦作用有限。
清晰,View(包括實現了IView的acty和fragment)展示和輕度UI交互,M存取數據,P處理各路交互;中間通過接口協議,各自開發,思路清晰。上述Person例子中,使用MVP的確清晰了些,但是我們模擬上述Person的例子的調用鏈,V->P:P->M:M->P:P->V:V,發現比MVC的調用鏈變的復雜。也就是說,層內部的代碼清晰帶來了層間的復雜度。
大業務,這點也是筆者最為疑惑的地方。因為讀者并未實操過業務比較復雜的MVP模式的項目,而網上所有可見的MVP項目的業務,就如他們的作者說言,學習和鍛煉而已,業務實在簡單,對于這樣的項目,MVP的確比著==深度耦合MVC用法==更為清晰和解耦,但其本身也未突出明顯的好處,因為如前所述,MVC是==被==用成了深度耦合。比如再說下,解耦的那塊,還記得MVC實現Person例子的3,4的需求過度吧,原來設置person.name;后來變成了設置person.age;也就是說,這些是很正常的需求變更,那MVP中IView的確應該有pserson.name的協議,甚至person.address的協議。這里,問題來了,person,如此小的對象,就對應了個一個3個方法大小的接口。若Person復雜到數十個屬性,豈不是接口也要擴大到數十個;伴隨這種情況的后果是,Person屬性變化和UI變化,也會出現粒度較粗的耦合。再者,當復雜到如此程度的接口出現,那么設計者不得不考慮減小接口的體積和復用,好吧,這二者又帶來了學習成本和復用耦合度。另外,復雜的業務也會使使用者也不得不考慮P層太厚帶來的問題--by CysionLiu。
上述也不是筆者危言聳聽,畢竟MVP早已提出但并未廣泛使用和標準化;筆者更多的是疑惑,比如說個筆者做的項目的一個頁面吧,若==單純==是用MVP替換MVC,筆者真是難以看出會有什么好處。該頁面需求如下:
- 頭部UI+ webview+ 中部UI + 兩個數據和風格不相干的List;
- 3個動畫動效;
- 頭部+中部UI有10+個元素需要填充數據;
- 5個url請求,對應2個url-host;
- 結果json中有數字作為key
- 分享
- webview中有js交互,有視頻播放需要優化,有下載圖片
- 統計,5個埋點以上,最多的點傳遞數據多達13個;
- 底部List有上拉加載事件;
- ...
上述情況,使用MVP估計也不會多清晰、解耦和輕量吧。因為本文主要是來探索MVC的,對MVP更多的只是用來佐證,以探索和回歸MVC,所以此處不再繼續MVP了。
4. 思索回歸,MVC的真實面目
我覺得 MVC 在今天,已經不是一個統一的概念了,很多都是把自己的想法強加到 MVC 上,什么叫 MVC?Android 里的 MVC 又是什么?首先官方貌似沒給出這種架構規范。按很多博客列舉圖中畫的 MVC ,Model 層 更新 View,然后把 layout xml 定義為 view 層,那么 Model 層是怎么更新的 layout xml ?如果要說 Android 中的 MVC,應該是 controller 更新 view 吧,怎么會是 model 更新 view。MVC 本來就是一個很老的概念,在 MVC 這個概念出現的時候 和 現在的情況已經不一樣了,還有什么 MVP ,可能只是換個名字罷了,我覺得介紹這些模式的時候 說 思想就可以了,怎么分層,說什么 MVP 從 MVC 演化而來,然后各種對比 Android 里 MVC 是什么樣子,MVP 又是什么樣子,或許全都只是一些技術愛好者的個人偏好而已,因為 Android 里的 MVC 并沒有所謂的標準寫法,MVC 也不是因為 Android 而提出的,而是很多人非要把某種寫法強加上 MVX 這么一個概念,而很多人的說法還不一致,畫的圖 和 寫法都不一樣,越看越懵,也就是說,我們完全可以撇開舊情節,直接把Presenter叫做Controller,也就沒有MVP這種提法了。
好了,上述大段其實說到底就在兩個字--思想。那么所謂MVP,MVVM甚至MVC都體現了什么思想呢?是分層,也就是說,不管MVX,體現的都是分層思想,甚至MVC本身在安卓開發中,代表的即使分層思想,也就是說,MVX只是差別于概念和幾種語法使用不同而已。
那么,分層意味著什么呢,意味著職責單一和清晰化。那么職責如何單一和清晰化呢,上述MVP的person例子倒能體現出一些,不過其分層思想沒有變化;也就是可以認為,對于MVC,使用一些語法和結構上的技巧,就能實現職責單一和清晰化,進一步實現分層,打造易維護,易拓展的結構,而之前MVC的各種被提出的缺點,某種程度上,大部分都與使用者自身的分層思想和技術水平相關。而一些使用了類似接口分層方式的,便給起了個不同的名字,叫做MVP,本質上,還是MVC,還是分層思想,并沒有使數據-展示型應用在結構上有本質的不同。
那么MVC怎么體現分層思想的呢,這又要回歸到最初的提法了,視圖--控制--數據,對,還是這三個;不過本著職責單一又清晰的觀點,我們對這三層,來個新的認識。
- 視圖V,即展示,什么是展示呢?舉個例子,打開開關,燈泡亮了,這個亮了就是展示;那么,若電壓低了,燈泡變暗呢?沒電了,燈泡不亮呢?這些,都是展示,有些童鞋肯定會認為這是控制,是交互。這就牽扯到交互控制和展示之間的一些區別了,這里,筆者認為,固有的,基本不受外界控制者改變而改變的行為,都可認為展示。也就是說,燈泡的變亮,變暗甚至不亮,都與控制者的改變(注意,不是控制的改變)無關,而是當發生這種控制行為,必然會發生的--by CysionLiu。
對應到安卓中,以LinearLayout為例,不管使用者怎么改變,它要么縱向布局,要么橫向布局,其本身就是這樣。而LinearLayout是什么呢,一個官方提供的View,也是我們認為的一種視圖。由此可以知道,MVC中V不僅僅包括xml文件,其應該包括的是眾多官方提供的View和具有特殊性質的自定義View,這點也比較容易理解,很多開源框架不就是自定義view嗎?使用者只需稍稍配置一下,很多炫酷特性便在View內部實現了,根本不需要M或者C干什么多余的事情。整體來說,V層,就是要好好的擔起展示的職責,不要把屬于展示的職責,交給其它層去做。
理論總是有些枯燥,這里,還是以Person那個例子說明一下吧。
這里,定義個AgeTextView.class;
public class AgeTextView extend TextView{
//省略某些邏輯
public void setAge(int age){
String ageStr = String.valueOf(age);
setText(ageStr);
if(person.age>=50&&person.age<=60){
setTextColor(Color.Yellow);
}else if(person.age>60){
setTextColor(Color.Red);
}else if(person.age<0||person.age>150){
setTextColor(Color.Grey);
setText("0");
}
}
}
那么,對于之前MVC實現該例子中的第6點來說,DemoActivity中代碼就變成了以下方式。
Map<String,Person> recordMap;//偽代碼,省略一些語句
private AgeTextView mTextShow;
public void act(Person person){
if(person==null){
person = recordMap.get("lastPerson");
if(person==null){
person = new Person("占位");
}
}else{
recordMap.put("lastPerson",person);
}
mTextShow.setAge(person.age);
}
通過以上變化,act方法中代碼一下少了一半;而對于V的職責上,也更清晰了,C層也變的輕量起來。畢竟我們定義的這個AgeTextView控件,本身就是對應這種顯示age的屬性。上述方式只是從V的角度去進行分層職責化,本身是基于一個前提的,即一個頁面中只有少數的UI才具有比較復雜的展示屬性,而這本身,的確也符合一般產品的開發過程。
- 數據M,即數據存取和操作;這里,數據的操作往往是一些開發同學反對的,認為數據的操作屬于業務邏輯,不該存在于M層。但是,我們看名字也能看出來,數據操作和業務邏輯是有差別的。比如上述例子中的age吧,age<0的判斷是屬于業務邏輯呢,還是數據操作呢?我想,絕大多數童鞋應該會贊同是數據操作吧,因為無論什么業務,age<0,都屬于應該避免的情況。除了這種,M就不能出現業務邏輯了嗎?也未必,如果按照上述例子中age不同,text顏色不同來認為是業務邏輯,的確不能在M中出現業務邏輯;然而在實際開發中,很多時候都是M可以處理的邏輯,比如緩存操作,當C需要M提供數據時,比較好的職責劃分是,C只管拿到數據,而不用管M內部是如何管控的數據。依據這個思路,可以進一步修改上述Person例子,比如之前DemoActivity和IPresenter中都出現了類似代碼:
public void getData(){
Model.get(this);//模擬從model獲取數據,傳入ICall回調
}
這里,我們補充下model類中的邏輯,讓其進行校驗,緩存等操作;
public class Model{
Map<String,Person> recordMap;//偽代碼,省略一些語句
public void get(ICall call){
Person person = fromRemoteOperation();//從遠程/數據庫等獲得數據
if(person==null){
person = recordMap.get("lastPerson");
if(person==null){
person = new Person("占位");
}
}else{
recordMap.put("lastPerson",person);
}
call.act(person);
}
}
通過以上操作,DemoActvity中act方法變成了:
AgeTextView mTextShow;
public void act(Person person){
mTextShow.setAge(person.age);
}
看吧,沒有通過MVP,也實現了Controller的輕量化,僅留了一行代碼;另外如前所述,對于V,將一些交互展示類邏輯放在view中處理,不僅使View顯得更具有血肉,也使C層職責縮小,更為清晰;對于M,其自身把控數據的校驗和緩存等操作,也使得其職責更為合理,也能使C層輕量和清晰,這里補充下,對于M來說,很多時候是充當請求的處理者,而作為請求的發送者有時候需要通過發送命令來配置和選擇處理者,這本身也體現了命令模式的特點,操作得當,可較好的使請求的發送和處理方解耦--by CysionLiu。
以上,也只是從這個簡單例子說明了另一種實現MVC的方式,主要是想給MVC正名,它并不一定臃腫。可能有些童鞋認為demo比較簡單,但從筆者自身的項目來看,是比較可行的,也不會有MVP那種接口亂飛的復雜化情況。當然,C層的處理也非常重要。
- 控制器C,其實更像調度器,在最開始的描述中,很多人傾向于認為其職責是業務邏輯+路由功能+視圖邏輯,這,真的有些重。其實經過上述V和M的介紹,基本這里得出結論,很多業務邏輯和視圖邏輯,都可以還給V和M來處理,對,是還給。而難以交還的那些業務和視圖邏輯,多半是由于這些邏輯往往組成了視圖業務邏輯,難以分開。此時,我認為應該采用代理的思想(非模式),即業務工具類(Helper)。將一個Activity(View)中的視圖業務邏輯“委托”給該工具類(View持有)。我們可以在View中實現針對View的動作的處理,比如數據刷新等,在工具類中對業務進行真正的處理。比如此處,再回到Person例子,不用自定義AgeTextView,而是委托給以下幫助類;
public class DemoHelper{
public static void showAge(TextView ageShow,int age){
String ageStr = String.valueOf(age);
ageShow.setText(ageStr);
if(person.age>=50&&person.age<=60){
ageShow.setTextColor(Color.Yellow);
}else if(person.age>60){
ageShow.setTextColor(Color.Red);
}else if(person.age<0||person.age>150){
ageShow.setTextColor(Color.Grey);
ageShow.setText("0");
}
}
}
在DemoActivity的act方法中,調用上述方法:
TextView mTextShow;
public void act(Person person){
DemoHelper.showAge(mTextShow,age);
}
這種委托思想,對業務的優化整合更為集中以及復用,也免去了頻繁的數據UI變動對二次接口的調用流程。但這種方式會帶來C和這個Helper類的耦合,不過鑒于這種視圖業務邏輯不會太多,這種思路也是比較可行的。
當然,有關C的重頭戲當然不是這個,剛才也提到C中還有調度功能,而這個調度功能的理解和使用也是一個非常重要的優化方式。先來聊聊調度吧,調度,可認為是在合適的時間,將合適的資源安排給合適的用戶。表現在代碼上,即V->C->M和M->C->V;當然這里的M和V都是經過充分職責化的,也就是說,調度時盡量避免有搶奪M和V職責的現象。另外,提到調度,自然會帶來一個管理體系,這個從任何管理/服務體系都可以看出來,比如公司的組織結構關系等。也就是說,調度沒必要甚至說,盡量不要,只維護一個大調度器,而是要構建一個調度體系,上級調度管理下層調度,下層調度管理下下層調度。而每層調度器的職責,基本可認為有兩項:一是管理下層調度器,二是處理本層的一些必須的視圖業務邏輯。
說是這樣說,可是怎樣在代碼中做呢?當然,這個官方提供了思路,我們也基本都已在使用,那就是Fragement和復用View,可是大家會說,這個我們早知道了,而且不管用啊,Fragment也容易變臃腫啊。是的,現在Fragment已經被用做了大的Controller,它的確已經可以被當做頂級的C來對待。
Fragment不合適,那么,復用View呢?也不適合,或者說應該要經過改造過才適合。畢竟一個單View一般只是個視圖載體,沒法攜帶數據和進行業務交互。那么給他一個載體,讓其既能體現視圖,也能處理業務邏輯和調度,不就變成了一個小Controller了嗎?怎樣才能實現這樣呢?聰明的讀者可能想到了--ViewHolder,對,就是它,這個為復用布局和列表item邏輯的一個小載體,是一個比較理想的小型C。那怎么使用它呢?沒什么高大上的,大家都用過,無論是對ListView還是對RecyclerView,官方都提供了對ViewHolder(Vh)的支持,更為關鍵的是,它們都支持根據不同的itemType,可對應不同的Vh,設計多么好啊,既實現了復用,也實現了模塊化。此時,有童鞋可能有疑問了,我們用了那么多次這些List或是recycler啦,也沒見Vh有怎么好的。針對這個疑問,我想借助類似組合模式的思想來解釋下,組合模式能使用戶對單個對象和組合對象的使用具有一致性;那么對于頁面來說,我們可以將一個單布局item,看成多個列表item羅列而成,也可以將多個列表item看做單一布局item,怎么實現,將在本系列第二篇文章來介紹,這里不再繼續了。
總體來說,對于C層的優化,是來自于分而治之的思想,而對于復雜頁面來說,通過這種分而治之,也就實現了頁面級別的模塊化思想;使得C層清晰且輕量。
寫在本篇最后,本文主要目的是,希望通過一步步分析和demo示例,揭示MVC更為真實和精致的一面。篇幅較長,可能看著乏味也不好理解。在接下來的一篇中,將主要針對C層的優化思想來場實戰,以使得筆者的觀點更容易理解。
篇幅較長,碼字不易。
歡迎交流,共同進步!