(47)C#设计模式—— 访问者模式(Vistor Pattern)

定义

访问者模式是封装一些施加于某种数据结构之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于数据结构之上的操作之间的耦合度降低,使得操作集合可以相对自由地改变。
数据结构的每一个节点都可以接受一个访问者的调用,此节点访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过长叫做“双重分流”。节点调用访问者,将它自己传入,访问者则将某种算符针对此节点执行。

结构

访问者模式是用来封装某种数据结构中的方法。具体封装过程是:每个元素接收一个访问者的调用,每个元素的指定方法接收访问者对象作为参数传入,访问者对象则反过来调用元素对象的操作。这里要明确一点:访问者模式中具体访问者的数目和具体节点的数目没有任何关系

访问者模式涉及到的角色:

  • 抽象访问者角色:声明一个或多个访问操作,使得所有具体访问者必须实现的接口。
  • 具体访问者角色:实现抽象访问者角色中声明的所有接口。
  • 抽象节点角色:声明一个接受操作,接受一个访问者对象作为参数。
  • 具体节点角色:实现抽象元素所规定的接收操作。
  • 结构对象角色:节点的容器,可以包含多个不同类或接口的容器。

实现

在将实现之前,我想先不用访问者模式的方式来实现某个场景——现在我想遍历每个元素对象,然后调用每个元素对象的Print方法来打印该元素对象的信息。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _38VistorPatternDemo
{
    //抽象元素角色
    public abstract class Element
    {
        public abstract void Print();
    }
    //具体元素
    public class ElementA:Element
    {
        public override void Print()
        {
            Console.WriteLine("我是元素A");
        }
    }
    public class ElementB : Element
    {
        public override void Print()
        {
            Console.WriteLine("我是元素B");
        }
    }
    //对象结构
    public class ObjectStructure
    {
        private ArrayList elements = new ArrayList();
        public ArrayList Elements
        {
            get { return elements; }
        }
        public ObjectStructure()
        {
            Random ran = new Random();
            for (int i = 0; i < 6; i++)
            {
                int ranNum = ran.Next(10);
                if(ranNum>5)
                {
                    elements.Add(new ElementA());
                }
                else
                {
                    elements.Add(new ElementB());
                }
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            ObjectStructure objstructure = new ObjectStructure();
            //遍历对象结构中的对象集合,访问每个元素的Print方法打印元素
            foreach (Element e in objstructure.Elements)
            {
                e.Print();
            }
        }
    }
}

上面的代码很准确的解决了我们刚才提出的场景,但是需求在时刻变化,如果此时我们除了想打印元素的信息外,还想打印出访问元素的时间,此时我们不得不去修改每个元素的Print方法,再加入相对应的访问时间。这样设计显然不符合开闭原则,即某个方法的改变,要去改变每个元素类。既然,这里变化的点是操作的改变,而每个数据元素的数据结构是不变的。所以此时就思考——能不能把操作于元素的操作和元素本身的数据结构分开呢?解开这两者的耦合度,如果这样是操作发生变化,就不需要去修改元素本身了。但是如果是元素数据结构发生变化,就不得不去修改元素类了。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _38VistorPatternDemo
{
    //抽象元素角色
    public abstract class Element
    {
        public abstract void Accept(IVistor vistor);
        public abstract void Print();
    }
    //具体元素
    public class ElementA:Element
    {
        public override void Accept(IVistor vistor)
        {
            vistor.Visit(this);
        }
        public override void Print()
        {
            Console.WriteLine("我是元素A");
        }
    }
    public class ElementB : Element
    {
        public override void Accept(IVistor vistor)
        {
            vistor.Visit(this);
        }

        public override void Print()
        {
            Console.WriteLine("我是元素B");
        }
    }
    //抽象访问者
    public interface IVistor
    {
        void Visit(ElementA a);
        void Visit(ElementB b);
    }
    //具体访问者
    public class ConcreteVistor:IVistor
    {
        //visit方法是再去调用元素的Accept方法
        public void Visit(ElementA a)
        {
            a.Print();
        }
        public void Visit(ElementB b)
        {
            b.Print();
        }
    }
    //对象结构
    public class ObjectStructure
    {
        private ArrayList elements = new ArrayList();
        public ArrayList Elements
        {
            get { return elements; }
        }
        public ObjectStructure()
        {
            Random ran = new Random();
            for (int i = 0; i < 6; i++)
            {
                int ranNum = ran.Next(10);
                if(ranNum>5)
                {
                    elements.Add(new ElementA());
                }
                else
                {
                    elements.Add(new ElementB());
                }
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            ObjectStructure objstructure = new ObjectStructure();
            //遍历对象结构中的对象集合,访问每个元素的Print方法打印元素
            foreach (Element e in objstructure.Elements)
            {
                //单个元素接受访问者访问
                e.Accept(new ConcreteVistor());
            }
        }
    }
}

从上面代码可知,使用访问者模式实现上面场景后,元素Print方法的访问封装到访问者对象中了,此时客户端与元素的Print方法就隔离开了。如果需要添加打印访问时间的需求,此时只需要在添加一个具体的访问者类即可。

适用场景

  • 如果系统有比较稳定的数据结构,而又有易于变化的算法时,此时可以考虑使用访问者模式。因为访问者模式使得算法操作的添加比较容易。
  • 如果一组类中,存在着相似的操作,为了避免出现大量重复的代码,可以考虑把重复的操作封装到访问者中。(当然也可以考虑使用抽象类了)
  • 如果一个对象存在着一些与本身对象不相干,或关系比较弱的操作时,为了避免操作污染这个对象,则可以考虑把这些操作封装到访问者对象中。

优缺点

优点

  • 访问者模式使得添加新的操作变的容易。如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,添加新的操作会变得很复杂。而适用访问者模式,增加新的操作就意味着添加一个新的访问者类。因此,使得添加新的操作变的容易
  • 访问者模式使得有关的行为操作集中到一个访问者对象中,而不是分散到一个个元素类中。这点类似于中介者模式
  • 访问者模式可以访问属于不同的等级结构的成员对象,而迭代只能访问属于同一个等级结构的成员对象

缺点

  • 增加新的元素变的困难。每增加一个新的元素意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中添加对应的具体操作。

总结

访问者模式是用来封装一些作用于某种数据结构之上的操作。它使得可以在不改变元素本身的前提下增加作用于这些元素的新操作,访问者模式的目的是把操作从数据结构中分离出来。

你可能感兴趣的:(C#设计模式)