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

歐陽大哥2013發表於2018-02-06

一直都有人撰文吹捧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層的邏輯也就是幾十行就能搞定了。


歡迎大家關注我的github地址,關注歐陽大哥2013

相關文章