大多数情况下你不需要访问者模式,但当一旦需要访问者模式时,那就是真的需要它了,这是设计模式创始人的原话。可以看出应用场景比较少,但需要它的时候是不可或缺的,这篇文章就开始学习最后一个设计模式——访问者模式。
一、概念理解
访问者模式概念:封装作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
通俗的解释就是,系统中有一些固定结构的对象(元素),在其内部提供一个accept()方法用来接受访问者对象的访问,不同的访问者对同一元素的访问内容不同,所以使得相同的元素可以产生不同的元素结果。
比如在一个人事管理系统中,有多个工种的员工和多个老板,不同的老板对同一个员工的关注点是不同的,CTO可能关注的就是技术,CEO可能更注重绩效。
员工就是一个稳定的元素,老板就是变化的,对应概念就是:封装员工的一些操作,可以在不改变员工类的前提下,增加新的老板访问同一个员工。
在访问者模式中包含五个角色,抽象元素、具体元素、抽象访问者、具体访问者、结构元素。
抽象元素:定义一个接受访问的方法accept,参数为访问者对象。
具体元素:提供接受访问者访问的具体实现调用访问者的访问visit,并定义额外的数据操作方法。
抽象访问者:这个角色主要是定义对具体元素的访问方法visit,理论上来说方法数等于元素(固定类型的对象,也就是被访问者)个数。
具体访问者:实现对具体元素的访问visit方法,参数就是具体元素。
结构对象:创建一个数组用来维护元素,并提供一个方法访问所有的元素。
二、案例实现
在一个公司有干活的工程师和管理者,也有抓技术的CTO和管绩效的CEO,CTO和CEO都会访问管理员和工程师,当公司来了新的老板,只需要增加访问者即可。
工程师和管理者就是元素、公司就是结构体、CEO、CTO就是访问者。
抽象元素:
/** * 员工 抽象元素 被访问者 * @author tcy * @Date 29-09-2022 */ public interface ElementAbstract { void accept(VisitorAbstract visitor); }
具体元素-工程师:
/** * 工程师 具体元素 被访问者 * @author tcy * @Date 29-09-2022 */ public class ElementEngineer implements ElementAbstract { private String name; private int kpi; ElementEngineer(String name){ this.name = name; this.kpi = new Random().nextInt(10); } public String getName() { return name; } public int getKpi() { return kpi; } @Override public void accept(VisitorAbstract visitor) { visitor.visit(this); } public int getCodeLineTotal(){ return this.kpi * 1000000; } }
具体元素-管理者:
/** * 管理者 具体元素 被访问者 * @author tcy * @Date 29-09-2022 */ public class ElementManager implements ElementAbstract { private String name; private int kpi; ElementManager(String name){ this.name = name; this.kpi = new Random().nextInt(10); } public String getName() { return name; } public int getKpi() { return kpi; } @Override public void accept(VisitorAbstract visitor) { visitor.visit(this); } public int getProductNum(){ return this.kpi * 10; } }
抽象访问者:
/** * 抽象访问者 * @author tcy * @Date 29-09-2022 */ public interface VisitorAbstract { void visit(ElementEngineer engineer); void visit(ElementManager manager); }
具体访问者-CEO
/** * 具体访问者CEO * @author tcy * @Date 29-09-2022 */ public class VisitorCEO implements VisitorAbstract { @Override public void visit(ElementEngineer engineer) { System.out.println("工程师:" + engineer.getName() + "KPI:" + engineer.getKpi()); } @Override public void visit(ElementManager manager) { System.out.println("经理:" + manager.getName() + "KPI:" + manager.getKpi() + " 今年共完成项目:" + manager.getProductNum() + "个"); } }
具体访问者-CTO
/** * 具体访问者CTO * @author tcy * @Date 29-09-2022 */ public class VisitorCTO implements VisitorAbstract { @Override public void visit(ElementEngineer engineer) { System.out.println("工程师:" + engineer.getName() + " 今年代码量" + engineer.getCodeLineTotal() + "行"); } @Override public void visit(ElementManager manager) { System.out.println("经理:" + manager.getName() + " 今年共完成项目:" + manager.getProductNum() + "个"); } }
结构体:
/** * 结构对象 * @author tcy * @Date 29-09-2022 */ public class Structure { List<ElementAbstract> list = new ArrayList<>(); public Structure addEmployee(ElementAbstract employee){ list.add(employee); return this; } public void report(VisitorAbstract visitor){ list.forEach(employee -> { employee.accept(visitor); }); } }
客户端:
/** * @author tcy * @Date 29-09-2022 */ public class Client { public static void main(String[] args) { //元素对象 ElementEngineer engineerZ = new ElementEngineer("小张"); ElementEngineer engineerW = new ElementEngineer("小王"); ElementEngineer engineerL = new ElementEngineer("小李"); ElementManager managerZ = new ElementManager("张总"); ElementManager managerW = new ElementManager("王总"); ElementManager managerL = new ElementManager("李总"); //结构体对象 Structure structure = new Structure(); structure.addEmployee(engineerZ).addEmployee(engineerW).addEmployee(engineerL).addEmployee(managerZ).addEmployee(managerW).addEmployee(managerL); structure.report(new VisitorCTO()); System.out.println("---------------------------------------"); structure.report(new VisitorCEO()); } }
访问者不愧是最难的设计模式,方法间的调用错综复杂,日常开发的使用频率很低,很多程序员宁可代码写的麻烦一点也不用这种设计模式,但是作为学习者就要学习各种设计模式了。
三、访问者模式在JDk中的应用
JDK的NIO中的 FileVisitor 接口采用的就是访问者模式。
在早期的 Java 版本中,如果要对指定目录下的文件进行遍历,必须用递归的方式来实现,这种方法复杂且灵活性不高。
Java 7 版本后,Files 类提供了 walkFileTree() 方法,该方法可以很容易的对目录下的所有文件进行遍历,需要 Path、FileVisitor 两个参数。其中,Path 是要遍历文件的路径,FileVisitor 则可以看成一个文件访问器,源码如下。
FileVisitor 主要提供了 4 个方法,且返回结果的都是 FileVisitResult 对象值,用于决定当前操作完成后接下来该如何处理。FileVisitResult 是一个枚举类,代表返回之后的一些后续操作,源码如下。
FileVisitResult 主要包含 4 个常见的操作。
- FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续。
- FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。
- FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点。
- FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。
通过访问者去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件,这个类中都提供了相对应的方法。它的实现也非常简单,代码如下。
在JDK的应用中我们提供的文件就看做是一个稳定元素,对应访问者模式中的抽象元素;而Files.walkFileTree()方法中的FileVisitor 参数就可看做是角色中的访问者。
四、访问者模式中的伪动态双分派
访问者模式中有一个重要的概念叫:伪动态双分派。
我们一步一步解读它的含义,什么叫分派?根据对象的类型而对方法进行的选择,就是分派(Dispatch)。
发生在编译时的分派叫静态分派,例如重载(overload),发生在运行时的分派叫动态分派,例如重写(overwrite)。
其中分派又分为单分派和多分派。
单分派:依据单个变量进行方法的选择就叫单分派,Java 动态分派(重写)只根据方法的接收者一个变量进行分配,所以其是单分派。
多分派:依据多个变量进行方法的选择就叫多分派,Java 静态分派(重载)要根据方法的接收者与参数这两个变量进行分配,所以其是多分派。
理解了概念我们接着看我们的案例:
@Override public void accept(VisitorAbstract visitor) { visitor.visit(this); }
我们案例中的accept方法,是由元素的运行时类型决定的,应该是属于动态单分派。
我们接着看 visitor.visit(this)又是一次动态单分派,两次动态单分派实现了双分派的效果,所以称为伪动态双分派。
这个概念理解就好,实际应用中知不知道这玩意都不影响。
五、总结
当你有个类,里面的包含各种类型的元素,这个类结构比较稳定,不会经常增删不同类型的元素。而需要经常给这些元素添加新的操作的时候,考虑使用此设计模式。
适用对象结构比较稳定每增加一个元素访问者都要大变动,但加新的操作很简单。集中相关的操作、分离无关的操作。
缺点只有两个字-复杂,号称是最复杂的设计模式。
设计模式的学习要成体系,推荐你看我往期发布的设计模式文章。