應(yīng)用MVP 模式對遺留代碼進行重構(gòu)

一 AV(Autonomous View)自治視圖

  • 在面向終端用戶的應(yīng)用中,都需要一個可視化的UI來與用戶交互.這個UI稱為View 視圖.
  • 在早期,我們習慣將所有前臺的邏輯,與視圖揉在一起,稱為AV 自治視圖.
    • 這些邏輯包括:數(shù)據(jù)呈現(xiàn)(Display),用戶動作的撲捉與響應(yīng),數(shù)據(jù)存儲等.
  • .Net 的Winform 和ASP.NET Web Form 采用的都是事件驅(qū)動模型.
  • AV是將所有UI相關(guān)的邏輯都注冊到視圖本身,或者視圖元素對應(yīng)的事件上.
  • 人機交互應(yīng)用的3個關(guān)注點.
    • 數(shù)據(jù)在UI上的展示.
    • UI處理邏輯.
    • 業(yè)務(wù)邏輯.
  • AV的缺陷
    • 首先,業(yè)務(wù)邏輯與UI無關(guān),所以應(yīng)該最大程度地被重用.而在AV中,業(yè)務(wù)邏輯糅合在UI中,無法重用.例如,從winform遷移到web form上.
    • 穩(wěn)定性:業(yè)務(wù)邏輯>UI處理邏輯>UI.
      • "短板效應(yīng)":?三者糅合在一起后,具有最弱穩(wěn)定性的UI決定了整體的穩(wěn)定性.
    • 任何涉及到UI的組件都是不可測試性的(至少是很難測試).所以AV對測試不友好.

?二. MVC模式

  • 針對AV的缺陷,采用SOC(關(guān)注點分離)來剝離3個部分.

  • 將人機交互應(yīng)用分為3個部分:

    • Model:對應(yīng)用狀態(tài)和業(yè)務(wù)功能的封裝.
      • 維護著整個應(yīng)用的狀態(tài)(數(shù)據(jù)和行為),并實現(xiàn)了所有的業(yè)務(wù)邏輯,可以看做為一個領(lǐng)域模型.
    • View:實現(xiàn)可視化界面的呈現(xiàn),捕捉最終用戶的交互操作(鍵盤,鼠標).
    • Controller.
      • View捕獲到用戶交互操作后會直接轉(zhuǎn)發(fā)給Controller,后者完成相應(yīng)的UI邏輯。
      • 如果需要涉及業(yè)務(wù)功能的調(diào)用,Controller會直接調(diào)用Model。
      • 在完成UI處理之后,Controller會根據(jù)需要控制原View或者創(chuàng)建新的View對用戶交互操作予以響應(yīng).
  • View和Model存在直接的聯(lián)系.View可以直接調(diào)用Model查詢其狀態(tài)信息。

    • 當Model狀態(tài)發(fā)生改變的時候,它也可以直接通知View.
  • Model對View的數(shù)據(jù)狀態(tài)改變通知,View對Controller的用戶交互通知.都是單向的消息交換.

    • 可以使用事件機制來實現(xiàn)這兩種通知.
    • 也可以通過觀察者模式通過注冊/訂閱的方式來實現(xiàn).
      • View 作為Model 的觀察者,通過注冊相應(yīng)的事件來檢測數(shù)據(jù)狀態(tài)的改變.
      • Controller 作為View 的觀察者,通過注冊相應(yīng)的事件來處理用戶的交互操作.

