学习设计模式《二十四》——访问者模式

一、基础概念

访问者模式的本质是【预留后路,回调实现】。仔细思考访问者模式,它的实现主要是通过预先定义好调用的通路,在被访问的对象上定义accept方法,在访问者的对象上定义visit方法;然后在调用真正发生的时候,通过两次分发技术,利用预先定义好的通路,回调到访问者具体的实现上。 明白了访问者模式的本质,就可以在定义一些通用功能,或者涉及工具类的时候让访问者派上大用场。你可以把已经实现好的一些功能作为已有的对象结构,因为在今后可能会根据实际需要为它增加新的功能,甚至希望开放接口来让其他开发人员扩展这些功能,所以你可以用访问者模式来设计,在这个对象结构上预留好通用的调用通路,在以后添加功能,或者其他开发人员来扩展的时候,只需要提供新的访问者实现,就能够很好地加入到系统中来了。

**访问者模式的定义:**表示一个作用于某个对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

|--------|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 序号 | 认识访问者模式 | 说明 |
| 1 | 访问者的功能 | 访问者模式能给一系列对象透明地添加新功能,从而避免在维护期间对这一系列对象进行修改,而且还能变相实现复用访问者所具有的功能。 由于是针对一系列对象的操作,这也导致,如果只想给一系列对象中的部分对象添加功能,就会有些麻烦;而且要始终能保证把这一系列对象都调用到,不管是循环,还是递归,总之要让每个对象都要被访问到。 |
| 2 | 调用通路 | 访问者之所以能实现【为一系列对象透明地添加新功能】注意是透明的,也就是这一系列对象是不知道被添加功能的。 重要的就是依靠通用方法,访问者这边说要去访问,就提供一个访问的方法(如visit方法);而对象那边说,好的,我接受你的访问,提供一个接受访问的方法(如accept方法)。这两个方法并不代表任何具体的功能,只是构成一个调用的通路,那么真正的功能实现在哪里呢?又如何调用到呢? 很简单,就是在accept方法里面,回调visit方法,从而回调访问者的具体实现上,而这个访问者的具体实现的方法才是要添加的新的功能。 |
| 3 | 两次分发技术 | 访问者模式能够实现在不改变对象结构的情况下,就可以给对象结构中的类增加功能,实现这个效果所使用的核心技术就是两次分发的技术。 在访问者模式中,当客户端调用ObjectStructure的时候,会遍历ObjectStructure中所有的元素,调用这些元素的accept方法,让这些元素来接受访问,这是请求的第一次分发;在具体的元素对象中实现accept方法的时候,会回调访问者的visit方法,等于请求第二次分发了,请求被分发给访问者来进行处理,真正实现功能的正是访问者的visit方法。 两次分发技术使得客户端的请求不再被静态地绑定在元素对象上,这个时候真正执行什么样的功能同时取决于访问者类型和元素类型,就算是同一种元素类型,只要访问者的类型不一样,最终执行的功能也会不一样,这样一来,就可以在元素对象不变的情况下,通过改变访问者的类型来改变真正执行的功能。 两次分发技术还有一个优点,就是可以在程序运行期间进行动态的功能组装和切换,只需要在客户端调用时,组合使用不同的访问者对象实例即可。 |
| 4 | 为何不在Component中实现回调visit方法 | 为什么不把回调访问者方法的调用语句放到父类中去,这样不就可以复用了吗? 这是不可以的,虽然看起来是相似的语句,但其实是不同的,主要的玄机就是在传入的this身上。this是代表当前的对象实例,在企业客户对象中传递的时企业客户对象的实例,在个人客户对象中传递的是个人客户对象实例,这样在访问者的实现中,可以通过不同的对象实例来访问不同的实例对象的数据,如果把这句话放到父类中,那么传递的就是父类对象的实例,是没有子对象的数据的,因此不能放到父类中去。 |
| 5 | 空的访问方法 | 并不是所有的访问方法都需要实现,由于访问者模式默认的是访问对象结构中的所有元素,因此在实现某些功能的时候,如果不需要涉及某些元素的访问方法,那么这些方法可以实现为空的(如:这个访问者只想处理组合对象,那么访问叶子对象的方法就可以为空,尽管还需要访问所有的元素对象)。 还有一种就是有条件接受访问,在自己的accept方法中进行判断,满足要求的则接受,不满足要求的相当于空的方法方法,什么都不做。 |
[认识访问者模式]

|--------|------------------------------------------------------------------|---------------------------------------------------------------------------|
| 序号 | 访问者模式的优点 | 访问者模式的缺点 |
| 1 | 好的扩展性 能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。 | 对象结构变化很困难 不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高。 |
| 2 | 好的复用性 可以通过访问者来定义整个对象结构通用的功能,从而提供复用程度。 | 破坏封装 访问者模式通常需要对象结构开放内部数据给访问者和ObjectStructure,这破坏了对象的封装性。 |
| 3 | 分离无关行为 可以通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。 | |
[访问者模式的优缺点]

何时选用访问者模式?

