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;这紧紧是个临时值——以后将看到。