首页 > 技术文章 > 行为型模式之操作复杂对象结构(访问者模式)

47Gamer 2020-10-19 18:46 原文

完整解决方案

Sunny软件公司开发人员使用访问者模式对OA系统中员工数据汇总模块进行重构,使得系统可以很方便地增加新类型的访问者,更加符合“单一职责原则”和“开闭原则”,重构后的基本结构如图所示:

在图中,FADepartment表示财务部,HRDepartment表示人力资源部,它们充当具体访问者角色,其抽象父类Department充当抽象访问者角色;EmployeeList充当对象结构,用于存储员工列表;FulltimeEmployee表示正式员工,ParttimeEmployee表示临时工,它们充当具体元素角色,其父接口Employee充当抽象元素角色。完整代码如下所示:

 //员工类:抽象元素类
interface Employee {
        public void accept(Department handler); //接受一个抽象访问者访问

    }
//全职员工类:具体元素类
class FulltimeEmployee implements Employee {
        private String name;
        private double weeklyWage;
        private int workTime;

        public FulltimeEmployee(String name, double weeklyWage, int workTime) {
            this.name = name;
            this.weeklyWage = weeklyWage;
            this.workTime = workTime;
        }

        //set、get方法省略

        public void accept(Department handler) {
            handler.visit(this); //调用访问者的访问方法
        }
}
//兼职员工类:具体元素类
class ParttimeEmployee implements Employee {
        private String name;
        private double hourWage;
        private int workTime;

        public ParttimeEmployee(String name, double hourWage, int workTime) {
            this.name = name;
            this.hourWage = hourWage;
            this.workTime = workTime;
        }

        //get、set方法省略

        public void accept(Department handler) {
            handler.visit(this); //调用访问者的访问方法
        }
}
//部门类:抽象访问者类
abstract class Department {
        //声明一组重载的访问方法,用于访问不同类型的具体元素
        public abstract void visit(FulltimeEmployee employee);

        public abstract void visit(ParttimeEmployee employee);
    }
//财务部类:具体访问者类
class FADepartment extends Department {
        //实现财务部对全职员工的访问 
        public void visit(FulltimeEmployee employee) {
            int workTime = employee.getWorkTime();
            double weekWage = employee.getWeeklyWage();
            if (workTime > 40) {
                weekWage = weekWage + (workTime - 40) * 100;
            } else if (workTime < 40) {
                weekWage = weekWage - (40 - workTime) * 80;
                if (weekWage < 0) {
                    weekWage = 0;
                }
            }
            System.out.println("正式员工" + employee.getName() + "实际工资为:" + weekWage + "元。");
        }

        //实现财务部对兼职员工的访问 
        public void visit(ParttimeEmployee employee) {
            int workTime = employee.getWorkTime();
            double hourWage = employee.getHourWage();
            System.out.println("临时工" + employee.getName() + "实际工资为:" + workTime * hourWage + "元。");
        }
    }
//人力资源部类:具体访问者类 
class HRDepartment extends Department {
        // 实现人力资源部对全职员工的访问 
        public void visit(FulltimeEmployee employee) {
            int workTime = employee.getWorkTime();
            System.out.println("正式员工" + employee.getName() + "实际工作时间为:" + workTime + "小时。");
            if (workTime > 40) {
                System.out.println("正式员工" + employee.getName() + "加班时间为:" + (workTime - 40) + "小时。");
            } else if (workTime < 40) {
                System.out.println("正式员工" + employee.getName() + "请假时间为:" + (40 - workTime) + "小时。");
            }
        }

        // 实现人力资源部对兼职员工的访问 
        public void visit(ParttimeEmployee employee) {
            int workTime = employee.getWorkTime();
            System.out.println("临时工" + employee.getName() + "实际工作时间为:" + workTime + "小时。");
        }
    }
//员工列表类:对象结构 
class EmployeeList {
        //定义一个集合用于存储员工对象 
        private ArrayList<Employee> list = new ArrayList<Employee>();

        public void addEmployee(Employee employee) {
            list.add(employee);
        }

