求知 文章 文库 Lib 视频 Code iProcess 课程 角色 咨询 工具 火云堂 讲座吧   建模者  
会员   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
     
   
分享到
设计模式随笔-让众口不再难调
 

作者:吕震宇,发布于2011-12-23

 

"众口难调"出自宋·欧阳修《归田录》卷一:"补仲山之衮,虽曲尽于巧心;和傅说之羹,实难调于众口。"其原意是各人的口味不同,很难做出一种饭菜使所有的人都感到好吃。众口是否真的难调呢?其实有个不错的办法可以解决众口难调的问题,那就是吃"自助餐"。

面对众口难调的问题去吃"自助餐"已经不是什么新鲜事,承办一个几百人、几千人的会议往往采用的都是自助餐的方式,让来宾各取所需,这就是所谓的以不变应万变。用更通俗的话来说,就是"东西都在这儿,自己看着办吧"。

在程序设计中解决这种众口难调的问题用的就是Visitor模式。在这里"众口难调"是指很难设计出一组对象(一桌饭菜)符合每个调用者的需要(口味),因为你根本就无法预料到这组对象的访问者是谁,有什么样的调用请求,访问什么样的数据。那么对付众口难调的方法"以不变应万变"在Visitor模式中就是指让一桌饭菜从你面前过,自己看着办就行了。

当然这里也有个先决条件,那就是你对每样饭菜都有一定的了解,自己才能做出选择。否则可能会瞪着一盘摆满小石子的盘子不知所措。反过来说,饭菜不需要知道来这里就餐的人的口味是什么,只管放出来让客人看着办就行了。这样,顾客和饭菜之间是不均衡的。顾客必须"有备而来",但饭菜却对顾客却"一视同仁"。

using System;

using System.Collections;

abstract class Visitor

{

  public abstract void VisitCoffee(Coffee c);

  public abstract void VisitMeat(Meat m);

  public abstract void VisitVegetable(Vegetable v);

}

class ZhangSan : Visitor

{

  public override void VisitCoffee(Coffee c)

  {

    Console.Write( "{0}: Take a cup of {1}, ",this, c);

    c.AddMilk();

    c.AddSugar();

    Console.WriteLine();

  }

  public override void VisitVegetable(Vegetable v)

  {

    Console.WriteLine( "{0}: Take some {1}", this, v );

  }

  public override void VisitMeat(Meat m)

  {

    Console.WriteLine( "I don't want any meat!");

  }

}

class LiSi : Visitor

{

  public override void VisitCoffee(Coffee c)

  {

    Console.Write( "{0}: Take a cup of {1}, ",this, c);

    c.AddSugar();

    Console.WriteLine();

  }

  public override void VisitVegetable(Vegetable v)

  {

    Console.WriteLine( "{0}: Take some {1}", this, v );

  }

  public override void VisitMeat(Meat m)

  {

    Console.WriteLine( "{0}: Take some {1}", this, m );

  }

}

abstract class Food

{

  abstract public void Accept( Visitor visitor );

}

class Coffee: Food

{

  override public void Accept( Visitor visitor )

  {

    visitor.VisitCoffee( this );

  }

  public void AddSugar()

  {

    Console.Write("add sugar. ");

  }

  public void AddMilk()

  {

    Console.Write("add milk. ");

  }

}

class Meat: Food

{

  override public void Accept( Visitor visitor )

  {

    visitor.VisitMeat( this );

  }

}

class Vegetable: Food

{

  override public void Accept( Visitor visitor )

  {

    visitor.VisitVegetable( this );

  }

}

class BuffetDinner

{

  private ArrayList elements = new ArrayList();

  public void Attach( Food element )

  {

    elements.Add( element );

  }

  public void Detach( Food element )

  {

    elements.Remove( element );

  }

  public void Accept( Visitor visitor )

  {

    foreach( Food f in elements )

    f.Accept( visitor );

  }

}

public class Client

{

  public static void Main( string[] args )

