論MVVM偽框架結構和MVC中M的實現機制

目錄

一直都有人撰文吹捧MVVM應用開發框架,文章把MVVM說的天花亂墜并且批評包括iOS和android所用的MVC經典框架。這篇文章就是想給那些捧臭腳的人們潑潑冷水,雖然有可能招致罵聲一片,但是目的是給那些剛入門的小伙伴一些參考和建議,以免誤入歧途。同時也給那些深陷其中不能自拔的小伙伴們敲敲警鐘,以免其在錯誤的道路上越走越遠。

------ MVVM并非框架,而只是簡單的文件夾分類 ------

MVVM被引入的前因后果

大概是在2010年左右移動端開發火了起來,起初是iOS,Android, WinPhone三個大平臺競爭,后來后者退出了角逐,變成了二分天下。從應用體系結構以及為開發者提供的框架體系來看,兩個平臺都是推出了經典MVC三層結構的開發方式,這三層所代表的意義是模型、視圖、控制。這個開發框架的初衷其實也很簡單:視圖負責展示和渲染,模型負責業務邏輯的實現,控制負責調度視圖的事件以及業務邏輯的調用以及通知視圖的刷新通知。 三部分松散耦合,各司其職。下面是經典的MVC框架結構:

MVC框架圖

一個很可惜的事實是不管是Android和iOS都只對C和V兩部分進行了標準的定義和實現:Android的視圖部分的實現是定義了各種控件以及通過XML文件來組裝視圖布局界面,iOS的視圖的實現也是定義了各種控件以及通過XIB或SB來組裝視圖布局界面; Android的控制部分則是通過Activity來實現,而iOS的控制部分則是通過UIViewController來實現的。而模型部分呢?因為每個應用的業務邏輯和應用場景并不相同,所以兩個平臺也無法也不能夠定義出一個通用的模型層出來,而是把模型層的定義留給了開發者來實現。然而這為我們的開發者在使用MVC框架開發應用時埋下了隱患。

早期的應用開發相對簡單,因為沒有標準的模型層的定義,而控制層又在工程生成時留下了很多可供開發者寫代碼的地方,所以很多開發人員就自然而然的將業務邏輯、網絡請求、數據庫操作、報文拼裝和解析等等全部代碼都放入了控制層里面去了,根本就不需要什么模型層的定義。 這樣隨著時間的推移和應用的復雜增加,就出現了C層膨脹的情況了。一個控制器的代碼可能出現了好幾千行的場景。于是乎有人就開始找解決方案來為C層瘦身了。又一個很可惜的事實是還沒有人去想著抽象出M層,而是用了如下方法來解決問題:

  • 客戶端和服務器之間交互的數據報文是否可以定義出一個個只有屬性而沒有方法的數據對象呢?這樣在處理和渲染界面時就不需要和原始的XML或者JSON或者其他的格式報文交互了,只要操作數據對象就好了。于是解決方案就是根據客戶端和服務器之間交互報文定義出一個個的數據模型,然后再開發出一套XML或者JSON和數據模型之間互轉的解析器來。最后將這一個個只有數據而沒有方法的對象數據模型統一放到一個地方,然后給他們定義為M模型層(呼!終于給出模型層的定義了,但是:Are you kidding me??)。這樣C層就不會再出現XML或JSON解析以及直接讀取報文的代碼了!而是把這部分代碼挪到模型層了(大家來看啊,我終于應用上了MVC框架了!)。 好了!瘦身第一步成功。但是但是,問題還在啊,我的業務邏輯還是一大片在C層啊,看來MVC這種框架也不過如此啊!根本沒有解決我的問題。不行,我不能再用MVC這種框架來開發我的應用了,我要另找它法,要繼續對C層瘦身。

  • 我的某個界面和某個業務邏輯是綁定在一塊的,這個界面的展示是通過調用某個業務邏輯來實現的,業務邏輯完成后要直接更新這個界面。這種緊密的調用和更新關系根本就不需要C層的介入。因此可以將這部分界面的更新刷新和業務邏輯的調用綁定在一塊, 二者結合為一個封閉而獨立的整體并形成獨立的類。這樣把這個類的代碼抽離出來了,存放到一個單獨的文件夾中。我把這個部分叫什么好呢?對了就叫視圖模型層VM吧!視圖模型層中的類定義了一個給外部使用的唯一接口來供C層調用。這樣我終于把一大部分代碼從C層中抽離出來了。我已經成功的實現了C層的進一步瘦身,并抽象出了一個視圖模型層了!(不過哪里好像不對,視圖模型層設計到了視圖、模型、視圖模型層三方面的交互和耦合) 不過沒有關系,反正我的C層進一步瘦身成功了!,我看看還可不可以繼續瘦身C層?