        //遍历访问员工集合中的每一个员工对象 
        public void accept(Department handler) {
            for (Object obj : list) {
                ((Employee) obj).accept(handler);
            }
        }
    }
为了提高系统的灵活性和可扩展性,我们将具体访问者类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示:
    import javax.xml.parsers .*; 
    import org.w3c.dom .*; 
    import org.xml.sax.SAXException; 
    import java.io .*;

    class XMLUtil {
        //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
        public static Object getBean() {
            try {
                // 创建文档对象
                DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = dFactory.newDocumentBuilder();
                Document doc;
                doc = builder.parse(new File("config.xml"));
                //获取包含类名的文本节点 
                NodeList nl = doc.getElementsByTagName("className");
                Node classNode = nl.item(0).getFirstChild();
                String cName = classNode.getNodeValue();
                //通过类名生成实例对象并将其返回 
                Class c = Class.forName(cName);
                Object obj = c.newInstance();
                return obj;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
配置文件config.xml中存储了具体访问者类的类名,代码如下所示:
<?xml version="1.0"?> 
<config> 
    <className>FADepartment</className> 
</config>
编写如下客户端测试代码:
class Client {
        public static void main(String args[]) {
            EmployeeList list = new EmployeeList();
            Employee fte1, fte2, fte3, pte1, pte2;
            fte1 = new FulltimeEmployee("张无忌", 3200.00, 45);
            fte2 = new FulltimeEmployee("杨过", 2000.00, 40);
            fte3 = new FulltimeEmployee("段誉", 2400.00, 38);
            pte1 = new ParttimeEmployee("洪七公", 80.00, 20);
            pte2 = new ParttimeEmployee("郭靖", 60.00, 18);
            list.addEmployee(fte1);
            list.addEmployee(fte2);
            list.addEmployee(fte3);
            list.addEmployee(pte1);
            list.addEmployee(pte2);
            Department dep;
            dep = (Department) XMLUtil.getBean();
            list.accept(dep);
        }
    }
编译并运行程序,输出结果如下:
正式员工张无忌实际工资为:3700.0元。 
正式员工杨过实际工资为:2000.0元。 
正式员工段誉实际工资为:2240.0元。 
临时工洪七公实际工资为:1600.0元。 
临时工郭靖实际工资为:1080.0元。
如果需要更换具体访问者类,无须修改源代码,只需修改配置文件,例如将访问者类由财务部改为人力资源部,只需将存储在配置文件中的具体访问者类FADepartment改为HRDepartment,如下代码所示:
<?xml version="1.0"?> 
<config> 
    <className>HRDepartment</className> 
</config>
重新运行客户端程序,输出结果如下:
正式员工张无忌实际工作时间为:45小时。 
正式员工张无忌加班时间为:5小时。 
正式员工杨过实际工作时间为:40小时。 
正式员工段誉实际工作时间为:38小时。 
正式员工段誉请假时间为:2小时。 
临时工洪七公实际工作时间为:20小时。 
临时工郭靖实际工作时间为:18小时。
如果要在系统中增加一种新的访问者,无须修改源代码,只要增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合“开闭原则”。
如果要在系统中增加一种新的具体元素,例如增加一种新的员工类型为“退休人员”,由于原有系统并未提供相应的访问接口(在抽象访问者中没有声明任何访问“退休人员”的方法),因此必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。
从增加新的元素的角度来看,访问者模式违背了“开闭原则”。
综上所述,访问者模式与抽象工厂模式类似,对“开闭原则”的支持具有倾斜性,可以很方便地添加新的访问者,但是添加新的元素较为麻烦。
 

访问者模式总结

由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,因此在实际应用中使用频率不是特别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在XML文档解析、编译器的设计、复杂集合对象的处理等领域访问者模式得到了一定的应用。

 

访问者模式的主要优点如下:

(1) 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。

(2) 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。

(3) 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。

 

访问者模式的主要缺点如下:

(1) 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。

(2) 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。

 

在以下情况下可以考虑使用访问者模式:

(1) 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。

(2) 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操  “污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。

对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。

推荐阅读