Delphi MVC模式

by Joanna Carter
译文:skyblue(转载请注明作者)

在一篇已经发表的文章中做了微小改动;可以从这篇文章中看到关于Model View Presenter(模型-视图-推荐者)的概念更胜于Model View Controller(模型-视图-控制器)。“但是我还不知道什么是模型-视图-控制器!”,你可能会说。好,在本文得最后篇章中我希望你得问题或者其他更多得问题能够得到解答。

在最近得文章中,我已经讨论过oo设计模式,包括观察者模式,并且我猜想MVC可能被当成一种超级观察者;但是我想最好把它描述成一种开发框架。

为了不使你有太多迷惑,我将忽略MVC中控制器的概念,并用MVP中的推介者作为替代来描述;他们完成大多数相同的工作,不同的是推介者在GUI脚本中的操作有点诡异。

让我们开始描述MVP的构成元素。

模型(Model)
模型在一个系统中表示一个具体对象的数据和行为,例如,一个列表或者一个树。模型将扮演驱动视图的角色。

视图(View)
视图是一种显示下层模型的可视方式。例如,TListView 或者 TTreeView。视图将扮演模型的观察者角色。

选择(Selection)
模型维护一个可供选择的对象,这个对象可以反映当前视图中突出的条目。任何指令发给模型处理都依赖这个选择对象。

指令(Command)
一个指令是模型上预知的每个行为。处理者和交互都要求模型处理指令。指令可以不被处理或者重新被处理,从而为结构提供基础。指令通常作为观察者,在选择模式中执行相同的操作于每个条目上。

处理者(Handler)
处理者用作获取简单的鼠标和菜单事件并对事件做出不需要在一个GUI组件上写入其逻辑反映的一种方式。一个处理者保存一个相关指令的引用。

交互(Interactor)
一个交互被用作处理复杂的事件,例如拖放,和处理者一样,从任何一个GUI元素上分离逻辑反映。一个交互通常从处理者派生

推荐者(Presenter)
推介者被依赖于:
  管理来自于视图的输入表示
  根据GUI选择对象刷新模型选择对象
  激活和撤销处理者和交互
  管理和执行指令

组件(Componet)
在一个窗体里每个可视控件通常以一种特殊格式呈现数据;然而这里有不止需要一个在下层数据上的视图的地方。MVP允许在一个单独组件里面同时有数据封装(model),可视外观(View)和输入管理(Presenter)。这些组件能够聚合形成大的组件和应用程序。

回到现实世界
讨论理论固然很好,但是在这个例子里面,我将创建一个简单但是完整的组件示范一下这些组成部分怎样建立和协同工作的。

我将描述的这个组件将基于字符串列表;在每个人大喊“难道这不是多余的?”之前,我必须强调一点,这是学习实践的捷径而且我们需要从简单学起。

