设计模式的征途—16.访问者(Visitor)模式

在患者就医时,医生会根据病情开具处方单,很多医院都会存在以下这个流程:划价人员拿到处方单之后根据药品名称和数量计算总价,而药房工作人员根据药品名称和数量准备药品,如下图所示。

在软件开发中,有时候也需要处理像处方单这样的集合对象结构,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式。在设计模式中,有一种模式可以满足上述要求,其模式动机就是以不同的方式操作复杂对象结构,该模式就是访问者模式。

访问者模式(Visitor) 学习难度:★★★★☆ 使用频率:★☆☆☆☆

一、OA系统员工数据汇总设计

1.1 需求背景

Background:M公司开发部想要为某企业开发一个OA系统,在该OA系统中包含一个员工信息管理子系统,该企业包括正式员工和临时工,每周HR部门和财务部等部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等等。该企业的基本制度如下:

(1)正式员工(Full time Employee)每周工作时间为40小时,不同级别、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时,所缺时间按照请假处理,请假锁扣工资以80元/小时计算,直到基本工资扣除到0为止。除了记录实际工作时间外,HR部需要记录加班时长或请假时长,作为员工平时表现的一项依据。

(2)临时员工(Part time Employee)每周工作时间不固定,基本工资按照小时计算,不同岗位的临时工小时工资不同。HR部只需要记录实际工作时间。

HR人力资源部和财务部工作人员可以根据各自的需要对员工数据进行汇总处理,HR人力资源部负责汇总每周员工工作时间,而财务部负责计算每周员工工资。

1.2 初始设计

  M公司开发人员针对需求,提出了一个初始的解决方案,其核心代码如下:

    public  class EmployeeList
    {
        // 员工集合
        private IList<Employee> empList = new List<Employee>();

        // 增加员工
        public void AddEmployee(Employee emp)
        {
            this.empList.Add(emp);
        }

        // 处理员工数据
        public void Handle(string deptName)
        {
            if (deptName.Equals("财务部"))
            {
                foreach (var emp in empList)
                {
                    if (emp.GetType().Equals("FullTimeEmployee"))
                    {
                        Console.WriteLine("财务部处理全职员工数据!");
                    }
                    else
                    {
                        Console.WriteLine("财务部处理兼职员工数据!");
                    }
                }
            }
            else if (deptName.Equals("人力资源部"))
            {
                foreach (var emp in empList)
                {
                    if (emp.GetType().Equals("FullTimeEmployee"))
                    {
                        Console.WriteLine("人力资源部处理全职员工数据!");
                    }
                    else
                    {
                        Console.WriteLine("人力资源部处理兼职员工数据!");
                    }
                }
            }
        }
    }

  不难发现,该解决方案存在以下问题:

  (1)EmployeeList类非常庞大,承担了过多的职责,既不便于代码复用,也不便于系统扩展,违背了单一职责原则。

  (2)包含了大量的if-else语句,测试和维护的难度增大。

  (3)如果要新增一个部门来操作员工数据集合,那么不得不修改EmployeeList类的源代码,违背了开闭原则。

  访问者模式是一个可以考虑用来解决的方案,它可以在一定程度上解决上述问题(大部分问题)。

二、访问者模式概述

2.1 访问者模式简介

  访问者模式是一种较为复杂的行为型模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。例如:处方单中的各种药品信息就是被访问的元素,而划价人员和药房工作人员就是访问者。访问者模式可以使得用户在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。

访问者(Visitor)模式:提供一个作用于某对象结构中的各元素的操作表示,它使得可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

2.2 访问者模式结构

  访问者模式结构图中包含以下5个角色:

  (1)Visitor(抽象访问者):抽象访问者为对象结构中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者则需要实现这些操作方法,定义对这些元素的访问操作。

  (2)ConcreteVisitor(具体访问者):具体访问者实现了抽象访问者声明的方法,每一个操作作用于访问对象结构中一种类型的元素。

  (3)Element(抽象元素):一般是一个抽象类或接口,定义一个Accept方法,该方法通常以一个抽象访问者作为参数。

  (4)ConcreteElement(具体元素):具体元素实现了Accept方法,在Accept方法中调用访问者的访问方法以便完成一个元素的操作。

  (4)ObjectStructure(对象结构):对象结构是一个元素的集合,用于存放元素对象,且提供便利其内部元素的方法。

三、重构OA系统员工数据汇总

