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

一 AV(Autonomous View)自治視圖

  • 在面向終端用戶的應(yīng)用中,都需要一個(gè)可視化的UI來與用戶交互.這個(gè)UI稱為View 視圖.
  • 在早期,我們習(xí)慣將所有前臺(tái)的邏輯,與視圖揉在一起,稱為AV 自治視圖.
    • 這些邏輯包括:數(shù)據(jù)呈現(xiàn)(Display),用戶動(dòng)作的撲捉與響應(yīng),數(shù)據(jù)存儲(chǔ)等.
  • .Net 的Winform 和ASP.NET Web Form 采用的都是事件驅(qū)動(dòng)模型.
  • AV是將所有UI相關(guān)的邏輯都注冊(cè)到視圖本身,或者視圖元素對(duì)應(yīng)的事件上.
  • 人機(jī)交互應(yīng)用的3個(gè)關(guān)注點(diǎ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的組件都是不可測(cè)試性的(至少是很難測(cè)試).所以AV對(duì)測(cè)試不友好.

?二. MVC模式

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

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

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

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

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

三. MVP模式

  • MVC模式存在的問題

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


  • MVP模式的目標(biāo)

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

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

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

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

      • 一個(gè)被動(dòng)的View.View中的UI元素(控件)不是由View本身操作,而是由Presenter控制對(duì)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對(duì)測(cè)試友好,因?yàn)樗械腢I處理邏輯都在Presenter中,便于測(cè)試.

      • 缺陷

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

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

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

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

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

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

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

五. 第二次的改造

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

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

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

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

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

六. 總結(jié)

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

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

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

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

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

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

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

    • 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.

  • 目標(biāo)

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

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

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

    • 業(yè)務(wù)代碼放在Model中.
    • 控件邏輯,封裝在View的實(shí)際實(shí)現(xiàn)類中.
    • Model是完全獨(dú)立的,不依賴于任何模塊.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捐康,更是在濱河造成了極大的恐慌仇矾,老刑警劉巖庸蔼,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異贮匕,居然都是意外死亡姐仅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門刻盐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掏膏,“玉大人,你說我怎么就攤上這事敦锌÷睿” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵乙墙,是天一觀的道長颖变。 經(jīng)常有香客問我生均,道長,這世上最難降的妖魔是什么腥刹? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任马胧,我火速辦了婚禮,結(jié)果婚禮上衔峰,老公的妹妹穿的比我還像新娘佩脊。我一直安慰自己,他們只是感情好垫卤,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布威彰。 她就那樣靜靜地躺著,像睡著了一般穴肘。 火紅的嫁衣襯著肌膚如雪抱冷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天梢褐,我揣著相機(jī)與錄音旺遮,去河邊找鬼。 笑死盈咳,一個(gè)胖子當(dāng)著我的面吹牛耿眉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鱼响,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼鸣剪,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了丈积?” 一聲冷哼從身側(cè)響起筐骇,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎江滨,沒想到半個(gè)月后铛纬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唬滑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年告唆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晶密。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擒悬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出稻艰,到底是詐尸還是另有隱情懂牧,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布尊勿,位于F島的核電站僧凤,受9級(jí)特大地震影響用狱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拼弃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一夏伊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吻氧,春花似錦溺忧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至振惰,卻和暖如春歌溉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背骑晶。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來泰國打工痛垛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桶蛔。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓匙头,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仔雷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蹂析,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容