1-如果想对一个对象接口实施一些依赖于对象结构中具体类的操作,可以使用访问者模式。

2-如果想对一个对象结构中的各个元素进行很多不同的而且不相关得操作,为了避免这些操作使得类变得杂乱,可以使用访问者模式。把这些操作分散到不同的访问者对象中去,每个访问者对象实现同一类功能。

3-如果对象结构很少变动,但是需要经常给对象结构中的元素对象定义新的操作,可以使用访问者模式。

二、访问者模式示例

业务需求:现在有一个客户管理的应用,需要对已有的客户管理功能进行扩展(已有的客户管理功能是:有两类客户:一类是企业客户;一类是个人客户,这两类客户都可以提出服务申请功能)已有的功能如下:

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.NoPattern
{
    /// <summary>
    /// 客户的父类
    /// </summary>
    abstract internal class Customer
    {
        /// <summary>
        /// 客户编号
        /// </summary>
        public string CustomerId { get; set; }=string.Empty;

        /// <summary>
        /// 客户名称
        /// </summary>
        public string CustomerName { get; set; } = string.Empty;

        /// <summary>
        /// 客户提出的服务请求方法
        /// </summary>
        public abstract void ServiceRequest();

    }//Class_end
}
cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.NoPattern
{
    /// <summary>
    /// 企业客户
    /// </summary>
    internal class EnterpriseCustomer : Customer
    {
        /// <summary>
        /// 联系人
        /// </summary>
        public string LinkMan {get; set;} = string.Empty;

        /// <summary>
        /// 联系电话
        /// </summary>
        public string TelPhone {get; set;} = string.Empty;

        /// <summary>
        /// 企业注册地址
        /// </summary>
        public string RegisterAddress {get; set;} = string.Empty;


        /// <summary>
        /// 企业客户提出的服务请求方法(这里只做示意)
        /// </summary>
        public override void ServiceRequest()
        {
            Console.WriteLine($"{this.CustomerName} 企业提出服务请求");
        }

    }//Class_end
}
cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.NoPattern
{
    /// <summary>
    /// 个人客户
    /// </summary>
    internal class PersonalCustomer : Customer
    {
        /// <summary>
        /// 联系电话
        /// </summary>
        public string TelPhone { get; set; } = string.Empty;

        /// <summary>
        /// 年龄
        /// </summary>
        public int Age { get; set; } = 0;
        
        /// <summary>
        /// 个人客户提出服务请求的方法(仅作示意)
        /// </summary>
        public override void ServiceRequest()
        {
            //个人客户提出的具体服务请求
            Console.WriteLine($"{this.CustomerName} 个人提出服务请求");
        }

    }//Class_end
}

可以看到,已有的客户管理功能是很少的,现在随着业务的发展,需要加强对客户管理的功能,假设现在需要增加以下功能:

《1》客户对公司产品的偏好分析:针对企业客户和个人客户有不同的分析策略,主要是根据以往的购买历史、潜在购买意向等进行分析,对于企业客户还要添加上客户所在行业的发展趋势、客户的发展预期等分析。

《2》客户价值在分析:针对企业客户和个人客户,有不同的分析方式和策略。主要是根据购买的金额大小、购买的产品和服务的多少、购买的频率等进行分析。

其实除了这些功能,还有很多潜在的功能,只是现在还没有要求实现(如:针对不同的客户进行需求调查;针对不同的客户进行满意度分析;客户消费预期分析等。虽然现在还不要求实现,但不排除今后有可能会要求实现)

2.1、不使用模式的示例

要实现上面要求的功能,也不难,一个很基本的思路是:既然不同类型的客户操作是不同的,那么在不同类型的客户中分别实现这些功能就可以了。

2.1.1、新增客户父类的功能扩展

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.NoPattern
{
    /// <summary>
    /// 客户的父类扩展
    /// </summary>
    abstract internal class CustomerExtand:Customer
    {
        /// <summary>
        /// 新增客户对公司产品的偏好分析
        /// </summary>
        public abstract void PredilectionAnalyze();

        /// <summary>
        /// 客户价值分析
        /// </summary>
        public abstract void WorthAnalyze();


    }//Class_end
}

2.1.2、新增企业客户的扩展新功能

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.NoPattern
{
    /// <summary>
    /// 企业客户
    /// </summary>
    internal class EnterpriseCustomerExtand : CustomerExtand
    {
        /// <summary>
        /// 联系人
        /// </summary>
        public string LinkMan { get; set; } = string.Empty;

        /// <summary>
        /// 联系电话
        /// </summary>
        public string TelPhone { get; set; } = string.Empty;

        /// <summary>
        /// 企业注册地址
        /// </summary>
        public string RegisterAddress { get; set; } = string.Empty;

        /// <summary>
        /// 企业客户提出的服务请求方法(这里只做示意)
        /// </summary>
        public override void ServiceRequest()
        {
            Console.WriteLine($"{this.CustomerName} 企业提出服务请求");
        }

        /// <summary>
        /// 企业客户对公司产品的偏好分析
        /// </summary>
        public override void PredilectionAnalyze()
        {
            //根据以往的购买历史,潜在的购买意向,以及客户所在行业的发展趋势预期等分析
            Console.WriteLine($"现状对企业客户【{this.CustomerName}】进行产品偏好分析");
        }

        /// <summary>
        /// 企业客户价值分析
        /// </summary>
        public override void WorthAnalyze()
        {
            //根据购买的金额大小,购买产品的产品和服务的多少,购买频率分析(企业客户的标准会比个人客户高)
            Console.WriteLine($"现在对企业客户【{this.CustomerName}】进行价值分析");
        }
    }//Class_end
}

