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

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

------ MVVM並不是框架,而只是簡單的文件夾分類 ------git

MVVM被引入的來龍去脈

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

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層裏面去吧。bash

    。。。。呼!!! 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

相關文章
相關標籤/搜索