  {

    BuffetDinner b = new BuffetDinner();

    b.Attach(new Coffee());

    b.Attach(new Vegetable());

    b.Attach(new Meat());

    ZhangSan z = new ZhangSan();

    LiSi l = new LiSi();

    b.Accept( z );

    Console.WriteLine("----------------------");

    b.Accept( l );

  }

}

但是,Visitor模式中所蕴涵的思想绝非一个众口难调的例子所能完全表达的。其中还有"条件外置"(我起的名字)的含义在里面(应当归纳到"Find what vary and encapsulate it"的范畴,是我对这句话的理解),也就是说将条件判断从一个类中抽取出来,或交由专门的对象进行处理,或通过配置文件由用户手工控制。说白了就是做成"活"的。这种"条件外置"往往离不开多态的帮忙。面向对象中多态性是说,可以将子类型对象赋值给父类型对象,如果子类型复写了父类型的某个方法,那么当调用父类型对象的此方法时,自动转而调用子类型复写后的方法。条件外置在众多的模式中都有体现:

比如在简单工厂模式中,我们需要判断类型,然后进行加工,如下:

public Light Create(string LightType)

{

  if(LightType == "Bulb")

  return new BulbLight();

  else if(LightType == "Tube")

  return new TubeLight();

  else

  return null;

}

我们就可以让BulbLight与TubeLight共同继承自Light,再加上一个与之相对应的工厂架构,于是条件便外置到了客户端的手中:

public static void Main()

{

  Creator c = new BulbCreator();

  Light l = c.factory();

  l.TurnOn();

  l.TurnOff();

 }

}

如果你想生产什么样子的灯泡就给 Creater c 赋值什么类型的工厂就行了。有人可能还会问,那什么时候使用什么类型的工厂不还是要进行条件判断吗,对了,条件外置后总还是要有人处理的,只不过不在Factory里面,也不在Light里面。你可以集中管理,也可以用配置文件,总之保证了系统绝大多数模块的稳定性。

另外,在策略模式的案例中,如果没有应用"策略模式",那么我们也必须在业务对象中使用一个长长的If结构,判断何时使用哪个策略。但是借助多态性将"条件外置"后,便将判断控制权交由其他类进行处理,使得业务对象更加稳定。

至于到底将条件放到什么地方有很多解决办法,其一是放到一个专门的类中,这便应了《Design Pattern Explained》一书中的"Find what vary and Encapsulate it."这句话,发现变化的东西并且将其封装起来。其二,就是外置成基于XML或纯文本的配置文件,随时可以方便的进行修改。如果愿意,甚至可以编写一个图形化配置工具实现这一功能。

但是条件外置带来的"负"面影响便是需要"Design to Interface(针对接口或抽象编程)"。针对抽象编程增加了系统的稳定性,提高了可扩展性,并且让用户只与抽象打交道,恰好也应用了"最小知识原则"。但这对于必须了解对象类型的系统却是个灾难。Visitor模式便是在这个灾难中成活下来的一个很好的例子。虽然还有些争议,但解决的已经是很好了。

那么在Visitor模式中又是如何将"条件外置"的呢?这个外置工作恐怕比起前两种类型要复杂一些,用到了双重分派(Double Dispatch),所以我们还要从头说起。先让我们现看看没有用Visitor模式的"众口难调"的例子。

using System;

using System.Collections;

public class Visitor

{

  public virtual void Visit(ArrayList dinner)

  {

    foreach(object f in dinner)

    {

      if(f is Coffee)

      VisitCoffee((Coffee)f);

      else if(f is Vegetable)

      VisitVegetable((Vegetable)f);

      else if(f is Meat)

      VisitMeat((Meat)f);

      else

      {

        // do nothing

      }

    }

  }

  protected virtual void VisitCoffee(Coffee c){}

  protected virtual void VisitVegetable(Vegetable v){}

  protected virtual void VisitMeat(Meat m){}

}

public class ZhangSan : Visitor

{