2.1.3、新增个人客户扩展新功能

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.NoPattern
{
    /// <summary>
    /// 个人客户
    /// </summary>
    internal class PersonalCustomerExtand : CustomerExtand
    {
        /// <summary>
        /// 联系电话
        /// </summary>
        public string TelPhone { get; set; } = string.Empty;

        /// <summary>
        /// 年龄
        /// </summary>
        public int Age { get; set; } = 0;

        /// <summary>
        /// 个人客户提出服务请求的方法(仅作示意)
        /// </summary>
        public override void ServiceRequest()
        {
            //个人客户提出的具体服务请求
            Console.WriteLine($"{this.CustomerName} 个人提出服务请求");
        }

        /// <summary>
        /// 个人客户对公司产品的偏好分析
        /// </summary>
        public override void PredilectionAnalyze()
        {
            Console.WriteLine($"现在对个人客户【{this.CustomerName}】进行产品偏好分析");
        }

        /// <summary>
        /// 个人客户价值分析
        /// </summary>
        public override void WorthAnalyze()
        {
            Console.WriteLine($"现在对【{this.CustomerName}】进行价值分析");
        }
    }//Class_end
}

2.1.4、客户端测试

cs 复制代码
namespace VisitorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            NoPatternTest();
            Console.ReadLine();
        }


        /// <summary>
        /// 不使用模式的测试
        /// </summary>
        private static void NoPatternTest()
        {
            Console.WriteLine("------不使用模式的测试------");
            //准备测试数据
            List<NoPattern.CustomerExtand>customerList= PreparedTestDatas();

            foreach (var item in customerList)
            {
                //进行偏好分析
                item.PredilectionAnalyze();
                //进行价值分析
                item.WorthAnalyze();
                Console.WriteLine();
            }
        }

        //准备测试数据
        private static List<NoPattern.CustomerExtand> PreparedTestDatas()
        {
            List<NoPattern.CustomerExtand> customerList=new List<NoPattern.CustomerExtand>(); ;

            //为了测试方便准备的测试数据
            NoPattern.CustomerExtand ce1= new NoPattern.EnterpriseCustomerExtand();
            ce1.CustomerName = "XXX集团";
            customerList.Add(ce1);

            NoPattern.CustomerExtand ce2= new NoPattern.EnterpriseCustomerExtand();
            ce2.CustomerName = "XXX公司";
            customerList.Add(ce2);

            NoPattern.CustomerExtand ce3= new NoPattern.EnterpriseCustomerExtand();
            ce3.CustomerName = "张三";
            customerList.Add(ce3);
            
            return customerList;
        }

    }//Class_end
}

2.1.5、运行结果

2.1.6、有何问题?

上面的实现已经满足了现在的要求,这种实现有没有什么问题呢?仔细分析会发现存在如下两个问题:

《1》在企业客户和个人客户的类中,都分别实现了提出服务请求、进行产品偏好分析、进行客户价值分析等功能(也就是说:这些功能的实现代码是混杂在同一个类中的,而且相同的功能分散到了不同的类中去实现,会导致整个系统难以理解、难以维护)。

《2》更为痛苦的是:采用这样的实现方式,如果要给客户扩展新的功能(如:前面提到的针对不同的客户进行需求调查、针对不同的客户进行满意度分析、客户消费预期分析等)每次扩展,都需要改动企业客户的类和个人客户的类,当然也可以通过它们扩展子类的方式,但是这样可能会造成过得的对象层次。

那么有没有办法,能够在不改变客户对象结构中各元素类的前提下,为这些类定义新的功能?也就是要求不改变企业客户和个人客户类,就能为企业客户和个人客户类定义新的功能?

用来解决上面问题的一个合理方案就是访问者模式。

2.2、使用访问者模式示例一

2.2.1、思路分析

仔细分析上面的示例(对于客户这个对象结构,不想改变类,又要添加新的功能,很明显就需要一种到你给他的方式,在运行期间把功能动态地添加到对象结构中去)。有些朋友可能会想起装饰模式,装饰模式可以实现为一个对象透明地添加功能,但装饰模式基本上在现有功能的基础之上进行功能添加,实际上是对现有功能的加强或者改造,并不是在现有功能不改动的情况,为对象添加新的功能。访问者模式解决这个问题的基本思路如下:

《1》定义一个接口来代表需要新加入的功能,为了通用,就定义一个通用的功能方法来代表新加入的功能。

