操作复杂对象结构——访问者模式(三)

26.3 完整解决方案

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

操作复杂对象结构——访问者模式(三) - 图1

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

  1. import java.util.*;
  2. //员工类:抽象元素类
  3. interface Employee
  4. {
  5. public void accept(Department handler); //接受一个抽象访问者访问
  6. }
  7. //全职员工类:具体元素类
  8. class FulltimeEmployee implements Employee
  9. {
  10. private String name;
  11. private double weeklyWage;
  12. private int workTime;
  13. public FulltimeEmployee(String name,double weeklyWage,int workTime)
  14. {
  15. this.name = name;
  16. this.weeklyWage = weeklyWage;
  17. this.workTime = workTime;
  18. }
  19. public void setName(String name)
  20. {
  21. this.name = name;
  22. }
  23. public void setWeeklyWage(double weeklyWage)
  24. {
  25. this.weeklyWage = weeklyWage;
  26. }
  27. public void setWorkTime(int workTime)
  28. {
  29. this.workTime = workTime;
  30. }
  31. public String getName()
  32. {
  33. return (this.name);
  34. }
  35. public double getWeeklyWage()
  36. {
  37. return (this.weeklyWage);
  38. }
  39. public int getWorkTime()
  40. {
  41. return (this.workTime);
  42. }
  43. public void accept(Department handler)
  44. {
  45. handler.visit(this); //调用访问者的访问方法
  46. }
  47. }
  48. //兼职员工类:具体元素类
  49. class ParttimeEmployee implements Employee
  50. {
  51. private String name;
  52. private double hourWage;
  53. private int workTime;
  54. public ParttimeEmployee(String name,double hourWage,int workTime)
  55. {
  56. this.name = name;
  57. this.hourWage = hourWage;
  58. this.workTime = workTime;
  59. }
  60. public void setName(String name)
  61. {
  62. this.name = name;
  63. }
  64. public void setHourWage(double hourWage)
  65. {
  66. this.hourWage = hourWage;
  67. }
  68. public void setWorkTime(int workTime)
  69. {
  70. this.workTime = workTime;
  71. }
  72. public String getName()
  73. {
  74. return (this.name);
  75. }
  76. public double getHourWage()
  77. {
  78. return (this.hourWage);
  79. }
  80. public int getWorkTime()
  81. {
  82. return (this.workTime);
  83. }
  84. public void accept(Department handler)
  85. {
  86. handler.visit(this); //调用访问者的访问方法
  87. }
  88. }
  89. //部门类:抽象访问者类
  90. abstract class Department
  91. {
  92. //声明一组重载的访问方法,用于访问不同类型的具体元素
  93. public abstract void visit(FulltimeEmployee employee);
  94. public abstract void visit(ParttimeEmployee employee);
  95. }
  96. //财务部类:具体访问者类
  97. class FADepartment extends Department
  98. {
  99. //实现财务部对全职员工的访问
  100. public void visit(FulltimeEmployee employee)
  101. {
  102. int workTime = employee.getWorkTime();
  103. double weekWage = employee.getWeeklyWage();
  104. if(workTime > 40)
  105. {
  106. weekWage = weekWage + (workTime - 40) * 100;
  107. }
  108. else if(workTime < 40)
  109. {
  110. weekWage = weekWage - (40 - workTime) * 80;
  111. if(weekWage < 0)
  112. {
  113. weekWage = 0;
  114. }
  115. }
  116. System.out.println("正式员工" + employee.getName() + "实际工资为:" + weekWage + "元。");
  117. }
  118. //实现财务部对兼职员工的访问
  119. public void visit(ParttimeEmployee employee)
  120. {
  121. int workTime = employee.getWorkTime();
  122. double hourWage = employee.getHourWage();
  123. System.out.println("临时工" + employee.getName() + "实际工资为:" + workTime * hourWage + "元。");
  124. }
  125. }
  126. //人力资源部类:具体访问者类
  127. class HRDepartment extends Department
  128. {
  129. //实现人力资源部对全职员工的访问
  130. public void visit(FulltimeEmployee employee)
  131. {
  132. int workTime = employee.getWorkTime();
  133. System.out.println("正式员工" + employee.getName() + "实际工作时间为:" + workTime + "小时。");
  134. if(workTime > 40)
  135. {
  136. System.out.println("正式员工" + employee.getName() + "加班时间为:" + (workTime - 40) + "小时。");
  137. }
  138. else if(workTime < 40)
  139. {
  140. System.out.println("正式员工" + employee.getName() + "请假时间为:" + (40 - workTime) + "小时。");
  141. }
  142. }
  143. //实现人力资源部对兼职员工的访问
  144. public void visit(ParttimeEmployee employee)
  145. {
  146. int workTime = employee.getWorkTime();
  147. System.out.println("临时工" + employee.getName() + "实际工作时间为:" + workTime + "小时。");
  148. }
  149. }
  150. //员工列表类:对象结构
  151. class EmployeeList
  152. {
  153. //定义一个集合用于存储员工对象
  154. private ArrayList<Employee> list = new ArrayList<Employee>();
  155. public void addEmployee(Employee employee)
  156. {
  157. list.add(employee);
  158. }
  159. //遍历访问员工集合中的每一个员工对象
  160. public void accept(Department handler)
  161. {
  162. for(Object obj : list)
  163. {
  164. ((Employee)obj).accept(handler);
  165. }
  166. }
  167. }

