{

Delphi与访问者模式(Visitor模式)

作者 陈省

 

进一步研究一下前面提到的公司,假设现在公司除了有正式员工之外,在工作比较繁忙的时候,还会雇佣一些临时工,这些临时工同正式员工有很多不同的地方,就是他们即没有领导的属性,也没有新员工的属性,但是他们又都属于公司的一部分,因此需要把他们抽象成单独的对象。

也就是说现在TPeople聚合类中,有两种不同的对象,一个是TPerson,一个是TTemporyPerson,对于这两类不同的人来说,有些操作是一样的,比如公司对于所有的员工都提供免费的午餐,但是正式员工的菜好些,临时工的饭菜差一些。有些操作又不一样,比如对于普通员工,都给买保险,但是对于临时工,则没有保险。 对于这类问题,比较容易想到的设计办法是给TTemporyPerson和TPerson类都增加午餐的方法,但是TTemporyPerson则没有保险的方法。

 

但是,很多时候,客户需要统一的对聚合体中的所有对象遍历来执行一致的操作,但是有些对象有保险的方法,有些没有,这样的设计会造成遍历操作的不一致,就需要if else 语句来实现多分派,伪代码如下:

procedure TIterator.Iterate();

begin

  for I:=0 to Collection.count-1 do

  begin

if Collection.Item[I] is TPerson then

  AObj.保险

Else Collection.Item[I] is TTemporyPerson then

  //do nothing

end;

end;

 

如果聚合中的类种类比较多的话,使用If else 语句,同时引入了显式的对象类别判断,使得不好维护。另一个解决办法是给TTemperPerson提供一个空的保险方法,这样可以去掉if else,但是本身从业务逻辑这又是不符合实际的,对类方法造成了污染。

 

比较容易想到的办法是定义多个不同的迭代子,来对不同的对象进行操作。但是这又有一个新的问题,就是当类已经写好了的时候,突然客户提出新的要求,这时需要添加新的方法,但是为了防止类增加新方法后造成系统不稳定,或者引入bug,又不能对已有的类进行修改。这样就只能从原有的类继承新的类来添加派生方法,那么这样的修改又会导致需要修改迭代子。

 

一个更好的解决办法是从TTemporyPerson,TPerson类中将这些差异比较大的操作或者未来要添加的新方法从对象分离出来,构成独立的对象层次,这样做的好处是可以在不污染原来类的基础上给类增加新的方法。

 

这种模式其实同前面迭代子模型中的TTask委托事件很类似,只不过是将原来的委托事件定义到了TOperation类中。修改后的类体系框架图示意如下:

修改后的调用方法伪代码为:

 

procedure TPeople.Iterate(AOperation:TOperation);

begin

  for I:=0 to Collection.count-1 do

  begin

    AOperation.Execute(Collection.Item[I]);

  end;

end;

 

procedure TSafeOperation.Execute(APerson:TPeopleItem);

begin

  if APerson is TTemporyPerson then

   //do nothing

  else if APerson is TPerson then

  //执行保险操作

end;

 

procedure TLunchOperation.Execute(APerson:TPeopleItem);

begin

  if (APerson is TTemporyPerson) then

    //执行午餐操作

else if (APerson is TPerson) then

  //执行午餐操作

end;

 

注意在上面的午餐类的方法中APerson是被按照不同对象类型进行的处理,这是因为临时工的午餐标准可能只有5块,正式工的午餐标准可能只有10块。

 

在上面的方式中,每增加一个新的方法,只要添加一个新的TOperation类就可以了,同时消除了迭代过程的一致性问题,但是要注意的是这个模式仍然没有消除if else的分派语句。针对这个问题,将上面的模型进一步改造成Visitor模式,就可以解决了,改造后的类结构图示意如下:

在上图中TOperation相当于访问者模式中的Visitor,而TPeople相当于对象的聚合,TPeopleItem对应于聚合中的元素。注意在TOperation基类中我们将Execute方法改造为了两个强类型的Execute方法,派生类对这两个方法进行重载,以针对不同的类提供不同的操作。修改后的类的调用方法采用了类似乒乓球的交互调用方式,伪代码示意如下:

 

procedure TPeople.Iterate(AOperation:TOperation);

begin

  for I:=0 to Collection.count-1 do

  begin

    Collection.Item[I].AcceptOperation(AOperation);

  end;

end;

 

procedure TTemporyPerson.AcceptOperation(AOperation:TOperation);

begin

  AOperation.ExecuteTemporyPerson(self);

end;

 

procedure TPerson.AcceptOperation(AOperation:TOperation);

begin

  AOperation.ExecutePerson(self);

end;

 

procedure TSafeOperation.ExecuteTempory(APerson:TTemporyPerson);

begin

  //do nothing

end;

 

procedure TSafeOperation.ExecutePerson(APerson:TPerson);

begin

  //执行保险操作

end;

 

procedure TLunchOperation.ExecuteTempory(APerson:TTemporyPerson);

begin

  //执行5块钱标准的午餐操作

end;

 

procedure TLunchOperation.ExecutePerson(APerson:TPerson);

begin

  //执行10块钱标准的午餐操作

end;

 

下面是访问过程的时序图:

 

访问者模式的优缺点

 

可以在不改动原有类的情况给聚合体中的类添加新的方法,比如有时需要为类添加一些测试方法,但是将测试方法添加到现有类中有一个问题,这些测试方法在最终的发行版本是不必要的,直接添加测试方法到类中会污染类,而使用访问者模式就不存在这样的问题。

 

同时访问者模式将操作集中到访问者对象中,可以对聚合体中所有的不同类都生效,而不用再分别的向不同的类中添加操作,同时还可以消除if else这样不好维护的多分派代码。

 

但是访问者模式也有缺点,它不适用于聚合中的对象种类不稳定而经常变化的场合,聚合体中每增加一个类TPeopleItemX,所有的访问者对象都需要添加一个ExecutePersonX的操作,如果有10个访问者类,则增加一个被访问的元素类的话,就需要修改已有的10个访问者类,反而增加了工作量。

 

}

本站原创及翻译内容保留版权,欢迎转贴,转贴时请注明转自Delphi深度探索