MVVM各層的依賴關系
  • 我的很多視圖的事件是在C層中處理的,那我是不是可以把C層的事件處理也拿出來呢? 干脆就拿出來吧。但是怎么拿出來呢?于是乎我又不停的尋找,終于找到一個叫RAC的東西了,這個東西好啊,他可以負責處理視圖的各種事件,以及可以負責連續的網絡調用。等等。。。 RAC就是有點晦澀難懂!難以學習,代碼難以閱讀和調試。怎么辦? 沒有關系,只要是能將C層的代碼瘦身這些又算什么。。。大不了就是多趟一點坑,多搞幾次培訓就好了。 嗯! 就這么辦,那我把這部分代碼也放入到VM層里面去吧。

    。。。。呼!!! C層終于瘦身成功。然后大家看啊,我的C層里面真的是什么代碼也沒有了。。。 它不再處理視圖的事件了,因為事件讓RAC給處理了、它也不處理視圖的刷新和業務邏輯的調用了因為讓視圖模型MV給處理掉了、他也不處理數據的解析了因為讓模型層給替換掉了。嗯。。。。我要給這種沒有C層或者不需要C層的框架起個名字,叫什么好呢? 就叫:MVVM吧。。。 我的應用可以不要C層了,然后我就奔走相告。將C層無用大白于天下。。

真的是這樣嗎?答案是NO!!!

首先我想說的是一個優秀的框架中各層次的拆分并不是簡單的將代碼進行歸類和劃分,層次的劃分是橫向的,而模塊的劃分則是縱向的 。 這其中涉及到了層次之間的耦合性和職責的劃分,以及層與層之間的交互接口定義和方式,同時層內的設計也應該具有高度的內聚性和結構性。而這些設計的要求并沒有在所謂的MVVM中體現出來。

