• Double Dispatch And Visitor Pattern


                                         

    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

          如果你对这两句描述不是很清楚, 那你知道overrideoverload吗?Simple Polymorphism 就意味使用了override, multi- polymorphism则意味着使用了overload

    前者可能你比较熟悉,后者呢?两者又有什么不同?什么情况下我们会需要后者呢?你见过它们同时出现吗?

    多态是用于封装变化的,比如常见的那个Shape Draw的例子。Client不用考虑具体是哪个Shape,通过多态自然能调用到相应的那个ShapeDraw方法(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?");
            }

            public virtual void OpenBy(Drawf dwarf)
            {
                Console.WriteLine("It's slowly opened");
            }

            public virtual void OpenBy(Giant giant)
            {
                Console.WriteLine("It's just easily broken...Crasp!");
            }
        }

        class HeavyDoor : Door
        {
            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 Main()
            {
                Door ironDoor = new HeavyDoor();
                ironDoor.OpenBy(new Drawf());
                ironDoor.OpenBy(new Giant());
            }
        }

        答案也很明显

     

    这个例子同时应用了两种多态, 但是却只体现了第一种多态封装变化的效果!如何将两者都体现出来?看下面的测试

    class Game
        {
            static void Main()
            {

                Door ironDoor = new HeavyDoor();
                List<Monster> monsters = new List<Monster>();
                monsters.Add(new Drawf());
                monsters.Add(new Giant());

                foreach (Monster m in monsters)
                    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++, javac# 中都不支持。 现在你可以理解为什么前面的答案出乎你的意料了。因为后者方法是静态绑定的, 也就是在编译期就确定了将要执行哪个Draw方法,而在编译期,编译器显然只能将monsters集合中的对象判断为Monster类型,从而去执行 void OpenBy(Monster monster)方法。

    2 Visitor Pattern without Double Dispatch

            通过上一节的例子和解释, 你应该对override, overload 以及他们各自的绑定机制――动态绑定和静态绑定有所了解。

            接下来看一个更具实际意义的例子。假设我想做一个计算器,那么肯定要先建立一个表达式系统。 

     abstract class Expression
    {
        public abstract int Evaluate();
    }

        class ConstantExpression : Expression
        {       
            
    int constant;
            public override int Evaluate()
            {
                return constant;
            }
        }

        class SumExpression : Expression
        {
            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; }
            }

            public SumExpression(Expression left, Expression right)
            {
                this.left = left;
                this.right = right;
            }
        }

     
       class EvaluateVisitor
        {
            public int Visit(ConstantExpression e)
            {
                return e.Constant;
            }
            public int Visit(SumExpression e)
            {
                return Visit(e.Left) + Visit(e.Right);
            }
        } 

        class Program
        {
            static void Main()
            {
                ConstantExpression constExp = new ConstantExpression(10); 
                SumExpression sumExp = new SumExpression(new ConstantExpression(1),
                                                         new ConstantExpression(1));

                EvaluateVisitor evalVisitor = new EvaluateVisitor();
                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.LeftExpress类型, 但是我们根本没有提供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; }
        }

        public ConstantExpression(int con)
        {
            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; } }

        public void Visit(ConstantExpression e)
        {
            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 Main()
        {
            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模式全解

  • 相关阅读:
    foreach和each
    one
    存储
    动态添加
    百度描点
    php环境配置
    图文并茂
    css实现鼠标移上去变大,旋转,转别人的额
    vagrant box打包前的准备
    VirtualBox压缩打包
  • 原文地址:https://www.cnblogs.com/idior/p/325036.html
Copyright © 2020-2023  润新知