1: Override VS. Overload
多态可以说是面向对象世界中一件锋利的武器, 封装变化是它的能力的体现。但是你听说过几种多态?
Simple Polymorphism :the object whose method is called is decided run-time.
multi- polymorphism :the object which method is called is decided upon the type of the argument
如果你对这两句描述不是很清楚, 那你知道override和overload吗?Simple Polymorphism 就意味使用了override, 而multi- polymorphism则意味着使用了overload
前者可能你比较熟悉,后者呢?两者又有什么不同?什么情况下我们会需要后者呢?你见过它们同时出现吗?
多态是用于封装变化的,比如常见的那个Shape Draw的例子。Client不用考虑具体是哪个Shape,通过多态自然能调用到相应的那个Shape的Draw方法(whose method)。但是这时我们只有一个变化的对象――Shape。 如果画的地方也变呢?比如我可以画在屏幕上, 也可以画到打印机上。现在我们有两个同时会变的因素, 那么Draw方法又通过什么来实现封装变化呢? Simple Polymorphism 显然是不够用了,multi- polymorphism 自然也该出场了。
就从一个游戏开始吧,在这个游戏中有一个怪物开门的场景。怪物有很多种,本游戏的出场人物包括了矮人和泰坦,门也有两种 :一种普通的木头门, 还有就是很重的铁门。
现在怪物和门登场了…
interface Monster {}
class Drawf : Monster {}
class Giant : Monster {}
class Door
{
public virtual void OpenBy(Monster monster)
{
Console.WriteLine("Who are u?");
}
{
Console.WriteLine("It's slowly opened");
}
public virtual void OpenBy(Giant giant)
{
Console.WriteLine("It's just easily broken...Crasp!");
}
}
{
public override void OpenBy(Drawf dwarf)
{
Console.WriteLine("It won't open");
}
public override void OpenBy(Giant giant)
{
Console.WriteLine("It's slowly opened");
}
}
这里为了同时封装两种变化(怪物和门),我也同时使用了override 和 overload 。
接着怪物开始开门了…
class Game
{
static void
{
Door ironDoor = new HeavyDoor();
ironDoor.OpenBy(new Drawf());
ironDoor.OpenBy(new Giant());
}
}
答案也很明显
这个例子同时应用了两种多态, 但是却只体现了第一种多态封装变化的效果!如何将两者都体现出来?看下面的测试
class Game
{
static void
{
Door ironDoor = new HeavyDoor();
List<Monster> monsters = new List<Monster>();
monsters.Add(new Drawf());
monsters.Add(new Giant());
ironDoor.OpenBy(m);
}
}
现在你能猜到结果吗?仔细想想别急着看答案。
很正常?Ok. 你可以直接去看下一节的内容了,如果猜错了请先复习一下下面的基础知识吧.
你肯定听说过所谓的”动态绑定”,通常意义上的多态也就是通过它实现的,简而言之――the object whose method is called is decided run-time . 重点就在于这个run-time . 而第二种多态――the object which method is called is decided upon the type of the argument 这里面可没有出现run-time 倒不是说它不支持,而是不一定,不过在c++, java,c# 中都不支持。 现在你可以理解为什么前面的答案出乎你的意料了。因为后者方法是静态绑定的, 也就是在编译期就确定了将要执行哪个Draw方法,而在编译期,编译器显然只能将monsters集合中的对象判断为Monster类型,从而去执行 void OpenBy(Monster monster)方法。
2: Visitor Pattern without Double Dispatch
通过上一节的例子和解释, 你应该对override, overload 以及他们各自的绑定机制――动态绑定和静态绑定有所了解。
接下来看一个更具实际意义的例子。假设我想做一个计算器,那么肯定要先建立一个表达式系统。
abstract class Expression
{
public abstract int Evaluate();
}
{
int constant;
public override int Evaluate()
{
return constant;
}
}
{
Expression left, right;
public override int Evaluate()
{
return left.Evaluate() + right.Evaluate();
}
}
这里利用多态很好的实现了表达式计算的任务。但是把计算的功能放在表达式中并不是一个良好的设计,随着表达式的类型越来越复杂,可能我们需要对表达式进行语法分析,进行类型检查, 设置判断表达式中有多少个常量,多少个变量。如果把这些功能都放在表达式中,一方面不符合责任分离的原则,二来一旦有了新的功能需求我们就要修改所有的表达式对象,维护的恶梦就这样开始了。
于是我就想到了用Visitor模式将计算的功能从Express类中分离出来。(将过多没有密切联系的功能从原来的对象中脱离出来,避免对象过于庞大,这是使用Visitor模式的最重要的原因)。下面是代码实现,请耐心看完。
abstract class Expression { }
class ConstantExpression : Expression
{
private int constant;
public int Constant
{
get { return constant; }
}
public ConstantExpression(int con)
{
constant = con;
}
}
class SumExpression : Expression
{
private Expression left, right;
public Expression Right
{
get { return right; }
}
public Expression Left
{
get { return left; }
}
{
this.left = left;
this.right = right;
}
}
{
public int Visit(ConstantExpression e)
{
return e.Constant;
}
public int Visit(SumExpression e)
{
return Visit(e.Left) + Visit(e.Right);
}
}
class Program
{
static void
{
ConstantExpression constExp = new ConstantExpression(10);
new ConstantExpression(1));
Console.WriteLine(evalVisitor.Visit(constExp));
Console.WriteLine(evalVisitor.Visit(sumExp));
}
}
看完上面的代码,可能很多人会有疑问了。 你这里用的是什么Visitor模式?Accept方法呢?IVisitor接口呢? 怎么Visit方法还有返回值?怎么和《Design Pattern》上的Visitor模式完全不是一回事?
为什么要和书上的一样呢?你难道没发现以上的疑问都体现了这个版本的优点?没有Accpet方法意味着在最初的设计中我根本不需要为以后是否需要使用Visitor模式做考虑。没有IVisitor接口,意味着我可以随意的定义我的Visit方法,而不需要一个统一的形式,比如这里为了计算方便我让Visit方法有了返回值。既然这个版本的Visitor模式这么好,怎么Gof没有想到? 呵呵,露馅了,因为上面的代码根本无法通过编译
class EvaluateVisitor
{
public int Visit(ConstantExpression e)
{
return e.Constant;
}
public int Visit(SumExpression e)
{
return Visit(e.Left) + Visit(e.Right);
}
}
联系第一节介绍的内容,你就会发现尽管e.Left在运行期是ConstantExpression类型,但是由于Overload的方法是静态绑定的, 而在编译期e.Left是Express类型, 但是我们根本没有提供Visit(Expression e)这样的方法, 编译自然出错了。
3: Visitor Pattern with Double Dispatch
回过头去看看传统的Visitor模式又是什么样子的呢?
abstract class Expression
{
public abstract void Accept(Visitor v);
}
class ConstantExpression : Expression
{
private int constant;
public int Constant
{
get { return constant; }
}
{
constant = con;
}
public override void Accept(Visitor v)
{
v.Visit(this);
}
}
class SumExpression : Expression
{
private Expression left, right;
public Expression Right
{
get { return right; }
}
public Expression Left
{
get { return left; }
}
public SumExpression(Expression left, Expression right)
{
this.left = left;
this.right = right;
}
public override void Accept(Visitor v)
{
v.Visit(this);
}
}
interface Visitor
{
void Visit(ConstantExpression e);
void Visit(SumExpression e);
}
class EvaluateVisitor : Visitor
{
private int result;
public int Result { get { return result; } }
{
result=e.Constant;
}
public void Visit(SumExpression e)
{
e.Left.Accept(this);
int lefeRe = this.result;
e.Right.Accept(this);
int rightRe = this.result;
result = lefeRe + rightRe;
}
}
class Program
{
static void
{
ConstantExpression constExp = new ConstantExpression(10);
SumExpression sumExp = new SumExpression(new ConstantExpression(1),
new ConstantExpression(1));
EvaluateVisitor evalVisitor = new EvaluateVisitor();
constExp.Accept(evalVisitor);
Console.WriteLine(evalVisitor.Result);
sumExp.Accept(evalVisitor);
Console.WriteLine(evalVisitor.Result);
}
}
可以看出这张图和前者相比,复杂了许多。但是只有这样我们才能在C#这种不直接支持Double Dispatch的语言中实现Visitor模式。
怎么解决的呢? 道理很简单就是用Twice Single Dispatch来模拟Double Dispatch。最能体现Twice Single Dispatch的代码如下:
public override void Accept(Visitor v)
{
v.Visit(this);
}
其一般形式为:
public void MethodA(A a)
{
a.MethodB(this);
}
其实说到底,不支持Double Dispatch就是由于不支持方法参数的动态绑定, 那我们就通过两次的O虚方法的动态绑定来模拟它。所以 a 又从方法参数变成了虚方法的拥有者,并把this作为参数传入。
明白了以上的道理,你也就知道为什么完全同样的Accpet方法没有被提到它们的基类中――-因为方法参数的类型是在编译期决定的。
看到了传统Visitor模式的解决思路,对于文章一开始的小游戏所引起的问题也就不难解决了。
4: Visitor Pattern with Reflection
再来看看第二节的模型,实在是很漂亮,让我不忍放弃。Gof 也想不出什么好的办法,只能无奈的采用了第三节的复杂模型。 不过,你记得吗?《Design Pattern》那本书可是十年前的作品。十年前可没有什么元数据,自然也没有Reflection。
通过反射我完全可以在运行期从函数参数中找回失去的类型信息。
我只要在第二节的EvaluateVisitor类中添加如下的一个方法,就万事告吉了。
public int Visit(Expression e)
{
Type[] types = new Type[] { e.GetType() };
MethodInfo mi = this.GetType().GetMethod("Visit", types);
if (mi==null)
throw new Exception("UnSupported!");
else
return (int)mi.Invoke(this,new object[]{e});
}
这个方法我个人是比较满意了,你觉得呢? 在写sample的时候,我总觉得泛型也应该能解决这个问题,不过在写完后也没想到一个比较方便的解决方案。可能是对泛型了解的还不够,装配脑袋来试试?
参考资料:
文章开始的Game的各种版本实现
Generic Double Dispatch Engine
Visitor模式全解