MVVM據說是來源于微軟的數據視圖的雙向綁定技術。也就是有一個VM的類來實現數據的變化更新視圖,視圖的變化更新數據的處理,整個過程不需要再單獨編碼去處理。這個技術就和早年MFC里面的DDX/DDV技術相似。MVVM只是一種數據綁定技術的變種而不足以稱為框架。框架中的層的要素要具有職責和功能的屬性。就MVVM中所定義的M只能理解為純數據。縱觀整個iOS和android中的所有系統框架庫都沒有出現過讓一批數據結構組成一個層的概念。即使如所謂的存儲層也是數據庫和表以及數據庫引擎三者的結合體為一層。 其實之所以說控制器膨脹根源在于我們的手寫布局視圖在控制器中完成這里占用了非常多的代碼, 業務處理和實現也在控制器中完成。蘋果和Google已經給出了通過SB和XML來實現視圖的構建。至于復雜的業務邏輯也完全可以通過拆分為多個子視圖控制器或者多個Fragment 來完成。請問如果一個設計的足夠好的C層,何來膨脹這么一說!

  • 首先要正確的理解MVC中的M是什么?他是數據模型嗎?答案是NO。他的正確定義是業務模型。也就是你所有業務數據和業務實現邏輯都應該定義在M層里面,而且業務邏輯的實現和定義應該和具體的界面無關,也就是和視圖以及控制之間沒有任何的關系,它是可以獨立存在的,您甚至可以將業務模型單獨編譯出一個靜態庫來提供給第三方或者其他系統使用。在上面經典MVC圖中也很清晰的描述了這一點:控制負責調用模型,而模型則將處理結果發送通知給控制,控制再通知視圖刷新。因此我們不能將M簡單的理解為一個個干巴巴的只有屬性而沒有方法的數據模型。其實這里面涉及到一個最基本的設計原則,那就是面向對象的基本設計原則:就是什么是類?類應該是一個個具有相同操作和不同屬性的對象的抽象。我想現在任何一個系統里面都沒有出現過一堆只有數據而沒有方法的數據模型的集合被定義為一個單獨而抽象的模型層來供大家使用吧 我們不能把一個保存數據模型的文件夾來當做一個層,這并不符合橫向切分的規則。所以說MVVM里面的所謂對M層的定義就是一個偽概念。

  • 上面我已經說明M層是業務模型層而非數據模型層,業務模型層應該封裝所有的業務邏輯的實現,并且和具體視圖無關。我們不能將一個視圖的展現邏輯綁死在一個業務處理邏輯里面,因為有可能存在一個業務邏輯有多種不同的展現形式,也可能界面展示會隨著應用升級而變化,但是業務邏輯是相對穩定的。即使是某個視圖確實就跟這個業務是緊密耦合的,也不應該做強耦合綁定。所以上面所謂的VM這種將視圖的展示和業務的處理邏輯綁定在一塊是非常蹩腳的方式,因為這樣的設計方式已經完全背離了系統里面最基本的展示和實現應該分離處理原則。而且這種設計的思維是和分層的理念是背離的。因為他出現了視圖和業務的緊耦合和相互雙向依賴問題,以及和所謂的M層也要緊耦合的存在。所以說MVVM里面所謂的VM層的定義也是一個偽概念。所謂的VM層這里面只不過是按頁面進行的功能拆分而已,根本就談不上所謂的層的概念。

  • 再來說說事件處理。經典的C層設計的目的是負責事件處理和調度,不論是按鈕點擊還是UITableview的delegate以及ListView的Adapter都最好放在C層來處理,這也是符合C層最本質的定義:就是C層是一個負責調度和控制的模塊,它是V層和M層的粘合劑,他的作用就是處理視圖的事件,然后調用業務邏輯,然后接收業務邏輯的處理結果通知,然后再通知視圖去刷新界面,這就是C層存在的意義。而且系統默認也是按這個方式設計的。而RAC的出現則將這部分的處理給活生生的代替掉了。也就是通過RAC所謂的響應式和觸發式這種機制就能實現將事件的調度處理放在任何地方任何時候都能完成。這樣做的目的使得我們可以分散和分解代碼。但結果出現的問題呢?就是同一個單元調度處理邏輯和功能的構建完全放在了一個地方,但不同的單元邏輯的又分散在不同的地方,無法去分類統一管理和維護。因此你無法一下子就知道某個功能所有調度到底是如何實現以及在哪里實現的。因為RAC將功能構建和事件處理完全粘合到一個大的函數體內部,并且是代碼套代碼的模式,這種方式嚴重的破壞了面向對象里面的構建和處理分離的設計模式理論。更麻煩的是其高昂的學習和維護成本,代碼閱讀理解困難,以及無處不在的閉包使用。試想一下這個對于一個初學者來說是不是噩夢?,一旦出了問題對于維護和代碼調試是不是噩夢?而且使用不當就會出現循環引用的嚴重問題。這樣一來原本C層一個調度總管的職責被RAC來接管后,這些處理將變得分散和無序,當我們要做一些統一的管理比如HOOK和AOP方面的東西時就變得無法下手了。 不可否認的是RAC在處理連續調用以及順序響應方面有一定的優勢。一個例子是我們可能有連續的多個跟服務器的網絡請求,這時候用RAC進行這種處理能方便的解決問題。但是我想說的是當存在這種場景時,我們更加應該將這種連續的網絡調用在M層內部消化掉,而只給C層提供一個簡易而方便的接口,讓C層根本不需要關心這種調用的連續性。因此可以說為了把C層的代碼給消化掉而引入RAC的機制,不僅沒有簡化掉系統反而降低了系統的可維護性和可讀性。RAC機制根本就不適合用在事件處理中。優秀的應用和框架并不在代碼的多寡,而是整體系統的代碼簡單易讀,各部分職責分明,容易維護的調試