为了提高系统的灵活性和可扩展性,我们将具体访问者类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示:

  1. import javax.xml.parsers.*;
  2. import org.w3c.dom.*;
  3. import org.xml.sax.SAXException;
  4. import java.io.*;
  5. class XMLUtil
  6. {
  7. //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
  8. public static Object getBean()
  9. {
  10. try
  11. {
  12. //创建文档对象
  13. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  14. DocumentBuilder builder = dFactory.newDocumentBuilder();
  15. Document doc;
  16. doc = builder.parse(new File("config.xml"));
  17. //获取包含类名的文本节点
  18. NodeList nl = doc.getElementsByTagName("className");
  19. Node classNode=nl.item(0).getFirstChild();
  20. String cName=classNode.getNodeValue();
  21. //通过类名生成实例对象并将其返回
  22. Class c=Class.forName(cName);
  23. Object obj=c.newInstance();
  24. return obj;
  25. }
  26. catch(Exception e)
  27. {
  28. e.printStackTrace();
  29. return null;
  30. }
  31. }
  32. }

配置文件config.xml中存储了具体访问者类的类名,代码如下所示:

  1. <?xml version="1.0"?>
  2. <config>
  3. <className>FADepartment</className>
  4. </config>

编写如下客户端测试代码:

  1. class Client
  2. {
  3. public static void main(String args[])
  4. {
  5. EmployeeList list = new EmployeeList();
  6. Employee fte1,fte2,fte3,pte1,pte2;
  7. fte1 = new FulltimeEmployee("张无忌",3200.00,45);
  8. fte2 = new FulltimeEmployee("杨过",2000.00,40);
  9. fte3 = new FulltimeEmployee("段誉",2400.00,38);
  10. pte1 = new ParttimeEmployee("洪七公",80.00,20);
  11. pte2 = new ParttimeEmployee("郭靖",60.00,18);
  12. list.addEmployee(fte1);
  13. list.addEmployee(fte2);
  14. list.addEmployee(fte3);
  15. list.addEmployee(pte1);
  16. list.addEmployee(pte2);
  17. Department dep;
  18. dep = (Department)XMLUtil.getBean();
  19. list.accept(dep);
  20. }
  21. }

编译并运行程序,输出结果如下:

  1. 正式员工张无忌实际工资为:3700.0元。
  2. 正式员工杨过实际工资为:2000.0元。
  3. 正式员工段誉实际工资为:2240.0元。
  4. 临时工洪七公实际工资为:1600.0元。
  5. 临时工郭靖实际工资为:1080.0元。

如果需要更换具体访问者类,无须修改源代码,只需修改配置文件,例如将访问者类由财务部改为人力资源部,只需将存储在配置文件中的具体访问者类FADepartment改为HRDepartment,如下代码所示:

  1. <?xml version="1.0"?>
  2. <config>
  3. <className>HRDepartment</className>
  4. </config>

重新运行客户端程序,输出结果如下:

  1. 正式员工张无忌实际工作时间为:45小时。
  2. 正式员工张无忌加班时间为:5小时。
  3. 正式员工杨过实际工作时间为:40小时。
  4. 正式员工段誉实际工作时间为:38小时。
  5. 正式员工段誉请假时间为:2小时。
  6. 临时工洪七公实际工作时间为:20小时。
  7. 临时工郭靖实际工作时间为:18小时。

如果要在系统中增加一种新的访问者,无须修改源代码,只要增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合“开闭原则”。

如果要在系统中增加一种新的具体元素,例如增加一种新的员工类型为“退休人员”,由于原有系统并未提供相应的访问接口(在抽象访问者中没有声明任何访问“退休人员”的方法),因此必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了“开闭原则”。

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