《2》在对象结构上添加一个方法,作为通用的功能方法,也就是可以代表被添加的功能,在这个方法中传入具体的实现新功能的对象。

《3》在对象结构的具体实现对象中实现这个方法,回调传入具体的实现新功能的对象,就相当于调用到新功能上了。

《4》提供一个能够循环访问整个对象结构的类,让这个类来提供符合客户端业务需求的方法,来满足客户端调用的需要。

这样一来,只要提供实现新功能的对象给对象结构,就可以为这些对象添加新的功能,由于在对象结构中定义的方法是通用的功能方法,所以什么新功能都可以加入。

**要使用访问者模式来重写示例,首先是要按照访问者模式的结构,分离出两个类层次来,一个是对应于元素的类层次;**一个是对应于访问者的类层次。元素的类层次已经有了(就是客户的对象层次);对应于访问者的类层次目前还没有,不过,按照访问者模式的结构,应该是先定义一个访问者接口,然后把每种业务实现成为一个单独的访问者对象,也就是说应该使用一个访问者对象来实现对客户的偏好分析,而用另外一个访问者对象来实现对客户的价值分析。

2.2.2、定义客户类的父类

在客户类的父类中新增接受访问者访问的方法;然后把能够分离出去放到访问者中实现的方法,从该客户的父类中删除(如:客户提出请求的方法、客户偏好分析方法、客户价值分析方法)。

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 各种客户的父类
    /// </summary>
    abstract internal class Customer
    {
        /// <summary>
        /// 客户的编号
        /// </summary>
        public string CustomerId { get; set; }=string.Empty;

        /// <summary>
        /// 客户名称
        /// </summary>
        public string CustomerName { get; set; } = string.Empty;

        /// <summary>
        /// 接受访问者的访问
        /// </summary>
        /// <param name="visitor">访问者</param>
        public abstract void Accept(IVisitor visitor);


    }//Class_end
}

2.2.3、企业客户类的实现

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 企业客户
    /// </summary>
    internal class EnterpriseCustomer : Customer
    {
        //联系人
        public string LinkMan { get; set; } = string.Empty;
        //联系电话
        public string TelPhone { get; set; } = string.Empty;
        //企业注册地址
        public string RegisterAddress { get; set; } = string.Empty;

        public override void Accept(IVisitor visitor)
        {
            //回调访问者对象的方法
            visitor.VisitEnterpriseCustomer(this);
        }
    }//Class_end
}

2.2.4、个人客户类实现

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 个人客户
    /// </summary>
    internal class PersonalCustomer : Customer
    {
        //联系电话
        public string TelPhone { get; set; } = string.Empty;
        //年龄
        public int Age { get; set; } = 0;

        public override void Accept(IVisitor visitor)
        {
           //回调访问者对象方法
           visitor.VisitPersonalCustomer(this);
        }
    }//Class_end
}

2.2.5、访问者接口定义

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 访问者接口
    /// </summary>
    internal interface IVisitor
    {
        /// <summary>
        /// 访问企业客户,相当于给企业客户添加访问者的功能
        /// </summary>
        /// <param name="ec">企业客户对象</param>
        void VisitEnterpriseCustomer(EnterpriseCustomer ec);

        /// <summary>
        /// 访问个人客户,相当于给个人客户添加访问者功能
        /// </summary>
        /// <param name="pc">个人客户对象</param>
        void VisitPersonalCustomer(PersonalCustomer pc);

    }//Interface_end
}

2.2.6、各个访问者的实现

接下来实现的每个访问者对象类都只负责一类的功能处理。

《1》实现客户提出请求服务的功能访问者

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 具体的访问者,实现客户提出的服务请求功能
    /// </summary>
    internal class ServiceRequestVisitor : IVisitor
    {
        public void VisitEnterpriseCustomer(EnterpriseCustomer ec)
        {
            //企业客户提出的具体服务请求
            Console.WriteLine($"【{ec.CustomerName}】企业提出服务请求");
        }

        public void VisitPersonalCustomer(PersonalCustomer pc)
        {
           //个人客户提出的具体服务请求
           Console.WriteLine($"【{pc.CustomerName}】客户提出服务请求");
        }
    }//Class_end
}

《2》实现对客户偏好分析功能的访问者

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 具体的访问者,实现对客户的偏好分析
    /// </summary>
    internal class PredilectionAnalyzeVisitor : IVisitor
    {
        public void VisitEnterpriseCustomer(EnterpriseCustomer ec)
        {
            //根据以往购买的历史,潜在购买意向以及客户所在行业的发展趋势,客户的预期等分析
            Console.WriteLine($"现在对企业客户【{ec.CustomerName}】进行产品偏好分析");
        }

        public void VisitPersonalCustomer(PersonalCustomer pc)
        {
            Console.WriteLine($"现在对个人客户【{pc.CustomerName}】进行偏好分析");
        }
    }//Class_end
}