------ MVVM被引入的根本原因是對M層的錯誤認識所引起的 ------

MVC中M層實現的準則

說了那么多,可以總結出所謂的MVVM其實并不是一種所謂的框架或者模式,他只是一個偽框架而已,他只是將功能和處理按文件夾的方式進行了劃分,最終的的結果是系統亂成了一鍋粥。毫無層次可言,所具有的唯一優點是把C層的代碼和功能完全弱化了。其實出現這種設計方法最根本的原因就是沒有對M層進行正確的理解定義和拆分。那么我們應該如何正確的來定義和設計M層呢?下面是我個人認為的幾個準則(也許跟其他人的理念有出入):

  • 定義的M層中的代碼應該和V層和C層完全無關的,也就是M層的對象是不需要依賴任何C層和V層的對象而獨立存在的。整個框架的設計最優結構是V層不依賴C層而獨立存在,M層不依賴C層和V層獨立存在,C層負責關聯二者,V層只負責展示,M層持有數據和業務的具體實現,而C層則處理事件響應以及業務的調用以及通知界面更新。三者之間一定要明確的定義為單向依賴,而不應該出現雙向依賴。下面是三層的依賴關系圖:
三層之間的單向依賴關系

只有當你系統設計的不同部分都是單向依賴時,才可能方便的進行層次拆分以及每個層的功能獨立替換。

  • M層要完成對業務邏輯實現的封裝,一般業務邏輯最多的是涉及到客戶端和服務器之間的業務交互。M層里面要完成對使用的網絡協議(HTTP, TCP,其他)、和服務器之間交互的數據格式(XML, JSON,其他)、本地緩存和數據庫存儲(COREDATA, SQLITE,其他)等所有業務細節的封裝,而且這些東西都不能暴露給C層。所有供C層調用的都是M層里面一個個業務類所提供的成員方法來實現。也就是說C層是不需要知道也不應該知道和客戶端和服務器通信所使用的任何協議,以及數據報文格式,以及存儲方面的內容。這樣的好處是客戶端和服務器之間的通信協議,數據格式,以及本地存儲的變更都不會影響任何的應用整體框架,因為提供給C層的接口不變,只需要升級和更新M層的代碼就可以了。比如說我們想將網絡請求庫從ASI換成AFN就只要在M層變化就可以了,整個C層和V層的代碼不變。下面是M層內部層次的定義圖:
M層內部的封裝層次
  • 既然我們的應用是一個整體但又分模塊,那么業務層內部也應該按功能模塊進行結構劃分,而不應該簡單且平面的按照和服務器之間通信的接口來進行業務層次的平面封裝。我相信有不少人都是對M層的封裝就是簡單的按照和服務器之間的交互接口來簡單的封裝。下面的兩種不同的M層實現的業務封裝方式:
兩種不同的M層封裝實現

我們還可以進一步的對業務邏輯抽象出M層的接口和實現兩部分,這樣的一個好處是相同的接口可以有不同的實現方式,以及M層可以隱藏非常多的內部數據和方法而不暴露給調用者知道。通過接口和實現分離我們還可以在不改變原來實現的基礎上,重新重構業務部分的實現,同時這種模式也很容易MOCK一個測試實現,這樣在進行調試時可以很簡單的在真實實現和MOCK實現之間切換,而不必每次都和服務器端進行交互調試,從而實現客戶端和服務器之間的分別開發和調試。下面是一個升級版本的M層體系結構:

基于接口的M層實現
  • M層如何和C層交互的問題也需要考慮,因為M層是不需要知道C層和V層的存在的,那么M層在業務處理完畢后如何去通知C層呢?方法有很多種:
    • 我們可以為M層的通知邏輯定義Delegate協議,然后讓C層去實現這些協議,然后M層提供一個delegate屬性來賦值處理業務通知的對象。
    • 我們也可以定義眾多的NSNotification或者事件總線,然后當M層的業務處理完畢后可以發送通知,并且在C層實現通知的處理邏輯。
    • 我們可以用閉包回調或者接口匿名實現對象的形式來實現業務邏輯完成的通知功能。而且可以定義出標準:所有M層對象的方法的最后一個參數都是一個標準的如下格式的block或者接口回調:
typedef void (^UICallback)(id obj, NSError * error);

這種模式其實在很多系統中有應用到。大家可以參數考蘋果的CoreLocation.framework中的地理位置反解析的類CLGeocoder的定義。還有一點的是在AFN以及ASI中的網絡請求部分都是把成功和失敗的處理分成了2個block回調,但是這里建議在給C層的異步通知回調里面不區分2個block來調用,而是一個block用2個參數來解決。因為有可能我們的處理中不管成功還是失敗都可能有部分代碼是相似的,如果分開則會出現重復代碼的問題。

MVC中M層實現的簡單舉例

最后我們以一個簡單的用戶體系的登錄系統來實現一個M層。

1.定義標準的M層異步回調接口:

//定義標準的C層回調block。這里面的obj會根據不同對象的方法的返回而有差異。
typedef void (^UICallback)(id obj, NSError * error);

//這里定義標準的數據解析block,這個block供M層內部解析用,不對外暴露
typedef id (^DataParse)(id retData, NSError * error);

2.定義所有M層業務類的基類,這樣在通用基類里面我們可以做很多處理。比如網絡層的統一調用,加解密,壓縮解壓縮,我們還可以做AOP和HOOK方面的處理。

     @interface  ModelBase
          
           //定義一個停止請求的方法
           -(void) stopRequest;
           /**
             *定義一個網絡請求的唯一入口方法
             * url 請求的URL
             * inParam: 入參
             * outParse: 返回數據解析block,由派生類實現
             * callback: C層通知block
             */
           -(void) startRequest:(NSString*)url  inParam:(id)inParam outParse:(DataParse)outParse  callback:(UICallback)callback;
     @end

3.定義一個用戶類:

    @interface  ModelUser:ModelBase
  
        @property(readonly) BOOL isLogin;
        @property(readonly) NSString *name;
       
       //定義登錄方法,注意這個登錄方法的實現內部可能會連續做N個網絡請求,但是我們要求都在login方法內部處理,而不暴露給C層。
       -(void)login:(NSString*)name  password:(NSString*)password   callback:(UICallback)callback;
        //定義退出登錄方法
       -(void)logout:(UICallback)callback;
    @end

  

4.定義一個M層總體系統類(可選),這個類可以是單例對象:

    @interface ModelSystem:ModelBase
 
     +(ModelSystem*)sharedInstance;

    //聚合用戶對象,注意這里是readonly的,也就是C層是不能直接修改用戶對象,這樣保證了安全,也表明了C層對用戶對象的使用權限。
    @property(readonly)  ModelUser *user;  

    //定義其他聚合的模塊

    @end

5.在C層調用用戶登錄:

  @implementation LoginViewController

    -(IBAction)handleLogin:(UIButton*)sender
   {
        sender.userInteractionEnabled = NO;
        __weak LoginViewController  *weakSelf = self;
       [[ModelSystem sharedInstance].user  login:@"aaa" password:@"bbb"  callback:^(ModelUser *user, NSError *error){

        if (weakSelf == nil)
               return;
       sender.userInteractionEnabled = YES;
       if (error == nil)
       {
              //登錄成功,頁面跳轉
       }
       else
      {
            //顯示error的錯誤信息。。
      }}];
         
   }

   @end

可以看出上面的C層的部分非常簡單明了,代碼也易讀和容易理解。同時我們還看到了C層跟本不需要知道M層的登錄實現到底是如何請求網絡的,以及請求了幾個網絡操作,以及用的什么協議,以及什么數據報文格式,所有的這一切都封裝在了M層內部實現了。C層所要做的就是簡單的調用M層所提供的方法,然后在callback中通知界面更新即可。整個C層的邏輯也就是幾十行就能搞定了。

具體的模型層設計方法請參考M層的設計


歡迎大家關注我的github地址,關注歐陽大哥2013,關注我的簡書地址:http://www.lxweimin.com/u/3c9287519f58

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容