设计模式(18)行为型模式 - 访问者模式

前言

温故而知新
还是先复习前面学到的知识:
  • 行为型模式:描述多个类或对象之间怎么协助共同完成单个对象无法实现的任务,涉及算法与对象间的职责分配
  • 模板方法模式:定义一个操作流程中的算法骨架,特定的步骤方法的具体实现延迟到子类,即方便与重定义一个算法的特定步骤(请客过程具体吃什么有子类决定,父类只定义一个请客流程:点单 - 》 吃饭 - 》买单)
  • 命令模式:将请求命令封装成对象,并将执行者聚合到命令中,发送者维护一个保存命令的容器,调用发送者去使用命令对象即可完成命令,使得命令发送者和命令执行者完全解耦(点击遥控器上的命令按键即可完成具体开关灯–遥控器并不知道如何执行,只管发送命令)
接下来,是一个比较复杂抽象而且使用也较少的设计模式:访问者模式
现实中存在的问题 购物车的问题:
设计模式(18)行为型模式 - 访问者模式
文章图片

顾客在超市中将选择的商品,如苹果、图书等放在购物车中,然后到收银员处付款。
在购物过程中,顾客需要对这些商品进行访问,以便确认这些商品的质量。
收银员计算价格时也需要访问购物车内顾客所选择的商品。
此时,购物车作为一个ObjectStructure(对象结构)用于存储各种类型的商品,而顾客和收银员作为访问这些商品的访问者,他们需要对商品进行检查和计价。
不同类型的商品其访问形式也可能不同,如苹果需要过秤之后再计价,而图书不需要。
这个场景,可以思考一下什么去描述?
学习过程中,掌握主动性,学会提问,让疑问带领着去学习
用传统方法,会创建对象:苹果、图书、顾客、收银员、购物车,然后互相调用方法???
传统方法可以解决,但是要是又增加了商品:可乐,需要修改很多类
学了这节访问者模式,会有很好的解决方案
访问者模式 先大致了解一下访问者模式的概念,再经过案例分析
访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式
访问者模式的目的是封装一些施加于某种对象结构的元素之上的操作,一旦这些操作需要修改的话,接受这个操作的对象结构可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式
模式结构:
设计模式(18)行为型模式 - 访问者模式
文章图片

模式角色:
  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit(),该操作中的参数类型标识了被访问的具体元素。
  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由List、Set、Map 等聚合类实现
模式分析
  • 访问者模式中对象结构角色存储了不同类型的元素对象,以供不同访问者访问。
  • 访问者模式包括两个层次结构,一个是访问者层次结构,提供了抽象访问者和具体访问者,一个是元素层次结构,提供了抽象元素和具体元素。
  • 相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同访问方式访问。在访问者模式中,增加新的访问者无须修改原有系统,系统具有较好的可扩展性
模式案例 用访问者模式实现上面说的购物车案例
简单实现:
设计模式(18)行为型模式 - 访问者模式
文章图片

package com.company.Behavioral.visitor; import java.util.ArrayList; import java.util.List; //抽象访问者 abstract class Visitor { public abstract void visit(Apple apple); public abstract void visit(Book book); } //具体访问者:顾客 class Customer extends Visitor{@Override public void visit(Apple apple) { System.out.println("-----查看苹果质量----"); }@Override public void visit(Book book) { System.out.println("-----查看书的名字-----"); } } //具体 class Cashier extends Visitor{@Override public void visit(Apple apple) { System.out.println(" 查看苹果的价钱。。。"); }@Override public void visit(Book book) { System.out.println(" 查看书本的价钱。。。"); } } //抽象产品 abstract class Product { public abstract void accept(Visitor visitor); } //用到了双分派:在后面的程序中具体的访问者作为参数传递到Apple(第一次分派) //Apple类调用了作为参数的“具体访问者”中的方法visit,同时将自己(this)作为参数传入,完成第二次的分派 class Apple extends Product{@Override public void accept(Visitor visitor) { visitor.visit(this); } } class Book extends Product{@Override public void accept(Visitor visitor) { visitor.visit(this); } } //数据结构,管理了很多产品(Customer,Woman) class ShoppingCart{ //维护一个集合 private List products = new ArrayList<>(); //增加到集合 public void attach(Product p){ products.add(p); } //从集合移除 public void detach(Product p){ products.remove(p); } //访问者访问购物车的情况 public void display(Visitor visitor){ for (Product p : products){ p.accept(visitor); } } }class Client{ public static void main(String[] args) { //创建ShoppingCart ShoppingCart shoppingCart = new ShoppingCart(); shoppingCart.attach(new Apple()); shoppingCart.attach(new Apple()); shoppingCart.attach(new Book()); //顾客查看购物车 Customer customer = new Customer(); shoppingCart.display(customer); //收银员查看购物车 Cashier cashier = new Cashier(); shoppingCart.display(cashier); } }