2.2.7、对象结构的实现

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 对象结构,通常这里对元素对象进行遍历,让访问者能访问到所有元素
    /// </summary>
    internal class ObjectStructure
    {
        //要操作的客户列表
        private List<Customer> tmpList = new List<Customer>();

        /// <summary>
        /// 提供给客户端操作的高层接口,具体的功能由客户端传入的访问者决定
        /// </summary>
        /// <param name="visitor">客户端需要使用的访问者</param>
        public void HandleRequest(IVisitor visitor)
        {
            foreach (var customer in tmpList)
            {
                customer.Accept(visitor);
            }
            
        }

        /// <summary>
        /// 组件对象结构,向对象结构中添加元素(不同对象结构有不同的构建方式)
        /// </summary>
        /// <param name="customer">需加入的对象</param>
        public void AddElement(Customer customer)
        {
            this.tmpList.Add(customer);
        }

    }//Class_end
}

2.2.8、客户端测试

cs 复制代码
namespace VisitorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            VisitorDemoOneTest();

            Console.ReadLine();
        }

        /// <summary>
        /// 访问者模式示例一测试
        /// </summary>
        private static void VisitorDemoOneTest()
        {
            Console.WriteLine("------访问者模式示例一测试------");
            //创建结构对象
            VisitorDemoOne.ObjectStructure oe=new VisitorDemoOne.ObjectStructure();
            //准备测试数据
            VisitorDemoOne.Customer cr1=new VisitorDemoOne.EnterpriseCustomer();
            cr1.CustomerName = "XXX集团";
            oe.AddElement(cr1);   
            
            VisitorDemoOne.Customer cr2=new VisitorDemoOne.EnterpriseCustomer();
            cr2.CustomerName = "XXX公司";
            oe.AddElement(cr2);

            VisitorDemoOne.Customer cr3 = new VisitorDemoOne.PersonalCustomer();
            cr3.CustomerName = "张三";
            oe.AddElement(cr3);

            //客户提出服务请求,传入服务请求的Visitor
            VisitorDemoOne.IVisitor visitor = new VisitorDemoOne.ServiceRequestVisitor();
            oe.HandleRequest(visitor);
            Console.WriteLine();
            
            //要对客户进行偏好分析,传入偏好分析的Visitor
            visitor = new VisitorDemoOne.PredilectionAnalyzeVisitor();
            oe.HandleRequest(visitor);
            Console.WriteLine();

        }

    }//Class_end
}

2.2.9、运行结果

在这里使用访问者模式重新实现了前面的示例功能,把各类相同的功能放在单独的访问者对象中,使得代码不再杂乱,系统结构也更加清晰,能方便地维护了,解决了前面示例的一个问题。

2.2.10、功能扩展

现在给访问者模式扩展新功能(即扩展客户价值分析功能);只用把新的功能实现为具体的访问者,然后在客户端调用的时候使用这个访问者对象来访问对象结构即可。

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoOne
{
    /// <summary>
    /// 具体的访问者,实现对客户价值的分析
    /// </summary>
    internal class WorthAnalyzeVisitor : IVisitor
    {
        public void VisitEnterpriseCustomer(EnterpriseCustomer ec)
        {
            //根据购买金额的大小,购买的产品和服务的多少、购买的频率进行分析,企业客户的标准必个人客户高
            Console.WriteLine($"现在对企业客户【{ec.CustomerName}】进行价值分析");
        }

        public void VisitPersonalCustomer(PersonalCustomer pc)
        {
            Console.WriteLine($"现在对个人客户【{pc.CustomerName}】进行价值分析");
        }
    }//Class_end
}

2.2.11、客户端测试

cs 复制代码
namespace VisitorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            VisitorDemoOneTest();
            Console.ReadLine();
        }

        /// <summary>
        /// 访问者模式示例一测试
        /// </summary>
        private static void VisitorDemoOneTest()
        {
            Console.WriteLine("------访问者模式示例一测试------");
            //创建结构对象
            VisitorDemoOne.ObjectStructure oe=new VisitorDemoOne.ObjectStructure();
            //准备测试数据
            VisitorDemoOne.Customer cr1=new VisitorDemoOne.EnterpriseCustomer();
            cr1.CustomerName = "XXX集团";
            oe.AddElement(cr1);   
            
            VisitorDemoOne.Customer cr2=new VisitorDemoOne.EnterpriseCustomer();
            cr2.CustomerName = "XXX公司";
            oe.AddElement(cr2);

            VisitorDemoOne.Customer cr3 = new VisitorDemoOne.PersonalCustomer();
            cr3.CustomerName = "张三";
            oe.AddElement(cr3);

            //客户提出服务请求,传入服务请求的Visitor
            VisitorDemoOne.IVisitor visitor = new VisitorDemoOne.ServiceRequestVisitor();
            oe.HandleRequest(visitor);
            Console.WriteLine();
            
            //要对客户进行偏好分析,传入偏好分析的Visitor
            visitor = new VisitorDemoOne.PredilectionAnalyzeVisitor();
            oe.HandleRequest(visitor);
            Console.WriteLine();

            //新增对客户的价值分析,传入价值分析的Visitor
            visitor = new VisitorDemoOne.WorthAnalyzeVisitor();
            oe.HandleRequest(visitor);
        }

    }//Class_end
}