三. MVP模式

  • MVC模式存在的問題

    • View和Model可以繞過Controller來直接進行交互.
    • 對于用戶驅(qū)動的程序(人機交互),我們不需要Model來主動通知View數(shù)據(jù)狀態(tài)的變化.所以,Model應(yīng)該是完全獨立的.


  • MVP模式的目標

    1. 測試(Unit Test)友好.
    • 關(guān)注點分離.
    • 正交性.
      • 每一個操作都只改變一件事情,而沒有其它的副作用.
  • 解依賴

    • 對View 和Model 解耦.
    • 降低了Presenter 對View 的依賴.從依賴于具體的View 到依賴于抽象的IView 接口.
  • 交互

    • Presenter對Model的單向調(diào)用.
    • Presenter和View之間的雙向交互.這個是核心.
  • Presenter 和View 之間交互的方式

    • PV(Passive View)
      • 為了不做對UI的測試(難到幾乎不能),應(yīng)該在UI中不進行UI邏輯的處理.

      • 一個被動的View.View中的UI元素(控件)不是由View本身操作,而是由Presenter控制對UI元素的操作.

      • 需要將View中的元素以屬性或者其他方式暴露,以供Presenter操作.

      • 在數(shù)據(jù)綁定中,控件類型的選擇應(yīng)該是View內(nèi)部的邏輯,不應(yīng)該出現(xiàn)在Presenter中.

        • 所以,在IView的定義中,不能涉及到具體的控件類型.
        • 而是返回一種數(shù)據(jù)綁定所需的數(shù)據(jù)類型.
        • 然后在View內(nèi)部處理數(shù)據(jù)到控件的綁定.
      • PV對測試友好,因為所有的UI處理邏輯都在Presenter中,便于測試.

      • 缺陷

        • 對于一個復(fù)雜的UI(含有很多元素),IView接口將會十分龐大.
        • Presenter需要對UI元素進行操作,所以要了解很多的UI細節(jié).造成簡單事情復(fù)雜化.
      • Soc

        • 將諸如格式化,數(shù)據(jù)綁定這些簡單的UI邏輯移到View中.在View中進行一些簡單的UI邏輯處理.
        • View本身僅實現(xiàn)單純獨立的UI邏輯,它處理的數(shù)據(jù)應(yīng)該是Presenter推送給它的.
          • 所以View盡可能不維護數(shù)據(jù)狀態(tài).在Iview接口的定義中不包含屬性.
        • Presenter所需的View狀態(tài)應(yīng)該是View在請求交互處理時給它的.

四. 第一次改造:最薄的View.

  • 起源:由于View持有對Presenter的引用,所以理論上,View是可以無限制地調(diào)用Presenter的.

    • 基于以前AV的編碼習慣,很可能造成以下的問題:
      • 大部分(甚至所有)的UI處理邏輯都寫到View中.
      • 而Presenter的作用就是Proxy,僅僅是調(diào)用View中的方法而已.
  • 采用事件訂閱的方式來完成Presenter和View的交互.

    • 首先,在IView中定義事件Handler.
    • 為了隔離事件參數(shù)中e的類型污染(一些控件的事件參數(shù),會引入一些測試不友好的類型),定義一系列的事件參數(shù)類型.
    • 然后,在View的控件事件處理函數(shù)中.
      • 將處理事件需要的上下文信息,包裝到一個自定義的事件參數(shù)中,然后 Raise Event.
    • 最后,在Presenter中,訂閱IView暴露的各種事件,并進行處理.處理時需要的上下文在自定義的事件參數(shù)中.
  • 優(yōu)缺點

    • View只完成了純粹的布局展示.
    • 在事件處理流程中,如果需要Cancel處理,會比較難做到.

五. 第二次的改造

  • 在View中調(diào)用Presenter的方法.完成部分的UI邏輯.

  • 工程劃分(使用Company來替代真實信息).

    • Company.MVP.ICommonView.
      • 包含了對使用到的控件的抽象View接口,在每個接口中暴露出來Presenter需要使用到的屬性和函數(shù).
      • 每一種控件類型一個接口.
    • Company.MVP.ComonViews.
      • 對于每一個控件,實現(xiàn)一個繼承了IXXXView接口的類.
      • 在這些類中,體現(xiàn)了具體控件的屬性和方法的細節(jié).
    • Company.MVP.Common.
      • 該工程含有3個子文件夾.
        • ModelObjects. Model的一部分,業(yè)務(wù)模型的抽象類.
        • Service. Model的另外一部分,定義了數(shù)據(jù)訪問接口.
        • View:定義了UI頁面需要實現(xiàn)的接口.
    • Company.MVP.Presenter.
      • Presenter的具體實現(xiàn).
    • Company.MVP.Service.
      • 數(shù)據(jù)訪問接口的具體實現(xiàn).
    • Company.Client.
      • 具體的UI工程.會實現(xiàn)Common中View的UI頁面接口.
  • 工程間依賴.
    • Prensenter僅僅依賴于ICommonView和Common.而跟具體的UI控件類型,具體的UI畫面無關(guān).
    • 所以,可以使用一個Presenter來對應(yīng)多個的View展示(Client).
  • 單元測試

    • 針對Presenter.
      • 對于Service和View,由于P中操作的是兩者的接口.所以可以使用Mock來模擬這兩個部分.
      • 而Model是可以簡單地New出來的,不需要進行Mock.
    • 針對Model.
      • 使用業(yè)務(wù)場景,進行測試.而且對其測試時,不需要進行Mock.
    • 針對View.
      • 可以進行少量的測試.因為有IView接口,所以可以Mock控件的屬性和行為,來針對UI頁面進行測試.
  • 更換控件類型

    • UI應(yīng)用中,最經(jīng)常遇到的情形.例如,現(xiàn)在要將界面上的一個TextBox控件替換為EditText控件
      • 在UI實現(xiàn)的Client工程的具體頁面類上,將實例化以前的成員時使用的類型從TextBoxView修改為EditTextView即可.
      • 其他的類和工程不需要修改.
      • 改動被限定在了特定的地方.避免了短板效應(yīng).

