Model-View-Presenter模式之 Step by Step
長期以來,一直在C/S的世界里埋頭苦干,偶爾會(huì)有朋友問我為什么沒轉(zhuǎn)向Web。我想,暫時(shí)的,除了架構(gòu)與模式,我還沒有額外的時(shí)間和精力。但歸根結(jié)底,無論C/S亦或B/S,Server才是最讓我著迷的部分。其中,UI無關(guān)的設(shè)計(jì)一直是分層架構(gòu)方案的重要組成。它和無關(guān)持久化PI(Persistence Ignorance)一樣,可以讓我們把UI呈現(xiàn)相對獨(dú)立出來,與應(yīng)用層和領(lǐng)域模型脫耦。簡單地說,就是可以把一個(gè)基于WinForm實(shí)現(xiàn)的UI換成ASP.NET等Web形式的,或者同時(shí)支持窗口與報(bào)表兩種UI形式,而不影響其他層的實(shí)現(xiàn)。
具體到實(shí)踐中,MVC模式(Model-View-Controller)已經(jīng)成為了UI無關(guān)設(shè)計(jì)的經(jīng)典,在企業(yè)架構(gòu)中得到廣泛應(yīng)用。Model維護(hù)業(yè)務(wù)模型與數(shù)據(jù),View提供呈現(xiàn),Controller控制程序流程、傳遞和處理請求。本質(zhì)上,這是一種Observer模式的具體實(shí)現(xiàn)。但是,MVC如此普及,卻并非完美。它也有缺點(diǎn),其中包括View可以直接訪問Model,View中耦合了一部分業(yè)務(wù)對象轉(zhuǎn)換的邏輯。即便是常見的ASP.NET的MVC,也不是一種純粹的MVC,因?yàn)樵贏SP.NET的結(jié)構(gòu)里,并沒有一個(gè)嚴(yán)格的、獨(dú)立的領(lǐng)域模型。
于是,MVP(Model-View-Presenter)逐漸步入了我們的視野。在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的方式下,作為系統(tǒng)核心的領(lǐng)域模型層,其上會(huì)有一個(gè)暴露領(lǐng)域模型功能的應(yīng)用服務(wù)層,其下方則是提供數(shù)據(jù)庫訪問、事務(wù)管理等功能的基礎(chǔ)層。應(yīng)用服務(wù)層界定了整個(gè)系統(tǒng)的邊界。在這種傳統(tǒng)方式下,View將直接與Service交互。而在MVP下,將會(huì)在二者之間增加一個(gè)Presenter。Presenter將負(fù)責(zé)向Service提交請求,并驅(qū)動(dòng)View的更新。
在MSDN里,有一篇Jean-Paul Boodhoo撰寫的《設(shè)計(jì)模式: Model View Presenter》,可以作為對MVP概念的一個(gè)入門。我選擇在該文基礎(chǔ)上,以類似向?qū)У姆绞酵瓿梢粋€(gè)MVP的示例,作為對該文的一個(gè)補(bǔ)充。特別要說明的是,我這個(gè)例子是非常粗糙的,而且只是文本描述性的,不是完整的、可運(yùn)行的。
假設(shè)我們要實(shí)現(xiàn)一個(gè)叫SomeForm的Windows Form。其中的Combox用于選擇不同的SomeEntity,然后下方的TextBox和NumericUpDown顯示SomeEntity中的姓名Name與薪水Salary。
第一步,是把上面這個(gè)Form進(jìn)行抽象,把我們關(guān)心的用于表達(dá)UI狀態(tài)的屬性集中到一個(gè)接口IViewSomeForm中。在上圖可知,SomeForm要維護(hù)的狀態(tài)屬性包括三個(gè):對應(yīng)ComboBox的所有SomeEntity項(xiàng),對應(yīng)Name的string,對應(yīng)Salary的decimal。對ComboBox的抽象,可以理解為一個(gè)集合Collection,該集合的每個(gè)元素包括兩個(gè)屬性:Text用于顯示,Tag儲(chǔ)存關(guān)聯(lián)的對象,所以需要另行設(shè)計(jì)。
public interface IViewSomeForm { string FullName { get; set; }decimal Salary { get; set; } }有了前面對ComboBox的抽象理解,便可以設(shè)計(jì)一個(gè)抽象的IUiComplexItem對應(yīng)ComboBox的Item,用抽象的IUiComplexItemCollection對應(yīng)ComboBox的Item集合。于是,先用UiComplexItemCombo實(shí)現(xiàn)了我們需要的ComboBox的兩個(gè)內(nèi)嵌屬性Text與Tag。至于SomeDTO,暫時(shí)就理解為SomeEntity的替身,我們把它綁定到ComboBox某項(xiàng)上的對象,以方便通過item.Tag這樣的方式提取SomeEntity的信息。
public interface IUiComplexItem {}public class UiComplexItemCombo : IUiComplexItem {public UiComplexItemCombo(string text, SomeDTO tag){Text = text;Tag = tag;}public string Text { get; private set; }public SomeDTO Tag { get; private set; } }接下來,由于UiComplexItemCombo只是實(shí)現(xiàn)了對ComboBox中Item的一個(gè)映射,而ComboBox通常維護(hù)的是一個(gè)Item的集合,所以我再添加一個(gè)接口IUiComplexItemCollection及其實(shí)現(xiàn),用于管理這些Item。完成了這些以后,IUiComplexItemCollection實(shí)際成為了ComboBox的一個(gè)代理。
public interface IUiComplexItemCollection {void Add(IUiComplexItem complexItem);void Clear();IUiComplexItem SelectedComplexItem { get; } }public class UiComplexItemCollectionProxy : IUiComplexItemCollection {private readonly ComboBox _innerControl;public UiComplexItemCollectionProxy(ComboBox innerControl){_innerControl = innerControl;}public void Add(IUiComplexItem complexItem){this._innerControl.Items.Add(complexItem);}public void Clear(){this._innerControl.Items.Clear();}public IUiComplexItem SelectedComplexItem{get { return this._innerControl.SelectedItem as IUiComplexItem; }} }有了這些準(zhǔn)備,我們的IViewSomeForm變成了下面這個(gè)樣子。其中,原始類型我們提供了getter與setter,而對復(fù)雜的控件類型則只提供了IUiComplexItemCollection的getter,這是因?yàn)槲覀儗⒁煤竺娉霈F(xiàn)的Presenter借由IUiComplexItemCollection去綁定和驅(qū)動(dòng)底層的該控件。
public interface IViewSomeForm {// raw type (both getter & setter)string FullName { get; set; }decimal Salary { get; set; }// complex type (only getter)IUiComplexItemCollection Combo { get; } }第二步,為SomeForm添加接口IViewSomeForm并實(shí)現(xiàn)之,讓SomeForm能被該接口驅(qū)動(dòng)。其中的_textboxName、_updownSalary和_comboBox是布置在SomeForm窗體上的TextBox、NumericUpDown、ComboBox等控件。
public class SomeForm : Form, IViewSomeForm {// for raw typeprivate readonly TextBox _textboxName = new TextBox();private readonly NumericUpDown _updownSalary = new NumericUpDown();// for complex typeprivate readonly ComboBox _comboBox = new ComboBox();private UiComplexItemCollectionProxy _comboProxy;public string FullName{get { return this._textboxName.Text; }set { this._textboxName.Text = value; }}public decimal Salary{get { return (decimal) this._updownSalary.Value; }set { this._updownSalary.Value = value; }}public IUiComplexItemCollection Combo{get { return this._comboProxy; }} }第三步,SomeForm暴露了它的接口,所以開始準(zhǔn)備驅(qū)動(dòng)View的表現(xiàn)器Presenter。從前述MVP的結(jié)構(gòu)看,Presenter一頭連著View,另一頭連著為Presenter提供數(shù)據(jù)和服務(wù)的Service。所以我們使用依賴注入的方式,為Presenter注入IViewSomeForm與IService對象。不過為簡單起見,我在例子中只選擇了注入IViewSomeForm。
public class Presenter {private readonly IViewSomeForm _view;private readonly IService _service;public Presenter(IViewSomeForm view){_view = view;_service = new SomeService();} }完成注入后,Presenter便可以通過IView暴露的屬性,去改變View的呈現(xiàn)。因此,我們先整理Presenter關(guān)心的事件,這是非常重要的一步。這些事件中,其中一個(gè)是View的加載,Presenter需要幫助SomeView中的ComboBox綁定好Item的集合,以方便用戶在下拉列表中選擇。另一個(gè)事件是ComboBox的選擇項(xiàng)變化后,Presenter應(yīng)該更新Name與Salary的顯示值。于是,我們?yōu)镻resenter添加如下兩個(gè)方法OnIntialize()與OnSelectedIndexChanged()。
其中的OnInitialize(),是從Service中讀取一個(gè)SomeDTO的列表,然后利用一個(gè)轉(zhuǎn)換子convertor,把SomeDTO列表轉(zhuǎn)換為SomeForm中ComboBox需要的Item的集合,可以簡單理解為“從Service獲取數(shù)據(jù),然后綁定到View中的控件上”。
public class Presenter {private readonly IViewSomeForm _view;private readonly IService _service;public Presenter(IViewSomeForm view){_view = view;_service = new SomeService();}public void OnInitialize(){var convertor = new ConConvertorDTOToUiComplexItem();var dotList = _service.GetDTOList() as IList<SomeDTO>;convertor.BindTo(dotList, _view.Combo);}public void OnSelectedIndexChanged(){var dto = ((UiComplexItemCombo) _view.Combo.SelectedComplexItem).Tag;_view.FullName = dto.Name;_view.Salary = dto.Salary;} }第四步,有了Presenter,接下來就需要修改View的實(shí)現(xiàn),讓二者可以互動(dòng)了。這個(gè)例子很簡單,在SomeForm窗口的Load事件中完成了前面Presenter關(guān)心的兩件事。一是Presenter初始化,準(zhǔn)備好ComboBox中的下拉列表。另一個(gè)是當(dāng)ComboBox的選擇改變時(shí),Presenter要及時(shí)更新Name與Salary,所以我們將之前Presenter中定義的方法OnSelectedIndexChanged(),利用委托綁定到ComboBox的SelectedIndexChanged事件上。
public class SomeForm : Form, IViewSomeForm {// for raw typeprivate readonly TextBox _textboxName = new TextBox();private readonly NumericUpDown _updownSalary = new NumericUpDown();// for complex typeprivate readonly ComboBox _comboBox = new ComboBox();private UiComplexItemCollectionProxy _comboProxy;public string FullName{get { return this._textboxName.Text; }set { this._textboxName.Text = value; }}public decimal Salary{get { return (decimal) this._updownSalary.Value; }set { this._updownSalary.Value = value; }}public IUiComplexItemCollection Combo{get { return this._comboProxy; }}private Presenter _presenter;private void form_Load(object sender, System.EventArgs e){this._presenter = new Presenter(this);this._presenter.OnInitialize();this._comboProxy = new UiComplexItemCollectionProxy(this._comboBox);this._comboBox.SelectedIndexChanged += delegate { this._presenter.OnSelectedIndexChanged(); };} }至此,一個(gè)MVP的模型算是基本完成了。當(dāng)然,作為一個(gè)例子,它很粗糙。我們還可以再進(jìn)一步嘗試抽象和解耦,可以增加一些事件接口,提供多線程支持,或者完善依賴注入的方法。比如,根據(jù)前述第三步中Presenter關(guān)心的事件列表,在IViewSomeForm中定義一些event并暴露給Presenter,從而方便Presenter將自己的事件處理方法掛載上IView,而不是使用上面form_Load()中那種生硬的事件綁定方法。
當(dāng)然,MVP也不是沒有缺點(diǎn)。MVP的主要問題是增加了額外的接口和交互,提高了系統(tǒng)的復(fù)雜度。同時(shí),某個(gè)Presenter定義總是與某一個(gè)IView定義存在緊密的聯(lián)系,這種聯(lián)系有時(shí)會(huì)帶來額外的耦合問題。
還需要討論的,就是關(guān)于接口所屬層的劃分與如何保持領(lǐng)域模型封閉性了。從個(gè)人的理解而言,將領(lǐng)域?qū)ο笾苯颖┞督o上層是很危險(xiǎn)的、也是不恰當(dāng)?shù)?#xff0c;這樣做容易使領(lǐng)域模型遭到“污染”。另一方面,現(xiàn)代分布式的系統(tǒng),也對數(shù)據(jù)傳輸和表示提出了扁平化的要求,因此可以選擇增加一個(gè)數(shù)據(jù)傳輸層,或者將DTO的相關(guān)功能合并入應(yīng)用服務(wù)層,專門用于管理數(shù)據(jù)傳輸對象DTO(Data Transfer Object)。而在MVP相關(guān)接口的分配上,我個(gè)人傾向于將UI呈現(xiàn)相關(guān)的所有接口都定義在Presenter中,因?yàn)镻resenter與IView之間總有一個(gè)固定的對應(yīng)關(guān)系。于是,我們得到了如下所示的一張架構(gòu)圖(點(diǎn)擊可查看1600x1400大圖)。
最后,附上前面引用的Convertor和其他一些相關(guān)的示例代碼。
public interface IService {IList<DTO> GetDTOList(); }public class SomeService : IService {public IList<DTO> GetDTOList(){ return new List<SomeDTO>() as IList<DTO>;} }public interface IConvertor<T> {void BindTo(IList<T> dtoList, IUiComplexItemCollection uiComplexItemCollection); }public class ConvertorDTOToUiComplexItem : IConvertor<SomeDTO> {public void BindTo(IList<SomeDTO> dtoList, IUiComplexItemCollection uiComplexItemCollection){uiComplexItemCollection.Clear();foreach (var dto in dtoList)uiComplexItemCollection.Add(new UiComplexItemCombo(dto.Name, dto));} }public abstract class DTO {public int Id { get; set; } }public class SomeDTO : DTO {public string Name { get; set; }public decimal Salary { get; set; } }轉(zhuǎn)載于:https://www.cnblogs.com/Abbey/archive/2012/04/26/2472030.html
超強(qiáng)干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的Model-View-Presenter模式之 Step by Step的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EAS BOS 发布
- 下一篇: 二分(更新中)