  protected override void VisitCoffee(Coffee c)

  {

    Console.Write( "{0}: Take a cup of {1}, ",this, c);

    c.AddMilk();

    c.AddSugar();

    Console.WriteLine();

  }

  protected override void VisitVegetable(Vegetable v)

  {

    Console.WriteLine( "{0}: Take some {1}", this, v );

  }

  protected override void VisitMeat(Meat m)

  {

    Console.WriteLine( "I don't want any meat!");

  }

}

public class Coffee

{

  public void AddSugar()

  {

    Console.Write("add sugar. ");

  }

  public void AddMilk()

  {

    Console.Write("add milk. ");

  }

}

public class Meat

{

}

public class Vegetable

{

}

public class Client

{

  private static ArrayList dinner = new ArrayList();

  public static void Main( string[] args )

  {

    dinner.Add(new Coffee());

    dinner.Add(new Vegetable());

    dinner.Add(new Meat());

    ZhangSan z = new ZhangSan();

    z.Visit(dinner);

  }

}

在这个例子中,各类食物没有公共父类,所以顾客在Visit的时候,必须将各类食物视作object,然后进行类型判断,再分别进行处理,处理时还不要忘了进行类型转换。长长的If结构会给系统引入很多不必要的麻烦,降低了系统的扩展性。

下面我对它应用条件外置:为了利用多态性,我们需要给条件中出现过的咖啡、蔬菜、肉赋予一个公共的父类"食物"。但这对我们这个例子这似乎是远远不够的。因为我们的顾客要"看菜吃饭",如果利用多态性隐藏了类型信息,只针对抽象"食物"编程的化,我们便失去了"看菜吃饭"的功能,我想这是谁也不希望发生的事情。因此,我们唯一的方法就是多态后再进行"回调",让每个具体类(咖啡、蔬菜、肉)自己报上名来,并且自己主动"回调"Visitor中与类型相关的方法。这样,Visitor只需撒下天罗地网,静待食物找上门来。这便是我对双重分派的一个认识。双重分派利用多态和回调消除了条件判断(这里没有外置条件),但也引入了一些新的问题:那就是当食物扩展时(比如说加入米饭),必须修改Visitor的天罗地网以提供米饭的回调函数,如果继承自Visitor的类很多的化,这个系统就变得脆弱起来。其实这也是我在后面要讨论的问题。

Visitor模式的的脆弱在于每个Visitor必须对所有它要访问的对象有个清晰的了解,一旦增加新类型对象,导致每个Visitor都必须发生变化。另外所有Element(在这个例子中是指具体食物)在进行回调的时候,也必须清楚回调的是Visitor的什么方法,方法名是什么等等,造成类与类之间耦合过于紧密。有没有更好的办法呢?我这里说说我的思路:

我的解决办法是二次利用多态性,并辅以模板方法模式(其实算不上模板方法,充其量算个缺省实现罢了)。C#中的多态性分成两个层面,一个是我们说的继承多态性,另外一个就是方法多态性。方法多态性是指某个类可以包含多个同名方法,但签名不同。系统在进行调用时自动匹配签名最相近的一个方法。我们平时使用最多的Console.WriteLine方法便有19个签名各异的"多态"。改造后的Visitor代码如下:

public class Visitor

{

  public virtual void Visit(Meat m)

  { this.Visit((object)m); }

  public virtual void Visit(Vegetable v)

  { this.Visit((object)v); }

  public virtual void Visit(Coffee c)

  { this.Visit((object)c); }

  public void Visit(Object f)

  {

    // do nothing

  }

}

可见,Visit方法总共有四个不同的签名。在缺省实现中,各方法都转而调用public void Visit(Object f)方法,也就是什么都不作。当客户调用Visit方法时,会自动匹配类型。如果匹配不上的类型也会最终落到public void Visit(Object f)方法的怀抱中。在Visitor的子类中,只需实现关注的焦点就行了。例如某人只吃蔬菜不吃肉,那么他就没有必要再复写Visit(Meat m)方法了。这么做带来的另外一个好处就是,当添加新的食品时,即使不改变Visitor的代码,也不会产生任何编译或运行时的错误。如果修改,也只需修改Visitor类并提供一个缺省实现。Visitor的子类如果并不关心新增加的类,便可不做任何改动。完整的代码如下:

using System;

using System.Collections;

public class Visitor

{

  public virtual void Visit(Meat m)

  { this.Visit((object)m); }

  public virtual void Visit(Vegetable v)

  { this.Visit((object)v); }

  public virtual void Visit(Coffee c)

  { this.Visit((object)c); }

  public void Visit(Object o)

 {

    // do nothing

  }

}

public class ZhangSan : Visitor

{

  public override void Visit(Coffee c)

  {

    Console.Write( "{0}: Take a cup of {1}, ",this, c);

    c.AddMilk();

    c.AddSugar();

    Console.WriteLine();

  }

  public override void Visit(Vegetable v)

  {

    Console.WriteLine( "{0}: Take some {1}", this, v );

  }

}

public class LiSi : Visitor

{

  public override void Visit(Coffee c)

  {

    Console.Write( "{0}: Take a cup of {1}, ",this, c);

    c.AddSugar();

    Console.WriteLine();

  }

  public override void Visit(Meat v)

  {

    Console.WriteLine( "{0}: Take some {1}", this, v );

  }

}

public abstract class Food

{

  public abstract void Accept( Visitor visitor );

}

public class Coffee: Food

{

  public override void Accept( Visitor visitor )

  {

    visitor.Visit(this);

  }

  public void AddSugar()

  {

    Console.Write("add sugar. ");

  }

  public void AddMilk()

  {

    Console.Write("add milk. ");

  }

}

public class Meat: Food

{

  public override void Accept( Visitor visitor )

  {

    visitor.Visit(this);

  }

}

public class Vegetable: Food

{

  public override void Accept( Visitor visitor )

  {

    visitor.Visit(this);

  }

}

public class BuffetDinner

{

  private ArrayList elements = new ArrayList();

  public void Attach( Food element )

  {

    elements.Add( element );

  }

  public void Detach( Food element )

  {

    elements.Remove( element );

  }

  public void Accept( Visitor visitor )

  {

    foreach( Food f in elements )

    f.Accept( visitor );

  }

}

public class Client

{

  public static void Main( string[] args )

  {

    BuffetDinner b = new BuffetDinner();

    b.Attach(new Coffee());

    b.Attach(new Vegetable());

    b.Attach(new Meat());

    ZhangSan z = new ZhangSan();

    LiSi l = new LiSi();

    b.Accept( z );

    Console.WriteLine("----------------------");

    b.Accept( l );

  }

}

"条件外置"是我对封装变化的一个理解。现在才发现,原来设计模式中还有很多深奥的东西等待开发呢。


相关文章

阻碍使用企业架构的原因及克服方法
世界级企业架构的行业挑战
企业架构和SOA架构的角色将融合
什么最适合您的组织?
相关文档

企业架构与ITIL
企业架构框架
Zachman企业架构框架简介
企业架构让SOA落地
相关课程

企业架构设计
软件架构案例分析和最佳实践
嵌入式软件架构设计—高级实践
企业级SOA架构实践
 
分享到
 
 
     


重构-使代码更简洁优美
Visitor Parttern
由表及里看模式
设计模式随笔系列
深入浅出设计模式-介绍
.NET中的设计模式
更多...   

相关培训课程

J2EE设计模式和性能调优
应用模式设计Java企业级应用
设计模式原理与应用
J2EE设计模式指南
单元测试+重构+设计模式
设计模式及其CSharp实现


某电力公司 设计模式原理
蓝拓扑 设计模式原理及应用
卫星导航 UML & OOAD
汤森路透研发中心 UML& OOAD
中达电通 设计模式原理
西门子 嵌入式设计模式
更多...   
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号