2.2.12、运行结果

可以看到现在扩展新功能和使用都十分方便,且不用对原有的客户类进行任何修改。

2.3、使用访问者模式示例二

访问者模式一个很常见的应用,就是和组合模式结合使用,通过访问者模式来给由组合模式构建的对象结构增加功能。对于使用组合模式构建的组合对象结构,对外有一个统一的外观,要想添加新的功能也不是很困难,只要在组件的接口上定义新的功能就可以了,槽糕的是这样一来,需要修改所有的子类。而且,每次添加一个新功能,都需要修改组件接口,然后修改所有的子类。

为了让组合对象结构更灵活、更容易维护和有更好的扩展性,可以把它改造成访问者模式和组合模式合起来实现。这样在今后进行功能改造的时候,就不需要再次改动这个组合对象结构了。

访问者模式和组合模式组合使用的思路:首先把组合对象结构中的功能方法分离出来,虽然维护组合对象结构的方法也可以分离出来,但是为了维持组合对象结构本身,这些方法还是放在组合对象结构中,然后把这些功能方法分别实现成访问者对象,通过访问者模式添加到组合的对象结构中去。

如下的示例就是通过访问者模式和组合模式组合起来实现的功能(输出对象的名称,在组合对象的名称前面添加"节点",在叶子对象的签名添加"叶子")。

2.3.1、定义访问者接口

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 访问组合对象结构的访问者接口
    /// </summary>
    internal interface IVisitor
    {
        /// <summary>
        /// 访问组合对象,相当于给组合对象添加访问者的功能
        /// </summary>
        /// <param name="composite">组合对象</param>
        void VisitComposite<T>(T composite);

        /// <summary>
        /// 访问叶子对象,相当于给叶子对象添加访问者的功能
        /// </summary>
        /// <param name="leaf">叶子对象</param>
        void VisitLeaf<T>(T leaf);

    }//Interface_end
}

2.3.2、改造组合对象的定义

对已有的组合对象进行改造,添加通用的功能方法,当然在参数上需要传入访问者。先在组件定义上添加这个方法,然后到具体的实现类中去实现。除了新添加这个方法外,组件定义没有其他改变。

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 抽象的组件对象,相当于访问者模式中的元素对象
    /// </summary>
    abstract internal class Component
    {
        /// <summary>
        /// 接受访问者的访问
        /// </summary>
        /// <param name="visitor">访问者对象</param>
        public abstract void Accept(IVisitor visitor);

        /// <summary>
        /// 向组合对象中加入组件对象
        /// </summary>
        /// <param name="child">被加入组合对象中的组件对象</param>
        public virtual void AddChild(Component child)
        {
            //默认实现,抛出例外,叶子对象没有这个功能或子组件没有实现这个功能
            Console.WriteLine("对象不支持该功能!!!"); 
        }

        /// <summary>
        /// 从组合对象中移除组件对象
        /// </summary>
        /// <param name="child">被移除的组件对象</param>
        public virtual void RemoveChild(Component child)
        {
            //默认实现,抛出例外,叶子对象没有这个功能或子组件没有实现这个功能
            Console.WriteLine("对象不支持该功能!!!");
        }

        /// <summary>
        /// 返回某个索引对应的组件对象
        /// </summary>
        /// <param name="index">需要获取的组件对象索引,索引从0开始</param>
        /// <returns>索引对应的组件对象</returns>
        public virtual Component GetChildren(int index)
        {
            Console.WriteLine("对象不支持这个功能");
            return null;
        }

    }//Class_end
}

2.3.3、实现组合对象和叶子对象

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 组合对象,可以包含其他组合对象或叶子对象
    /// 相当于访问者模式的具体Element实现对象
    /// </summary>
    internal class Composite : Component
    {
        //用来存储组合对象中包含的子组件对象
        protected List<Component> componentList=new List<Component>();
        //组合对象的名称
        protected string name=string.Empty;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="name">组合对象的名称</param>
        public Composite(string name)
        {
            this.name = name;
        }

        public string GetName
        {
            get { return name; }
        }

        public override void Accept(IVisitor visitor)
        {
            //回调访问者对象的对应方法
            visitor.VisitComposite(this);
            //循环子元素,让子元素也接受访问
            foreach (var item in componentList)
            {
                //调用子对象接受访问,变相实现递归
                item.Accept(visitor);
            }
        }

        public override void AddChild(Component child)
        {
           componentList?.Add(child);
        }

        public override void RemoveChild(Component child)
        {
            componentList?.Remove(child);
        }

        public override Component GetChildren(int index)
        {
            return componentList[index];
        }

    }//Class_end
}
cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 叶子对象,相当于访问者模式的具体Element实现对象
    /// </summary>
    internal class Leaf : Component
    {
        //叶子对象的名称
        private string name = string.Empty;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="name">需传入的叶子对象名称</param>
        public Leaf(string name)
        {
            this.name = name;
        }

        public string GetName
        {
            get { return name; }
        }

        public override void Accept(IVisitor visitor)
        {
            //回调访问者对象的相应方法
            visitor.VisitLeaf(this);
        }
    }//Class_end
}

