现在有这样一个需求,我要把公司中的所有人员信息都打印汇报上去,每一个员工都有这些信息:名字、性别、薪水,我们来看类图:
这个类图还是比较简单的,使用了一个模版方法模式,把所要的信息都打印出来,我们先来看一下抽象类:
/**
* @description 在一个单位里谁都是员工,甭管你是部门经理还是小兵
*/
public abstract class Employee {
// 0代表男性
public static final int MALE = 0;
// 1代表女性
public static final int FEMALE = 1;
private String name;
private int salary;
private int sex;
public final void report() {
System.out.println("姓名: " + name + "\t性别: " + (sex == FEMALE ? "女" : "男") + "\t薪水: " + salary + "\t" + getOtherInfo());
}
protected abstract String getOtherInfo();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
}
再看小兵的实现类:
/**
* @description 普通员工,也就是最小的小兵
*/
public class CommonEmployee extends Employee {
// 工作内容
private String job;
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
protected String getOtherInfo() {
return "工作: " +job + "\t";
}
}
再来看领导阶层:
/**
* @description 经理级人物
*/
public class Manager extends Employee {
// 业绩
private String performance;
public String getPerformance() {
return performance;
}
public void setPerformance(String performance) {
this.performance = performance;
}
@Override
protected String getOtherInfo() {
return "业绩: " + performance + "\t";
}
}
然后我们来看一下我们的invoker类:
public class Client {
public static void main(String[] args) {
for (Employee employee : mockEmployee()) {
employee.report();
}
}
public static List<Employee> mockEmployee() {
List<Employee> empList = new ArrayList<>();
CommonEmployee zhangSan = new CommonEmployee();
zhangSan.setJob("Java开发");
zhangSan.setName("张三");
zhangSan.setSalary(1800);
zhangSan.setSex(Employee.MALE);
CommonEmployee liSi = new CommonEmployee();
liSi.setJob("前端开发");
liSi.setName("李四");
liSi.setSalary(1900);
liSi.setSex(Employee.FEMALE);
Manager wangWu = new Manager();
wangWu.setPerformance("基本都是负值");
wangWu.setName("王五");
wangWu.setSalary(18750);
wangWu.setSex(Employee.FEMALE);
empList.add(zhangSan);
empList.add(liSi);
empList.add(wangWu);
return empList;
}
}
程序运行结果:
姓名: 张三 性别: 男 薪水: 1800 工作: Java开发
姓名: 李四 性别: 女 薪水: 1900 工作: 前端开发
姓名: 王五 性别: 女 薪水: 18750 业绩: 基本都是负值
接下来改进我们的代码:
每个普通员工类和经理类都一个report()
方法,它们要实现的内容不相同,而且还有可能会发生变动,那我们就让其他类来实现这个 report()
方法:
看代码实现:
public interface IVisitor {
// 定义可以访问普通员工
void visit(CommonEmployee commonEmployee);
// 定义可以访问部门经理
void visit(Manager manager);
}
public class Visitor implements IVisitor {
// 组装基本信息
private String getBasicInfo(Employee employee) {
return "姓名: " + employee.getName() + "\t性别: " +
(employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t薪水: " +
employee.getSalary() + "\t";
}
// 组装部门经理的信息
private String getManagerInfo(Manager manager) {
return getBasicInfo(manager) + "业绩: " + manager.getPerformance() + "\t";
}
// 组装普通员工的信息
private String getCommonEmployeeInfo(CommonEmployee commonEmployee) {
return getBasicInfo(commonEmployee) + "工作: " + commonEmployee.getJob() + "\t";
}
@Override
public void visit(CommonEmployee commonEmployee) {
System.out.println(getCommonEmployeeInfo(commonEmployee));
}
@Override
public void visit(Manager manager) {
System.out.println(getManagerInfo(manager));
}
}
public abstract class Employee {
// 0代表男性
public static final int MALE = 0;
// 1代表女性
public static final int FEMALE = 1;
private String name;
private int salary;
private int sex;
// 一个访问者过来访问
public abstract void accept(IVisitor visitor);
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
}
public class CommonEmployee extends Employee {
// 工作内容
private String job;
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public class Manager extends Employee {
// 业绩
private String performance;
public String getPerformance() {
return performance;
}
public void setPerformance(String performance) {
this.performance = performance;
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public class Client {
public static void main(String[] args) {
for (Employee employee : mockEmployee()) {
employee.accept(new Visitor());
}
}
public static List<Employee> mockEmployee() {
List<Employee> empList = new ArrayList<>();
CommonEmployee zhangSan = new CommonEmployee();
zhangSan.setJob("Java开发");
zhangSan.setName("张三");
zhangSan.setSalary(1800);
zhangSan.setSex(Employee.MALE);
CommonEmployee liSi = new CommonEmployee();
liSi.setJob("前端开发");
liSi.setName("李四");
liSi.setSalary(1900);
liSi.setSex(Employee.FEMALE);
Manager wangWu = new Manager();
wangWu.setPerformance("基本都是负值");
wangWu.setName("王五");
wangWu.setSalary(18750);
wangWu.setSex(Employee.FEMALE);
empList.add(zhangSan);
empList.add(liSi);
empList.add(wangWu);
return empList;
}
}
运行结果也完全相同,那回过头我们来看看这个程序是怎么实现的:
- 首先通过循环遍历所有元素;
- 其次,每个员工对象都定义了一个访问者;
- 再其次,员工对象把自己做为一个参数调用访问者
visit()
方法; - 然后,访问者调用自己内部的计算逻辑,计算出相应的数据和表格元素;
- 最后,访问者打印出报表和数据;
这样一来,如果打印的信息格式发生变化了,我只要修改Visitor
的实现或者再产生一个Visitor
就可以产生一个新的报表格式,而其他的类都不用修改。
以上讲的就是访问者模式,这个模式的通用类图如下:
看了这个通用类图,大家可能要犯迷糊了,这里怎么有一个ObjectStruture
这个类呢?你刚刚举得例子就没有呢?真没有吗?我们不是定义了一个List
了吗?这就是一个ObjectStruture
,我们来看这几个角色的职责:
- 抽象访问者(Visitor):抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是
visit()
方法的参数定义哪些对象是可以被访问的; - 具体访问者(ConcreteVisitor):访问者访问到一个类后该怎么干,要做什么事情;
- 抽象元素(Element):接口或者抽象类,声明接受那一类型的访问者访问,程序上是通过
accept()
方法中的参数来定义; - 具体元素:(ConcreteElement):实现 accept 方法,通常是 visitor.visit(this),基本上都形成了一
个套路了; - 结构对象(ObjectStruture):容纳多个不同类、不同接口的容器,比如
List
、Set
、Map
等,在项目中,一般很少抽象出来这个角色;
接下来我们来思考一下,访问者可以用在什么地方。在这种地方你一定要考虑到使用访问者模式:业务规则要求遍历多个不同的对象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据,(当然了,你使用instanceof
的话,能访问所有的数据,这个不争论),而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不同的操作。访问者模式还有一个用途,就是充当拦截器(Interceptor)角色,这个我们在后边来讲。
访问者模式有哪些优点呢?
- 首先是符合单一职责原则,具体元素角色也就是
Employee
这个类的两个子类负责数据的加载,而Visitor
类则负责报表的展现,两个不同的职责非常明确的分离开来,各自演绎而变化; - 其次,由于职责分开,继续增加对数据的操作是非常快捷的,例如现在要增加一个给最大老板的一份报表,这份报表格式又有所不同,容易处理吧,直接在
Visitor
中增加一个方法,传递过来数据后进行整理打印; - 最后,数据汇总,就以刚刚我们说的
Employee
的例子,如果我现在要统计所有员工的工资之和,怎么计算?把所有人的工资for
循环加一遍?是个办法,那我再提个问题,员工工资*1.2,部门经理工资*1.4,总经理*1.8,然后把这些工资加起来,你怎么处理?使用for
循环,然后使用instanceof
来判断是员工还是经理?可以解决,但不是个好办法,好办法是通过访问者模式来实现,把数据扔给访问者,由访问者来进行统计计算。
访问者模式的缺点也很明显:
访问者要访问一个类就必然要求这个类公布一些方法,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的;
还有一个缺点就是,具体角色的增加删除修改都是比较苦难的,就上面那个例子,你想想,你要是想增加一个成员变量,比如年龄
age
,Visitor
就需要修改,如果Visitor
是一个还好说,多个呢?业务逻辑再复杂点呢?
访问者模式是有缺点的,是事物都有缺点,但是这仍然掩盖不了它的光芒,访问者模式结合其他模式比如模版方法模式、状态模式、解释器模式、代理模式等就会非常强大,这个我们放在模式混编中来讲解。
在这里我提出三个扩展的功能共大家参考:
- 统计功能。在访问者模式中的使用中我也提到访问者的统计功能,汇总和报表是金融类企业非常常用的功能,基本上都是一堆的计算公式,然后出一个报表,很多项目是采用了数据库的存储过程来实现,这个我不是很推荐,除非海量数据处理,一个晚上要上亿、几十亿条的数据跑批处理,这个除了存储过程来处理没有其他办法的,你要是用应用服务器来处理,连接数据库的网络就是处于100%用状态,一个晚上也未必跑得完这批数据!除了这种海量数据外,我建议数据统计和报表的批处理通过访问者模式来处理会比较简单。好,那我们来统计一下公司人员的工资,先看类图:
看代码实现:
public interface IVisitor {
// 定义可以访问普通员工
void visit(CommonEmployee commonEmployee);
// 定义可以访问部门经理
void visit(Manager manager);
// 统计所有员工工资总和
int getTotalSalary();
}
public class Visitor implements IVisitor {
// 部门经理的工资系数是5
private static final int MANAGER_COEFFICIENT = 5;
// 员工的工资系数是2
private static final int COMMON_EMPLOYEE_COEFFICIENT = 2;
// 普通员工的工资总和
private int commonTotalSalary = 0;
// 部门经理的工资总和
private int managerTotalSalary = 0;
// 组装基本信息
private String getBasicInfo(Employee employee) {
return "姓名: " + employee.getName() + "\t性别: " +
(employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t薪水: " +
employee.getSalary() + "\t";
}
// 组装部门经理的信息
private String getManagerInfo(Manager manager) {
return getBasicInfo(manager) + "业绩: " + manager.getPerformance() + "\t";
}
// 组装普通员工的信息
private String getCommonEmployeeInfo(CommonEmployee commonEmployee) {
return getBasicInfo(commonEmployee) + "工作: " + commonEmployee.getJob() + "\t";
}
// 计算部门经理的工资总和
private void calManagerSalary(int salary) {
this.managerTotalSalary += salary * MANAGER_COEFFICIENT;
}
// 计算普通员工的工资总和
private void calCommonSalary(int salary) {
this.commonTotalSalary += salary * COMMON_EMPLOYEE_COEFFICIENT;
}
@Override
public int getTotalSalary() {
return managerTotalSalary + commonTotalSalary;
}
@Override
public void visit(CommonEmployee commonEmployee) {
System.out.println(getCommonEmployeeInfo(commonEmployee));
// 计算普通员工的薪水总和
calCommonSalary(commonEmployee.getSalary());
}
@Override
public void visit(Manager manager) {
System.out.println(getManagerInfo(manager));
// 计算部门经理的薪水总和
calManagerSalary(manager.getSalary());
}
}
public class Client {
public static void main(String[] args) {
IVisitor visitor = new Visitor();
for (Employee employee : mockEmployee()) {
employee.accept(visitor);
}
System.out.println("工资总额: " + visitor.getTotalSalary());
}
public static List<Employee> mockEmployee() {
List<Employee> empList = new ArrayList<>();
CommonEmployee zhangSan = new CommonEmployee();
zhangSan.setJob("Java开发");
zhangSan.setName("张三");
zhangSan.setSalary(1800);
zhangSan.setSex(Employee.MALE);
CommonEmployee liSi = new CommonEmployee();
liSi.setJob("前端开发");
liSi.setName("李四");
liSi.setSalary(1900);
liSi.setSex(Employee.FEMALE);
Manager wangWu = new Manager();
wangWu.setPerformance("基本都是负值");
wangWu.setName("王五");
wangWu.setSalary(18750);
wangWu.setSex(Employee.FEMALE);
empList.add(zhangSan);
empList.add(liSi);
empList.add(wangWu);
return empList;
}
}
// Employee及其两个子类是没有任何变化的
-
多个访问者
在实际的项目中,一个对象,多个访问者的情况非常多。其实我们上面例子就应该是两个访问者,为什么呢?报表分两种,一种是展示表,通过数据库查询,把结果展示出来,这个就类似于我们的那个列表;第二种是汇总表,这个是需要通过模型或者公式计算出来的,一般都是批处理结果,这个类似于我们计算工资总额,这两种报表格式是对同一堆数据的两种处理方式,从程序上看,一个类就有个不同的访问者了,那我们修改一下类图:
代码如下:
public interface IVisitor {
// 定义可以访问普通员工
void visit(CommonEmployee commonEmployee);
// 定义可以访问部门经理
void visit(Manager manager);
}
public interface IShowVisitor extends IVisitor {
// 展示报表
void report();
}
public interface ITotalVisitor extends IVisitor {
// 统计所有员工工资总和
void totalSalary();
}
public class ShowVisitor implements IShowVisitor {
private String info = "";
// 组装基本信息
private String getBasicInfo(Employee employee) {
return "姓名: " + employee.getName() + "\t性别: " +
(employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t薪水: " +
employee.getSalary() + "\t";
}
@Override
public void report() {
System.out.println(info);
}
@Override
public void visit(CommonEmployee commonEmployee) {
this.info += getBasicInfo(commonEmployee) + "工作: " + commonEmployee.getJob() + "\t\n";
}
@Override
public void visit(Manager manager) {
this.info += getBasicInfo(manager) + "业绩: " + manager.getPerformance() + "\t\n";
}
}
public class TotalVisitor implements ITotalVisitor {
// 部门经理的工资系数是5
private static final int MANAGER_COEFFICIENT = 5;
// 员工的工资系数是2
private static final int COMMON_EMPLOYEE_COEFFICIENT = 2;
// 普通员工的工资总和
private int commonTotalSalary = 0;
// 部门经理的工资总和
private int managerTotalSalary = 0;
// 计算部门经理的工资总和
private void calManagerSalary(int salary) {
this.managerTotalSalary += salary * MANAGER_COEFFICIENT;
}
// 计算普通员工的工资总和
private void calCommonSalary(int salary) {
this.commonTotalSalary += salary * COMMON_EMPLOYEE_COEFFICIENT;
}
@Override
public void totalSalary() {
System.out.println("总工资: " + (managerTotalSalary + commonTotalSalary));
}
@Override
public void visit(CommonEmployee commonEmployee) {
// 计算普通员工的薪水总和
calCommonSalary(commonEmployee.getSalary());
}
@Override
public void visit(Manager manager) {
// 计算部门经理的薪水总和
calManagerSalary(manager.getSalary());
}
}
public class Client {
public static void main(String[] args) {
IShowVisitor showVisitor = new ShowVisitor();
ITotalVisitor totalVisitor = new TotalVisitor();
for (Employee employee : mockEmployee()) {
employee.accept(showVisitor);
employee.accept(totalVisitor);
}
showVisitor.report();
totalVisitor.totalSalary();
}
public static List<Employee> mockEmployee() {
......
}
}
// Employee及其两个子类是没有任何变化的
-
拦截器
拦截器的核心作用是“围墙”作用,拦截器对被拦截的对象进行检查,符合规则的对象则开门放进去,继续执行下一个逻辑,不符合规则的则弹回(其实这也是过滤器的作用);拦截器还有一个作用是修改数据,对于符合规则数据可以进行修改,以便继续后序的逻辑。具备了这两个功能,拦截器的雏形就有了,访问者模式就可以实现简单的拦截器角色,我们来看类图:
着是不是和访问者模式的通用类图很类似?两个accept()
方法,其中参数为List
类型的则实现了拦截器栈的作用,DynamicProxy
类使用了动态代理和反射模式。拦截器实现起来也不复杂,今天就不实现了,这个作为作业,请大家自己来实现。计划在混编模式中一起探讨。
本文原书:
《您的设计模式》 作者:CBF4LIFE