设计模式(18)行为型模式 - 访问者模式
文章图片

访问者模式可以使我们在不改变元素(具体的产品)的情况下,定义一下作用于这些产品的新操作(新的访问者)
如果我们创建一个新的访问者:调查者,调查者任务是调查顾客购物的类型
只需要新建一个具体访问者继承访问者接口即可
访问者模式的优缺点 优点
  • 使得增加新的访问操作变得很容易
  • 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中
  • 可以跨过类的等级结构访问属于不同的等级结构的元素类
  • 让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作
缺点 仔细想想,其实上面的购物车例子有点问题。。。
因为访问的物品类型已经在访问者类中写死了,造成
  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
  • 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问
从这个缺点看出,访问者模式应该是使用在元素类种类稳定的系统中
适用场景
  • 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
  • 对象结构中元素对应的类很少改变,但经常需要在此对象结构上定义新的操作
具体中的使用
  1. Java XML处理技术DOM4J中,可以通过访问者模式的方式来读取并解析XML文档,VisitorSupport是DOM4J提供的Visitor接口的默认适配器,具体访问者只需继承VisitorSupport类即可
什么?不知道DOM4J?可以看看真的了解XML吗? - XML解析方式
DOM4J解析XML利用访问者(VisitorSupport)解析XML文件。通过重写各种类型的visit()方法解析各种节点,文本内容
具体使用:
//解析器 SAXReader saxReader = new SAXReader(); //解析具体的xml文件 Document doc = saxReader.read(new File("book.xml")); //Visitor是自定义的访问者,里面重写了VisitorSupport的visit方法 doc.accept(new Visitor());

class Visitor extends VisitorSupport{ @Override public void visit(Attribute node){ //自定义访问结点Attribute的操作 } @Override public void visit(Element node){ //自定义访问结点Element操作 } }

  1. 编译器的设计
程序代码是被访问的对象,它包括变量定义、变量赋值、逻辑运算、算术运算等语句,编译器需要对代码进行分析
可以将不同的操作封装在不同的类中,如检查变量定义的类、检查变量赋值的类、检查算术运算是否合法的类,这些类就是具体访问者,可以访问程序代码中不同类型的语句
可以将每一个不同编译阶段的操作封装到了跟该阶段有关的一个访问者类中
模式扩展 访问者模式同“组合模式”联用
【设计模式(18)行为型模式 - 访问者模式】访问者模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式
设计模式(18)行为型模式 - 访问者模式
文章图片

这样联用已经比较复杂了
访问者模式与迭代器模式联用
我们的访问者的对象结构类中维护了一个元素对象的集合,因此访问者模式经常需要与迭代器模式联用,在对象结构中使用迭代器来遍历元素对象(案例中使用for-each循环遍历也是用到了迭代器)
“开闭原则”倾斜
访问者模式以一种倾斜的方式支持“开闭原则”,增加新的访问者方便,但是增加新的元素很困难
还记得前面也有一个“开闭原则”倾斜:抽象工厂模式
抽象工厂模式中增加产品族很简单,增加产品等级很困难
这是因为,我们必须在一个对象结构(抽象工厂)写死完整的体系,只能做到针对一个方面,所以选择设计模式是需要根据实际情况抉择
总结
  • 访问者模式是封装一些施加与对象结构上的元素的操作,即将访问者隔离开这个对象结构,当新增访问操作时并不会改动这个对象结构
  • 访问者模式的结构有5个角色:抽象访问者、具体访问者、抽象元素角色、具体元素角色、对象结构角色
  • 访问者拥有访问所有的元素的操作;元素角色拥有接收访问者操作的接口;对象结构角色维护了一个存储元素角色的集合,并提供给访问者遍历元素角色的方法
  • 访问者模式的优点是新增访问者(访问操作)方便,在不修改对象结构的情况下可以新增对元素的访问;缺点是新增元素角色困难,一定程度会破坏系统的封装性
  • 访问者模式适用场景:一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作;需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类;对象结构中元素对应的类很少改变,但经常需要在此对象结构上定义新的操作
  • 访问者模式可以和组合模式联用,即元素对象可以有叶子元素对象、容器元素对象;访问者模式可以和迭代器模式联用,即对象结构中的元素使用迭代器遍历
访问者模式开闭原则倾斜,使用时需要根据实际情况选择

    推荐阅读