2.3.4、实现一个访问者

组合对象已经改造好了,现在需要提供一个访问者的实现,它会实现真正的功能,也就是要添加到对象结构中的功能。

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 具体的访问者,实现:输出对象的名称,在组合读写的名称前面添加"节点",在叶子对象的名称前面添加"叶子"
    /// </summary>
    internal class PrintNameVisitor : IVisitor
    {

        public void VisitComposite<T>(T composite)
        {

            Composite ce= composite as Composite;
            if (ce!=null)
            {
                //访问到组合对象的数据
                Console.WriteLine($"节点【{ce.GetName}】");
            }
           
        }

        public void VisitLeaf<T>(T leaf)
        {
            Leaf lf= leaf as Leaf;
            if (lf!=null)
            {
                Console.WriteLine($"叶子【{lf.GetName}】");
            }
        }
    }//Class_end
}

2.3.5、实现访问所有元素对象的结构对象------ObjectStructure

访问者是给一系列对象添加功能的,因此一个访问者需要访问所有的对象。为了方便遍历整个对象结构,通常会定义一个专门的类出来,在这个类中进行元素迭代访问,同时这个类提供客户端访问元素的接口。在我们这个示例中,由于组合对象结构中,已经实现了对象结构的遍历,本来是可以不需要ObjectStructure的,但是为了更清晰地展示访问者模式的结构,也为了今后的扩展或实现方便,还是定义一个ObjectStructure。

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 对象结构,通常这里对元素对象进行遍历,让访问者能访问到所有元素
    /// </summary>
    internal class ObjectStructure
    {
        //表示对象结构,可以是一个组合结构
        private Component root = null;

        /// <summary>
        /// 提供客户端操作的高层接口
        /// </summary>
        /// <param name="visitor">客户端需要使用的访问者</param>
        public void HandleRequest(IVisitor visitor)
        {
            //让组合对象结构中的根元素,接受访问,在组合对象结构中已经实现元素遍历
            if (root != null)
            {
                root.Accept(visitor);
            }
        }

        /// <summary>
        /// 传入组合对象结构
        /// </summary>
        /// <param name="component">组合对象结构</param>
        public void SetRoot(Component component)
        {
            this.root = component;
        }

    }//Class_end
}

2.3.6、客户端测试

cs 复制代码
namespace VisitorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            VisitorDemoTwoTest();

            Console.ReadLine();
        }

        /// <summary>
        /// 访问者模式示例二测试
        /// </summary>
        private static void VisitorDemoTwoTest()
        {
            Console.WriteLine("------访问者模式示例二测试------");

            //定义所有的组合对象
            VisitorDemoTwo.Component root=new VisitorDemoTwo.Composite("服装");
            VisitorDemoTwo.Component c1 = new VisitorDemoTwo.Composite("男装");
            VisitorDemoTwo.Component c2 = new VisitorDemoTwo.Composite("女装");

            //定义所有的叶子对象
            VisitorDemoTwo.Component l1 = new VisitorDemoTwo.Leaf("衬衣");
            VisitorDemoTwo.Component l2 = new VisitorDemoTwo.Leaf("夹克");
            VisitorDemoTwo.Component l3 = new VisitorDemoTwo.Leaf("裙子");
            VisitorDemoTwo.Component l4 = new VisitorDemoTwo.Leaf("套装");

            //按照树的结构组合对象和叶子对象
            root.AddChild(c1); 
            root.AddChild(c2);

            c1.AddChild(l1);
            c1.AddChild(l2);

            c2.AddChild(l3);
            c2.AddChild(l4);

            //创建ObjectStructure
            VisitorDemoTwo.ObjectStructure oe=new VisitorDemoTwo.ObjectStructure();
            oe.SetRoot(root);

            //调用ObjectStructure来处理请求功能
            VisitorDemoTwo.IVisitor visitor=new VisitorDemoTwo.PrintNameVisitor();
            oe.HandleRequest(visitor);
            Console.WriteLine();

        }

    }//Class_end
}

2.3.7、运行结果

现在可以仔细想一想,访问者模式是如何实现动态地给组件添加功能的?尤其是实现的机制是什么?真正实现新功能的地方在哪里?

访问者的方法就相当于作用于组合对象结构中各个元素的操作,是一种通用的表达,同样的访问者接口和同样的方法,只是提供不同的访问者具体实现,就表示不同的功能。同时在组合对象中,接受访问的方法,也是一个通用的表达,不管你是什么样的功能,统统接受就可以了,然后回调回去真正执行的功能。这样一来,各元素的类就不用再修改了,只要提供不同的访问者实现,然后通过这个通用的表达,就结合到组合对象中来了,相当于给所有的对象提供了新的功能。

2.3.8、谁负责遍历所有元素对象