六. 總結(jié)

  1. 關(guān)于代碼量
  • 使用MVP模式后,代碼量是肯定不會比原先的少的.

  • 考慮到View的重用,以及子Presenter的重用.代碼量增加的也不多.

  • 關(guān)于控件的View類型的接口抽象及實現(xiàn).

    • 對于控件的View的接口,可以只針對一個頁面,也可以在工程前期,定義好對一個控件所需的所有的操作.這樣就在全系統(tǒng)中使用一份View的接口.
    • View接口對外暴露的應(yīng)該是操作,而不是以控件屬性/方法的視角看待.也就是說Prensenter需要對控件進行什么類型的操作,就暴露一個這樣的操作出來.
  • 關(guān)于控件差異性的問題.

    • 系統(tǒng)中不同界面中,同一控件的操作接口可能是不同的.
    • 按照MVP的本意,是沒有View重用的概念的.
    • 但是,我們可以將同一控件基本的公用行為抽象為一個接口,然后使用一個類來實現(xiàn)它.然后在有特殊操作接口的畫面中,再定義一個繼承自公用接口的接口,然后使用一個類繼承公用類,并實現(xiàn)該接口.
  • 關(guān)于控件的事件鏈.

    • 在現(xiàn)有的代碼中,有很多地方用到了事件鏈的連鎖效應(yīng).
    • 個人認為,這是一種不太好的編程方式.這樣控件之間相互的依賴關(guān)系變得如此的復(fù)雜.改動事件鏈上的任何一個控件的任何一個事件處理,都需要查看其連帶的連鎖反映.
    • 在MVP中,我們在處理一個控件的操作時,會把所有控件需要展示的內(nèi)容一次性地處理好,然后一把交給View進行展示.而不是使用事件的連鎖效應(yīng).
    • 這樣,就解除了控件之間在事件上的相互依賴關(guān)系.
  • 關(guān)于單元測試.

    • 對于業(yè)務(wù)系統(tǒng)的單元測試,純粹的代碼覆蓋率是沒有意義的.
    • 需要關(guān)注的是測試的場景覆蓋率.
    • 即使覆蓋百分百的代碼.但是漏測了一種Case,一樣會出現(xiàn)Bug.
    • 所以,我們需要有很清晰的業(yè)務(wù)邏輯說明,來指導我們進行單元測試時的Case場景輸入.
  • 事件處理流程三部曲

    • IView中定義Event.
      Event ButtonClick.
    • View中觸發(fā)事件.
Private withevents  _item as button
  Public sub itemClick() handles _item.Click
RaiseEvent  ButtonClick
  • Presenter中掛接并處理事件
    AddHandler OKButton.ButtonClick , Addressof Save.

  • 目標

    • 一個(種)控件,對外提供統(tǒng)一的行為接口.
      • 行為包括:屬性,方法,事件.
  • 畫面類職責清晰.

    • 僅包含了控件的集合.
    • 沒有任何的邏輯處理代碼.
  • 更換控件類型時,改動最小.

    • 僅需更改畫面類中New控時使用的實際View類型.
  • 業(yè)務(wù)代碼和控件邏輯的分離.

    • 業(yè)務(wù)代碼放在Model中.
    • 控件邏輯,封裝在View的實際實現(xiàn)類中.
    • Model是完全獨立的,不依賴于任何模塊.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容