设计模式学习之访问者模式(Visitor,行为型模式)(21)
参考:https://www.cnblogs.com/edisonchou/p/7247990.html
在患者就医时,医生会根据病情开具处方单,很多医院都会存在以下这个流程:划价人员拿到处方单之后根据药品名称和数量计算总价,而药房工作人员根据药品名称和数量准备药品,如下图所示。
在软件开发中,有时候也需要处理像处方单这样的集合对象结构,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不唯一,可能需要提供多种不同的处理方式。在设计模式中,有一种模式可以满足上述要求,其模式动机就是以不同的方式操作复杂对象结构,该模式就是访问者模式。
访问者模式(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的具体代码如下:
编译运行后的结果如下:
如果需要更换具体访问者类,无须修改源代码,只需要修改一下配置文件。例如这里将访问者由人力资源部更改为财务部:

<?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)对象结构中对象对应的类很少改变 很少改变 很少改变(重要的事情说三遍),但经常需要在此对象结构上定义新的操作。
设计模式学习之访问者模式(Visitor,行为型模式)(21)的更多相关文章
- 设计模式学习之单例模式(Singleton,创建型模式)(4)
假如程序中有一个Person类,我的需求就是需要在整个应用程序中只能new一个Person,而且这个Person实例在应用程序中进行共享,那么我们该如何实现呢? 第一步: 新建一个Person类,类中 ...
- 设计模式学习之观察者模式(Observer,行为型模式)(7)
1.观察者模式又叫做发布-订阅模式. 2.观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象.这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己. 3 ...
- 访问者模式 Visitor 行为型 设计模式(二十七)
访问者模式 Visitor <侠客行>是当代作家金庸创作的长篇武侠小说,新版电视剧<侠客行>中,开篇有一段独白: “茫茫海外,传说有座侠客岛,岛上赏善罚恶二使,每隔十年 ...
- 设计模式23:Visitor 访问者模式(行为型模式)
Visitor 访问者模式(行为型模式) 动机(Motivation)在软件构造过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的修改,将会给子类带来繁重的 ...
- 设计模式17:Iterator 迭代器模式(行为型模式)
Iterator 迭代器模式(行为型模式) 动机(Motivation) 在软件构建过程中,集合对象内部结构常常变化各异.但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码可以透 ...
- 设计模式16:Mediator 中介者模式(行为型模式)
Mediator 中介者模式(行为型模式) 依赖关系的转化 动机(Motivation) 在软件构建过程中,经常出现多个对象互相关联交互的情况,对象之间经常会维持一种复杂的应用关系,如果遇到一些需求的 ...
- 设计模式22:Strategy 策略模式(行为型模式)
Strategy 策略模式(行为型模式) 动机(Motivation) 在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂:而且有时候支持 ...
- 设计模式21:State 状态模式(行为型模式)
State 状态模式(行为型模式) 动机(Motivation) 在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其支持的行为和读写状态的行为就可能完全不同. ...
- 设计模式20:Memento 备忘录模式(行为型模式)
Memento 备忘录模式(行为型模式) 对象状态的回溯 对象状态的变化无端,如何回溯.恢复对象在某个点的状态? 动机(Motivation) 在软件构建过程中,某些对象的状态在转换过程中,可能由于某 ...
- 设计模式19:Chain Of Responsibility 职责链模式(行为型模式)
Chain Of Responsibility 职责链模式(行为型模式) 请求的发送者与接受者 某些对象请求的接受者可能有多种多样,变化无常…… 动机(Motivation) 在软件构建过程中,一个请 ...
随机推荐
- red()、redinle()、redlines()三者之间的关系
# 关于read()方法: # 1.读取整个文件,将文件内容放到一个字符串变量中 # 2.如果文件大于可用内存,不可能使用这种处理 file_object = open("a.txt&quo ...
- [模板] 二分图博弈 && BZOJ2463:[中山市选2009]谁能赢呢?
二分图博弈 from BZOJ 1443 游戏(二分图博弈) - free-loop - 博客园 定义 1.博弈者人数为两人,双方轮流进行决策. 2.博弈状态(对应点)可分为两类(状态空间可分为两个集 ...
- 4.6 并发编程/IO模型
并发编程/IO模型 背景概念 IO模型概念 IO模型分类 阻塞IO (blocking IO) 特点: 两个阶段(等待数据和拷贝数据两个阶段)都被block 设置 server.setsockopt ...
- 清北学堂Day3
卷积公式(Dirichlet卷积) 这个式子看上去就很变态,那么他是什么意思呢: 就是说 函数f(x)和g(x)对于n的卷积等于n的每一个因子d在f(x)上的值乘上d/n在g(x)上的值的和 例:设g ...
- Java多线程_复习(更新中!!)
java多线程的常见例子 一.相关知识: Java多线程程序设计到的知识: (一)对同一个数量进行操作 (二)对同一个对象进行操作 (三)回调方法使用 (四)线程同步,死锁问题 (五)线程通信 等等 ...
- 无连接运输的UDP、可靠数据传输原理、面向连接运输的TCP
由[RFC 768]定义的UDP只是做了运输协议能够做的最少工作.除了复用/分解功能极少量的差错检测外,它几乎没有对IP增加别的东西.如果应用程序开发人员选择UDP而不是TCP,则该应用程序差不多就是 ...
- 在桌面右键创建html,css,js文件
1.在开始里面输入regedit,进入注册表编辑器. 2.打开HKEY_CLASSES_ROOT项. 3.打开.html/.css/.js项. 4.右键新建项,起名ShellNew. 5.新建字符串值 ...
- TCP/IP教程
一.TCP/IP 简介 TCP/IP 是用于因特网的通信协议. 通信协议是对计算机必须遵守的规则的描述,只有遵守这些规则,计算机之间才能进行通信. 什么是 TCP/IP? TCP/IP 是供已连接因特 ...
- JavaScript复习
一.常用对话框 1.alert(""):警告对话框,作用是弹出一个警告对话框 2.confirm(""):确定对话框,弹出一个带确定和取消按钮的对话框——确定返 ...
- 关于sniff函数的一个小坑
最近在用scapy模块写一个关于WiFi的脚本时用到sniff函数,其中遇到了一个小坑,记录如下: sniff函数是在指定网卡上每次嗅探到一个数据包后然后将它传给prn指定的函数.