在访问者模式中,访问者必须要能够访问到对象结构中的每个对象,因为访问者要为每个对象添加功能,为此特别在模式中定义了一个ObjectStructure,然后有ObjectStructure负责遍历访问一系列对象中的每个对象。在ObjectStruncture迭代多有的元素时,有分为如下两种情况:

《1》元素的对象结构是通过集合来组织的,因此直接在ObjectStructure中对结合进行迭代,然后对每一个元素调用accept就可以了。

《2》元素的对象结构是通过组合模式来组织的,通常可以构成对象树,这种情况一般就不需要ObjectStructure中迭代了。通常的做法是在组合对象的accept方法中,递归遍历它的子元素,然后调用子元素的accept方法。

不需要ObjectStructure的场景:

《1》只有一个被访问对象的时候,就不用了。

《2》访问的组合对象结构,从客户端的角度来看,它访问的其实就是一个对象,因此可以把ObjectStructure去掉,然后直接从客户端调用元素的accept方法,如下所示可以直接将ObjectStructure去掉,客户端可以直接调用组合对象结构的根元素的accept方法。

《1》扩展组合对象

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 组合对象,可以包含其他组合对象或叶子对象
    /// 相当于访问者模式的具体Element实现对象
    /// </summary>
    internal class CompositeExtand : Composite
    {
        public CompositeExtand(string name) : base(name)
        {
        }

        public override void Accept(IVisitor visitor)
        {
            //在这里只用回调访问方法即可,不用遍历所有子元素
           visitor.VisitComposite(this);
        }

        /// <summary>
        /// 新增方法【获取其包含的子组件】
        /// </summary>
        /// <returns></returns>
        public List<Component> GetChildComponent()
        {
            return base.componentList;
        }

    }//Class_end
}

《2》实现新的访问者对象

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VisitorPattern.VisitorDemoTwo
{
    /// <summary>
    /// 具体的访问者,实现:输出组合对象自身的结构
    /// </summary>
    internal class PrintStructVisitor : IVisitor
    {
        //前缀
        private string preStr = "";

        public void VisitComposite<T>(T composite)
        {
            CompositeExtand ce = composite as CompositeExtand;
            if (ce != null)
            {
                //先把自己输出
                Console.WriteLine($"{preStr}{ce.GetName}");
                //若还包含子组件,则输出这些子组件对象
                if (ce.GetChildComponent().Count>0)
                {
                    //然后添加一个空格,表示向后缩进一个空格
                    preStr += " ";
                    //输出当前对象的子对象
                    foreach (var item in ce.GetChildComponent())
                    {
                        item.Accept(this);
                    }

                    //把循环子对象多加入的一个空格去掉
                    preStr=preStr.Substring(0, preStr.Length-1);
                }
            }
        }

        public void VisitLeaf<T>(T leaf)
        {
            Leaf lf = leaf as Leaf;
            if (lf != null)
            {
                Console.WriteLine($"{preStr}-{lf.GetName}");
            }
        }
    }//Class_end
}

《3》客户端测试和运行结果

cs 复制代码
namespace VisitorPattern
{
    internal class Program
    {
        static void Main(string[] args)
        {
            VisitorDemoTwoTest2();
            Console.ReadLine();
        }

        /// <summary>
        /// 访问者模式示例二测试2
        /// </summary>
        private static void VisitorDemoTwoTest2()
        {
            Console.WriteLine("------访问者模式示例二测试------");

            //定义所有的组合对象
            VisitorDemoTwo.Component root = new VisitorDemoTwo.CompositeExtand("服装");
            VisitorDemoTwo.Component c1 = new VisitorDemoTwo.CompositeExtand("男装");
            VisitorDemoTwo.Component c2 = new VisitorDemoTwo.CompositeExtand("女装");

            //定义所有的叶子对象
            VisitorDemoTwo.Component l1 = new VisitorDemoTwo.Leaf("衬衣");
            VisitorDemoTwo.Component l2 = new VisitorDemoTwo.Leaf("夹克");
            VisitorDemoTwo.Component l3 = new VisitorDemoTwo.Leaf("裙子");
            VisitorDemoTwo.Component l4 = new VisitorDemoTwo.Leaf("套装");

            //按照树的结构组合对象和叶子对象
            root.AddChild(c1);
            root.AddChild(c2);

            c1.AddChild(l1);
            c1.AddChild(l2);

            c2.AddChild(l3);
            c2.AddChild(l4);

            VisitorDemoTwo.PrintStructVisitor visitor = new VisitorDemoTwo.PrintStructVisitor();
            root.Accept(visitor);

        }

    }//Class_end
}

三、项目源码工程

kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern

相关推荐
晨米酱6 小时前
JavaScript 中"对象即函数"设计模式
前端·设计模式
数据智能老司机11 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机12 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机12 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机12 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
使一颗心免于哀伤12 小时前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
数据智能老司机1 天前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
数据智能老司机1 天前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
烛阴1 天前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
李广坤1 天前
工厂模式
设计模式