3.1 重构后的设计结构

  在上图中,FinanceDepartment表示财务部,HRDepartment表示人力资源部,它们充当具体访问者的角色,其抽象父类Department充当抽象访问者角色;EmployeeList充当对象结构,用于存储员工列表;FullTimeEmployee表示全职员工,PartTimeEmployee表示兼职员工,它们充当具体元素角色,而其父类IEmployee(这里实现形式是interface)充当抽象元素角色。

3.2 具体代码实现

  (1)抽象元素=>IEmployee

    /// <summary>
    /// 抽象元素类:Employee
    /// </summary>
    public interface IEmployee
    {
        void Accept(Department handler);
    }

  (2)具体元素=>FullTimeEmployee,PartTimeEmployee

    /// <summary>
    /// 具体元素类:FullTimeEmployee
    /// </summary>
    public class FullTimeEmployee : IEmployee
    {
        public string Name { get; set; }
        public double WeeklyWage { get; set; }
        public int WorkTime { get; set; }

        public FullTimeEmployee(string name, double weeklyWage, int workTime)
        {
            this.Name = name;
            this.WeeklyWage = weeklyWage;
            this.WorkTime = workTime;
        }

        public void Accept(Department handler)
        {
            handler.Visit(this);
        }
    }

    /// <summary>
    /// 具体元素类:PartTimeEmployee
    /// </summary>
    public class PartTimeEmployee : IEmployee
    {
        public string Name { get; set; }
        public double HourWage { get; set; }
        public int WorkTime { get; set; }

        public PartTimeEmployee(string name, double hourWage, int workTime)
        {
            this.Name = name;
            this.HourWage = hourWage;
            this.WorkTime = workTime;
        }

        public void Accept(Department handler)
        {
            handler.Visit(this);
        }
    }

  (3)对象结构=>EmployeeList

    /// <summary>
    /// 对象结构类:EmployeeList
    /// </summary>
    public class EmployeeList
    {
        private IList<IEmployee> empList = new List<IEmployee>();

        public void AddEmployee(IEmployee emp)
        {
            this.empList.Add(emp);
        }

        public void Accept(Department handler)
        {
            foreach (var emp in empList)
            {
                emp.Accept(handler);
            }
        }

  (4)抽象访问者=>Department

    /// <summary>
    /// 抽象访问者类:Department
    /// </summary>
    public abstract class Department
    {
        // 声明一组重载的访问方法,用于访问不同类型的具体元素
        public abstract void Visit(FullTimeEmployee employee);
        public abstract void Visit(PartTimeEmployee employee);
    }

  (5)具体访问者=>FinanceDepartment,HRDepartment

    /// <summary>
    /// 具体访问者类:FinanceDepartment
    /// </summary>
    public class FinanceDepartment : Department
    {
        // 实现财务部对兼职员工数据的访问
        public override void Visit(PartTimeEmployee employee)
        {
            int workTime = employee.WorkTime;
            double hourWage = employee.HourWage;
            Console.WriteLine("临时工 {0} 实际工资为:{1} 元", employee.Name, workTime * hourWage);
        }

        // 实现财务部对全职员工数据的访问
        public override void Visit(FullTimeEmployee employee)
        {
            int workTime = employee.WorkTime;
            double weekWage = employee.WeeklyWage;

            if (workTime > 40)
            {
                weekWage = weekWage + (workTime - 40) * 50;
            }
            else if (workTime < 40)
            {
                weekWage = weekWage - (40 - workTime) * 80;
                if (weekWage < 0)
                {
                    weekWage = 0;
                }
            }

            Console.WriteLine("正式员工 {0} 实际工资为:{1} 元", employee.Name,  weekWage);
        }
    }

    /// <summary>
    /// 具体访问者类:HRDepartment
    /// </summary>
    public class HRDepartment : Department
    {
        // 实现人力资源部对兼职员工数据的访问
        public override void Visit(PartTimeEmployee employee)
        {
            int workTime = employee.WorkTime;
            Console.WriteLine("临时工 {0} 实际工作时间为:{1} 小时", employee.Name, workTime);
        }

        // 实现人力资源部对全职员工数据的访问
        public override void Visit(FullTimeEmployee employee)
        {
            int workTime = employee.WorkTime;
            Console.WriteLine("正式员工 {0} 实际工作时间为:{1} 小时", employee.Name, workTime);

            if (workTime > 40)
            {
                Console.WriteLine("正式员工 {0} 加班时间为:{1} 小时", employee.Name, workTime - 40);
            }
            else if (workTime < 40)
            {
                Console.WriteLine("正式员工 {0} 请假时间为:{1} 小时", employee.Name, 40 - workTime);
            }
        }
    }

  (6)客户端调用与测试

    public class Program
    {
        public static void Main(string[] args)
        {
            EmployeeList empList = new EmployeeList();
            IEmployee fteA = new FullTimeEmployee("梁思成", 3200.00, 45);
            IEmployee fteB = new FullTimeEmployee("徐志摩", 2000, 40);
            IEmployee fteC = new FullTimeEmployee("梁徽因", 2400, 38);
            IEmployee fteD = new PartTimeEmployee("方鸿渐", 80, 20);
            IEmployee fteE = new PartTimeEmployee("唐宛如", 60, 18);

            empList.AddEmployee(fteA);
            empList.AddEmployee(fteB);
            empList.AddEmployee(fteC);
            empList.AddEmployee(fteD);
            empList.AddEmployee(fteE);

            Department dept = AppConfigHelper.GetDeptInstance() as Department;
            if (dept != null)
            {
                empList.Accept(dept);
            }

            Console.ReadKey();
        }
    }

  其中,AppConfigHelper用于从配置文件中获得具体访问者实例,配置文件如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="DeptName" value="Manulife.ChengDu.DesignPattern.Visitor.HRDepartment, Manulife.ChengDu.DesignPattern.Visitor" />
  </appSettings>
</configuration>

  AppConfigHelper的具体代码如下:

    public class AppConfigHelper
    {
        public static string GetDeptName()
        {
            string factoryName = null;
            try
            {
                factoryName = System.Configuration.ConfigurationManager.AppSettings["DeptName"];
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return factoryName;
        }

        public static object GetDeptInstance()
        {
            string assemblyName = AppConfigHelper.GetDeptName();
            Type type = Type.GetType(assemblyName);

            var instance = Activator.CreateInstance(type);
            return instance;
        }
    }

  编译运行后的结果如下:

  

  如果需要更换具体访问者类,无须修改源代码,只需要修改一下配置文件。例如这里将访问者由人力资源部更改为财务部:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="DeptName" value="Manulife.ChengDu.DesignPattern.Visitor.FinanceDepartment, Manulife.ChengDu.DesignPattern.Visitor" />
  </appSettings>
</configuration>

  此时再次运行则会得到以下结果:

  

  可以看出,如果我们要在系统中新增访问者,那么无需修改源代码,只需新增一个新的具体访问者类即可,从这一点看,访问者模式符合开闭原则。

  但是,如果我们要在系统中新增具体元素,比如新增一个新的员工类型为“退休人员”,由于原系统并未提供相应的访问接口,因此必须对原有系统进行修改。所以,从新增新的元素来看,访问者模式违背了开闭原则

  因此,访问者模式与抽象工厂模式类似,对于开闭原则的支持具有“倾斜”性,可以方便地新增访问者,但是添加新的元素较为麻烦。

四、访问者模式总结

4.1 主要优点

  (1)增加新的访问操作十分方便,不痛不痒 => 符合开闭原则

  (2)将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中,类的职责更加清晰 => 符合单一职责原则

4.2 主要缺点

  (1)增加新的元素类很困难,需要在每一个访问者类中增加相应访问操作代码 => 违背了开闭原则

  (2)元素对象有时候必须暴露一些自己的内部操作和状态,否则无法供访问者访问 => 破坏了元素的封装性

4.3 应用场景

  (1)一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。=> 不同的类型可以有不同的访问操作

  (2)对象结构中对象对应的类很少改变 很少改变 很少改变(重要的事情说三遍),但经常需要在此对象结构上定义新的操作。

参考资料

  

  刘伟,《设计模式的艺术—软件开发人员内功修炼之道》

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

时间: 2025-01-04 09:11:28

设计模式的征途—16.访问者(Visitor)模式的相关文章

C++设计模式实现--访问者(Visitor)模式

一. 访问者模式 定义:表示一个作用于某对象结构中的各元素的操作.它你可以在不改变各元素的类的前提下定义作用于这些元素的新操作. 结构如下: 二. 举例 假设有一项科学实验,是用来对比两种种子在不同环境下的生长情况. 两种种子,一种是普通的种子(Seed_A),一种是太空运回的种子(Seed_B). 生长环境,分别是在多雨环境下(Rain_Status),阳光环境下(Sun_Status)等等. 结构如下: 代码如下: [cpp] view plaincopy //状态 class Status

设计模式之第16章-代理模式(Java实现)

设计模式之第16章-代理模式(Java实现) “现在朋友圈真是太让人蛋疼了啊.”“怎么说?”“一堆代理,各种卖东西的,看着好烦人.”“哎,删了呗.”“都是朋友,哪里好意思删啊.”“这倒也是...哎,迫于生计,没办法咯.还好我不玩.”“对了,你不就是代理的鼻祖么,身为代理模式,你作何感想.”“以代理之道还治代理之身啊.” 代理模式之自我介绍 最近出场率超级高,哦不,一直以来出场率都挺高的说的大名鼎鼎的模式,就是我-代理模式是也.有关我的定义如下:Provide a surrogate or pla

Java 实现访问者(Visitor)模式

interface Visitor { void visit(Gladiolus g); void visit(Chrysanthemum c); } // concrete visitor 名称访问 class StringVisitor implements Visitor { String s; public String toString() { return s; } public void visit(Gladiolus g) { s = "Gladiolus"; } pu

设计模式学习笔记--访问者(Visitor)模式

写在模式学习之前 什么是设计模式:在我们进行程序设计时,逐渐形成了一些典型问题和问题的解决方案,这就是软件模式:每一个模式描述了一个在我们程序设计中经常发生的问题,以及该问题的解决方案:当我们碰到模式所描述的问题,就可以直接用相应的解决方法去解决这个问题,这就是设计模式. 设计模式就是抽象出来的东西,它不是学出来的,是用出来的:或许你根本不知道任何模式,不考虑任何模式,却写着最优秀的代码,即使以"模式专家"的角度来看,都是最佳的设计,不得不说是"最佳的模式实践",这

设计模式的征途—文章目录索引

1.预备篇 UML类图10分钟快速入门 2.创建型模式 ① 设计模式的征途-01.单例(Singleton)模式 ② 设计模式的征途-02.简单工厂(Simple Factory)模式 ③ 设计模式的征途-03.工厂方法(Factory Method)模式 ④ 设计模式的征途-04.抽象工厂(Abstract Factory)模式 ⑤ 设计模式的征途-05.原型(Prototype)模式 ⑥ 设计模式的征途-06.建造者(Builder)模式 3.结构型模式 ① 设计模式的征途-07.适配器(A

设计模式(十七)访问者模式(Visitor)-行为型

访问者模式Visitor 访问者模式(Visitor Pattern)是GoF提出的23种设计模式中的一种,属于行为模式.据<大话设计模式>中说算是最复杂也是最难以理解的一种模式了. 定义(源于GoF<Design Pattern>):表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作. 从定义可以看出结构对象是使用访问者模式必备条件,而且这个结构对象必须存在遍历自身各个对象的方法.这便类似于Java语言当中的collection

Java设计模式(十一)访问者模式 中介者模式

(二十一)访问者模式 对已存在的类进行扩展,通常需要增加方法,但是如果需要的行为与现有的对象模型不一致,或者无法修改现有代码.在这种情况下,不更改类的层次结构,就无法扩展该层次结构的行为.如果运用了访问者模式,就可以支持开发人员扩展该类层次结构的行为. 和解释器模式一样,访问者模式通常是基于合成模式的. 访问者模式在不改变类层次结构的前提下,对该层次结构进行扩展. interface Visitor{ public void visit(VisiSubject sub); } interface

Visitor模式详解--设计模式(19)

Visitor模式来源:        在面向对象系统的开发和设计过程,经常会遇到一种情况就是需求变更(RequirementChanging),经常我们做好的一个设计.实现了一个系统原型,咱们的客户又会有了新的需求.我们又因此不得不去修改已有的设计,最常见就是解决方案就是给已经设计.实现好的类添加新的方法去实现客户新的需求,这样就陷入了设计变更的梦魇:不停地打补丁,其带来的后果就是设计根本就不可能封闭.编译永远都是整个系统代码.Visitor模式则提供了一种解决方案. Visitor模式作用:

NET设计模式 第二部分 行为型模式(16):命令模式(Command Pattern)

命令模式(Command Pattern) ——.NET设计模式系列之十七 TerryLee,2006年7月 概述 在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”.但在某些场合,比如要对行为进行“记录.撤销/重做.事务”等处理,这种无法抵御变化的紧耦合是不合适的.在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合[李建忠].这就是本文要说的Command模式. 意图 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行