在我开始示范MVP之前,我需要为观察者模式定义一些接口:

     ISubject = interface;
     
     IObserver = interface
     ['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
     procedure Update(Subject: ISubject);
     end;
     
     ISubject = interface
     ['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
     procedure Attach(Observer: IObserver);
     procedure Detach(Observer: IObserver);
     procedure Notify;
     end;

你可能注意到观察者包含一个update方法;因为在delphi里面许多可视组件都包含一个update方法不需要ISubject参数,我们需要做一些特别的组件用作视图。

模型
下一步,我们需要定义基于这个mvp组件的模型的行为:


     IListModel = interface
     ['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
     procedure BeginUpdate;
     procedure EndUpdate;
     
     function GetCount: Integer;
     function GetItem(Idx: Integer): string;
     
     procedure Add(Item: string);
     procedure Clear;
     procedure Insert(Item, Before: string);
     procedure Move(Item, Before: string);
     procedure Remove(Item: string);
     
     property Count: Integer
     read GetCount;
     property Item[Idx: Integer]: string
     read GetItem;
     end;

因为每个在这个模型中改变的属性应该告知它的观察者对象它已经改变了,BeginUpdate 和 EndUpdate用来让我们告诉事件发生,不是每个细小的变化都会被告知。随着我们讨论这个实现类这将变得明朗。

GetCount和GetItem都是属性的简单的存取方法。
增加和移除方法是我们定义模型的行为。
由于接口不包含实现,现在我们需要创建一个能执行定义的方法的类:

TListModel = class(TInterfacedObject, IListModel, ISubject)

这个类的申明有两点需要解释一下:

我们必须从TInterfacedObject继承胜于从TObject,因为这将自动让我们定义_AddRef, _Release and QueryInterface;这都是实现任何接口所必须的。

用接口允许我们建立一个实现类包括更多的行为。这不是多重继承,但是它给我们用于支持更多的特征或者行为提供了可能。

   private
     fItems: TStringList;
     fObservers: IInterfaceList;
     fUpdateCount: Integer;

私有开始部分申明了三个成员:

fItems是一个一般对象指针用于保存我们管理的字符串列表
fObeservers是一个接口指针用于保存每个分离的观察者列表;因为我们用这个接口指针在用完之后我们将不需要释放它。
fUpdateCount用于BeginUpdate和EndUpdate方法,我们将很快讨论得。

随着我们继续讨论这个类得方法,我想强调这些方法已经申明在私有部分是因为一个很好得原因。一个接口紧紧有'public',一旦实现对象已经创建到IModel接口指针中,TModel类型将不再需要引用它。因此每个接口得所有方法可以而且必须写在私有部分。

     // IListModel
     procedure BeginUpdate;
     procedure EndUpdate;
     function GetCount: Integer;
     function GetItem(Idx: Integer): string;
     procedure Add(Item: string);
     procedure Clear;
     procedure Insert(Item, Before: string);
     procedure Move(Item, Before: string);
     procedure Remove(Item: string);
     // ISubject
     procedure Attach(Observer: Observer);
     procedure Detach(Observer: IObserver);
     procedure Notify;
     public
     constructor Create; virtual;
     destructor Destroy; override;
     end;
     
现在让我们看看这些方法得实现部分:

     constructor TListModel.Create;
     begin
     inherited Create;
     fItems := TStringList.Create;
     fObservers := TInterfaceList.Create;
     end;
     
     destructor TListModel.Destroy;
     begin
     fItems.Free;
     inherited Destroy;
     end;
     
注意到我们把fObservers定义成一个TInterfaceList,由于我想保存一个接口指针得列表而且fObservers不需要释放因为它得引用计数接口指针再用完之后将全部垃圾回收。

     function TListModel.GetCount: Integer;
     begin
     Result := fItems.Count;
     end;
     
     function TListModel.GetItem(Idx: Integer): string;
     begin
     Result := fItems[Idx];
     end;
     
GetCount和GetItem可以自动说明,但是现在我将实现方法影响到列表模型的状态:
     procedure TListModel.Add(Item: string);
     begin
     BeginUpdate;
     fItems.Add(Item);
     EndUpdate;
     end;
     
     procedure TListModel.Clear;
     begin
     BeginUpdate;
     fItems.Clear;
     EndUpdate;
     end;
     
     procedure TListModel.Insert(Item, Before: string);
     begin
     BeginUpdate;
     fItems.Insert(fItems.IndexOf(Before), Item);
     EndUpdate;
     end;
     
     procedure TListModel.Move(Item, Before: string);
     var
     IndexOfBefore: Integer;
     begin
     BeginUpdate;
     IndexOfBefore := fItems.IndexOf(Before);
     if IndexOfBefore < 0 then
     IndexOfBefore := 0;
     fItems.Delete(fItems.IndexOf(Item));
     fItems.Insert(IndexOfBefore, Item);
     EndUpdate;
     end;
     
     procedure TListModel.Remove(Item: string);
     begin
     BeginUpdate;
     fItems.Delete(fItems.IndexOf(Item));
     EndUpdate;
     end;
     
     
这些方法得代码相当直接,但是我想你应该注意这些方法最后都调用了BeginUpdate和EndUpdate方法。这是这些方法得代码:

     procedure TListModel.BeginUpdate;
     begin
     Inc(fUpdateCount);
     end;
     
     procedure TListModel.EndUpdate;
     begin
     Dec(fUpdateCount);
     if fUpdateCount = 0 then
     Notify;
     end;
     
运用这些方法之后得想法不言而喻如果我们看到同时刷新很多属性的时候。如果不用这种结构,再每个属性改变之后,模型必须通知每个观察者它已经改变;如果相当多的属性很快发生了变化,这将触发成倍连续性得可视刷新,导致视图闪烁。然而,通过再每个属性改变之前调用BeginUpdate,我们增加fUpdateCount,它意味着当第一个属性发生改变,BeginUpdate和EndUpdate都将调用,但是测试fUpdateCount=0 失败。当我们再所有改变都完成之后调用EndUpdate,fUpdateCount现在应该被减小到0并调用Nodify。

最后,我们要实现这个模型得ISubject;但是注意当加入一个观察者后,我们需要调用Nodify确保新加入得观察者已经更新。
 
     procedure TListModel.Attach(Observer: IObserver);
     begin
     fObservers.Add(Observer);
     Notify;
     end;
     
     procedure TListModel.Attach(Observer: IObserver);
     begin
     fObservers.Remove(Observer);
     end;
     
     procedure TListModel.Notify;
     var
     i: Integer;
     begin
     for i := 0 to Pred(fObservers.Count) do
     (fObservers[i] as IObserver).Update(self);
     end;
     

视图
或者这里应该是“一个视图”?用MVP框架得一个好处就是改变和增加视图不影响模型。这是一个很长简单得视图用来表示我们得列表:

     TListBoxView = class(TListBox, IObserver)
     private
     procedure IObserver.Update = ObserverUpdate;
     procedure ObserverUpdate(Subject: ISubject);
     end;

和TListModel一样,任何接口得方法可以也必须申明再类得私有部分。因为你将注意到,一般类得方法名定义得和它的接口一样;但是这里有个冲突再TControl的Update方法和IObserver的Update方法中,解决办法就是在Update方法前加入IObserver指明。ObserverUpdate方法就是为了解决Update的命名冲突。
 
     procedure TListBoxView.ObserverUpdate (Subject: ISubject);
     var
     Obj: IListModel;
     i: Integer;
     begin
     Subject.QueryInterface(IListModel, Obj);
     if Obj <> nil then
     begin
     Items.BeginUpdate;
     Items.Clear;
     for i := 0 to Pred(Obj.Count) do
     Items.Add(Obj.Item[i]);
     Items.EndUpdate;
     end;
     end;

所有这些都是为观察者模式的观察者建立一个可视控件所必须的,也是为了实现IObserver.Update方法;这样就使得可视控件具有主观意识。

Subject参数通过ISubject指针传入这个方法,但是我们需要用TListModel来处理;第一行我们利用QueryInterface判断是否传入的Subject支
持IListModel接口。如果支持,Obj变量将设置成有效指针;如果不支持Obj将设置为nil。如果我们有一个有效Obj指针,我们现在就可以像对
待其他任何一个IListModel的引用使用Obj。

首先我们调用基于ListBox的Items字符串列表的BeginUpdate方法(避免闪烁);然后我们简单的清除字符串列表并读入ListMode中的每个Item。

当然最好紧紧更新那些已经变化了的Items,但是我已经说过这个实例将尽可能要简单!

另一视图

当我们创建我们第一个列表视图,我想我将告诉你在ListModel上创建一个选择性视图会多么简单:

     TComboBoxView = class(TComboBox, IObserver)
     private
     procedure IObserver.Update = ObserverUpdate;
     procedure ObserverUpdate(Subject: ISubject);
     end;

你将看到这个控件的申明和其他可视控件的申明多么类似;这个时候我想让用户在这个List Model上选择一个Item。当我们实现推介者的时候你将知道我用这个选择条目来做什么。

     procedure TComboBoxView.ObserverUpdate (Subject: ISubject);
     var
     Obj: IListModel;
     i: Integer;
     begin
     Subject.QueryInterface(IListModel, Obj);
     if Obj <> nil then
     begin
     Items.BeginUpdate;
     Items.Clear;
     for i := 0 to Pred(Obj.Count) do
     Items.Add(Obj.Item[i]);
     ItemIndex := 0;
     Items.EndUpdate;
     end;
     end;

唯一不同之处就是Combo box视图设置它的ItemIndex属性以便这个选择了的Item在编辑框中可见。现在我将其值设置为0;这紧紧是个临时值——以后将看到。

推介者
推介者的概念主要是处理所有视图和模型之间的交互。为了根见清楚的明白推介者的作用,我将最先描述另一个参与在MVP框架中组件;这将会
导致这篇文章过长,所以我将在下期更加全面的讨论。

如果我们紧紧描述MVC框架,可能所有的事件操作者都写入控制器会比在GUI窗体上面好;这将让我们和所有交互在列表MVC组件的上下文中的列表视图处理所有的逻辑关系。当我们创建好这些MVC组件时,我们在窗体上面可以简单的放置一个视图组件,创建一个控制器并用这个可视组件为控制器的视图属性赋值。

这样做我们将拦截控制器的所有这些事件并把这个视图加入进去作为Subject模型的一个观察者。

     IController = interface
     ['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
     function GetModel: IModel;
     function GetView: IView;
     procedure SetModel(Value: IModel);
     procedure SetView(Value: IView);
     property Model: IModel
     read GetModel
     write SetModel;
     property View: IView
     read GetView
     write SetView;
     end;
     
我将完整的IController的申明;当然实现类也要包括任何来自视图的事件操作者并将通过那些操作者调用模型,但是随着我们看到推介者和它相关联的操作者和交互者,还有接下来篇章中的选择模式和命令模式,我这里将不作更深的详细阐述。

结束语
在这篇文章中我们开始学习关于MVP的概念;高级oo设计模式。

MVP解决一个包含了模型,视图和推介者组成的组件的建立,并使之和选择模式,命令模式,操作者模式和交互者模式相联系。

我们建立了实现IListModel接口和ISubject接口的TListModel类。让我们可以不用通过多重继承混和不同的方法行为。

在建立TListModel中,我们注意到所有实现方法都应该放在类的私有部分中因为不能通过接口被访问他们。

视图的建立讨论了怎样获得一个标准可视控件并使它具有主观意识;我们也看到了怎样解决Observer的Update方法和TControl的Update方法的
命名冲突。

最后我们简单的接触到了推介者的概念,简单描述了控制器在MVC框架中怎样扮演它的角色。

该段落就此结束,希望你将赞同MVP在建立一个好的设计和可以轻松维护的程序中确实是最有价值的扮演者。

part 2

在第一篇文章中,我们学到了关于MVP框架的概念,高级oo设计模式。

我们创建了一个列表模型类用于实现一个列表模型接口和一个Subject接口。在设计的两个视图中,讨论了怎样获得一个标准可视控件并使它具
有主观意识;我们也看到了怎样解决Observer的Update方法和TControl的Update方法的命名冲突。

我们仅仅是接触了一点推介者的概念,简单描述了控制器在MVC框架中怎样扮演它的角色。在我们学习一些MVP架构中地其他部分之后,我们以后将更加全面地讨论推介者。

The Latest Model
我们在上章节用到的列表模型只是简单的介绍了MVP的基本概念。现在我们需要修改IListModel的版本以便在模型的所有Item中我们都可以用接口指针。

我们可以利用接口这种方式写出起连结作用的代码,尽管实现接口有许多种方式,所以我们需要用字符串做一个接口的模型,并用一个类实现它。

     IString = interface
     ['{xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx}']
     function GetAsString: string;
     procedure SetAsString(const Value: string);
     property AsString: string
     read GetAsString
     write SetAsString;
     end;
     
     TString = class(TInterfacedObject, IString, IVisited)
     private
     fString: string;
     // IString
     function GetAsString: string;
     procedure SetAsString(Value: string);
     // IVisited
     procedure Accept(Visitor: IVisitor);
     end;
     
现在我们需要改变列表模型接口使其可以和普通的IInterface类型兼容:
     IListModel = interface
     ['{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}']
     function GetCount: Integer;
     function GetItem(Idx: Integer): IInterface;
     procedure Add(const Item: IInterface);
     procedure Clear;
     procedure Insert(const Item, Before: IInterface);
     procedure Move(const Item, Before: IInterface);
     procedure Remove(const Item: IInterface);
     property Count: Integer
     read GetCount;
     property Item[Idx: Integer]: IInterface
     read GetItem;
     end;
     
现在有了一个一般的列表模型,在这个接口里面我们可以接受任何Item类型,只要它实现IInterface。你将注意到BeginUpdate和EndUpdate方法没有出现;后面会提到:

     TListModel = class(TInterfacedObject, IListModel, ISubject)
     private
     fItems: IInterfaceList;
     fSubject: ISubject;
     protected
     property Items: IInterfaceList
     read fItems;
     // ISubject
     property Subject: ISubject
     read fSubject
     implements ISubject;
     // IListModel
     procedure Add(const Item: IInterface);
     procedure Clear;
     function GetCount: Integer;
     function GetItem(Idx: Integer): IInterface;
     procedure Insert(const Item: IInterface; Before: IInterface);
     procedure Move(const Item: IInterface; Before: IInterface);
     procedure Remove(const Item: IInterface);
     end;
     
这里我们申明了一个类,实现了IListModel接口和ISubject接口。这个类的主要部分和前一个基本相同,除了我们用IInterface和TInterfaceList代替了String和TStringList。在实现上这两个版本的区别是不言而喻的:

     procedure TListModel.Add(const Item: IInterface);
     begin
     fSubject.BeginUpdate;
     if fItems = nil then
     fItems := TInterfaceList.Create;
     fItems.Add(Item);
     fSubject.EndUpdate;
     end;
     
     procedure TListModel.Clear;
     begin
     fSubject.BeginUpdate;
     fItems.Clear;
     fItems := nil;
     fSubject.EndUpdate;
     end;
     
     function TListModel.GetCount: Integer;
     begin
     if fItems <> nil then
     Result := fItems.Count
     else
     Result := 0;
     end;
     
     function TListModel.GetItem(Idx: Integer): IInterface;
     begin
     Result := fItems[Idx];
     end;
     
     procedure TListModel.Insert(const Item, Before: IInterface);
     var
     InsertIdx: Integer;
     begin
     if fItems = nil then
     fItems := TInterfaceList.Create;
     if fItems.IndexOf(Item) < 0 then
     begin
     fSubject.BeginUpdate;
     InsertIdx := fItems.IndexOf(Before);
     if InsertIdx < 0 then
     InsertIdx := 0;
     fItems.Insert(InsertIdx, Item);
     fSubject.EndUpdate;
     end;
     end;
     
     procedure TListModel.Move(const Item, Before: IInterface);
     var
     IdxItem: Integer;
     IdxBefore: Integer;
     MoveItem: IInterface;
     begin
     if fItems <> nil then
     begin
     fSubject.BeginUpdate;
     IdxItem := fItems.IndexOf(Item);
     if IdxItem >= 0 then
     begin
     MoveItem := fItems[IdxItem];
     fItems.Delete(IdxItem);
     IdxBefore := fItems.IndexOf(Before);
     if IdxBefore >= 0 then
     fItems.Insert(IdxBefore, MoveItem);
     fSubject.EndUpdate;
     end;
     end;
     end;
     
     procedure TListModel.Remove(const Item: IInterface);
     begin
     if fItems <> nil then
     begin
     fSubject.BeginUpdate;
     fItems.Remove(Item);
     fSubject.EndUpdate;
     end;
     end;
     
控制你的Subject
你可能注意到调用BeginUpdate实际上是作用着fSubject成员。利用接口的一个好处就是你可以用一个非常有力叫做聚合的技巧用另一个类委托一个接口的实现;一个类只能被写入一次然后就可以一次一次重复使用。

在MVP种,模型通常是有一个或更多的加入其中作为观察者的视图的Subjects。我们前面已经讨论过,这意味着模型发生改变时所有这些视图都将被通告并更新他们自己。

现在我们可以为观察者模式申明一个增强的接口:

     IObserver = interface
     ['{7504BB57-65D8-4D5D-86F1-EC8FFED8ED5E}']
     procedure Update(const Subject: IInterface);
     end;
     
     ISubject = interface
     ['{7A5BDDA0-C40C-40E5-BAB0-BF27E538C72A}']
     procedure Attach(const Observer: IObserver);
     procedure Detach(const Observer: IObserver);
     procedure Notify;
     
     procedure BeginUpdate;
     procedure EndUpdate;
     end;

现在你看到BeginUpdate和EndUpdate在上篇文章的IListModel中现在放到了ISubject接口中;这是因为他们被用来作为Subject更新机制的一个必须部分。

注意IObserver.Update现在用IInterface作为参数比ISubject要好;这样使其更加具有通用型。

虽然IObserver要在每个需要更新的类中重新实现,ISubject可以被一个类完全通用的实现,如果它用作一个实现ISubject和其他接口类的子对象,例如TListModel。

     TSubject = class(TInterfacedObject, ISubject)
     private
     fController: Pointer;
     fObservers: IInterfaceList;
     fUpdateCount: Integer;
     
     function GetController: IInterface;
     
     procedure Attach(const Observer: IObserver);
     procedure Detach(const Observer: IObserver);
     procedure Notify;
     
     procedure BeginUpdate;
     procedure EndUpdate;
     public
     constructor Create(const Controller: IInterface);
     end;
     
在TSubject类中有三个明显的地方:

1、fController和GetController用来使Notify方法可以传递一个包含这个Subject委托的类的实例,比传递它本身好,因为这意味观察者对象将仅仅拥有ISubject接口的属性,比需要实际的模型更好。
     constructor TSubject.Create(const Controller: IInterface);
     begin
     inherited Create;
     fController := Pointer(Controller);
     end;

如果我们注意TSubject的构造我们将看到我们强制储存一个IInterface的引用;这样避免在控制器对象上增加引用计数并让Subject在适当的时候释放。
     function TSubject.GetController: IInterface;
     begin
     Result := IInterface(fController);
     end;
     
GetController方法用于转换指针变量为IInterface类型并不增加引用计数。

2、fObservers简单的获取一个加入的Observers列表,是加入,分离和通知的重点。

     procedure TSubject.Attach(const Observer: IObserver);
     begin
     if fObservers = nil then
     fObservers := TInterfaceList.Create;
     if fObservers.IndexOf(Observer) < 0 then
     fObservers.Add(Observer);
     end;
     
     procedure TSubject.Detach(const Observer: IObserver);
     begin
     if fObservers <> nil then
     begin
     if fObservers.IndexOf(Observer) >= 0 then
     fObservers.Remove(Observer);
     if fObservers.Count = 0 then
     fObservers := nil;
     end;
     end;
     
     procedure TSubject.Notify;
     var
     i: Integer;
     begin
     if fObservers <> nil then
     for i := 0 to Pred(fObservers.Count) do
     (fObservers[i] as IObserver).Update(GetController);
     end;

你可能感兴趣的:(Delphi)