设计模式之访问者模式
访问者模式属于行为型模式;指将作用于某种数据结构中各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
双重分派
数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过程叫做“双重分派”。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。
双重分派意味着施加于节点之上的操作是基于访问者和节点本身的数据类型,而不仅仅是其中的一者。
单分派和多分派
方法的接收者和方法的参数统称为方法的宗量。 根据分派基于宗量多少(接收者是一个宗量,参数是一个宗量),可以将分派分为单分派和多分派。单分派是指根据一个宗量就可以知道调用目标(即应该调用哪个方法),多分派需要根据多个宗量才能确定调用目标。
访问者模式的UML类图如下:
从上图可以看出,访问者模式涉及到抽象访问者角色、具体访问者角色、抽象节点角色、具体节点角色、结构对象角色以及客户端角色等6个角色:
- 抽象访问者(Visitor)角色:声明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。
- 抽象节点(Node)角色:声明一个接受操作,接受一个访问者对象作为一个参量。
- 具体节点(Node)角色:实现了抽象元素所规定的接受操作。
- 结构对象(ObjectStructure)角色:有如下的一些责任,可以遍历结构中的所有元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或者一个聚集,如列(List)或集合(Set)。
从上图可以看到,抽象访问者角色为每一个具体节点都准备了一个访问操作,由于有两个节点,因此,对应的就有两个访问操作。
投票例子
我们都看过很多歌唱选秀类的综艺节目,当某个表演者表演完毕后,下面的评委需要对其表演进行评价(晋级,淘汰,待定),决定其是否晋级下一轮,我们这里就以这个为例子讲解访问者设计模式。
例子的UML类图:
抽象节点角色:
package com.charon.visitor;
/**
* @className: Person
* @description:
* @author: charon
* @create: 2022-03-27 17:18
*/
public abstract class Person {
abstract void accept(Action action);
}
具体节点角色:
package com.charon.visitor;
/**
* @className: Man
* @description:
* @author: charon
* @create: 2022-03-27 17:19
*/
public class Man extends Person {
private String name;
public Man(String name) {
this.name = name;
}
/**
* Gets the value of name
*
* @return the value of name
*/
public String getName() {
return name;
}
@Override
void accept(Action action) {
action.getManResult(this);
}
}
package com.charon.visitor;
/**
* @className: Woman
* @description:
* @author: charon
* @create: 2022-03-27 17:19
*/
public class Woman extends Person{
private String name;
public Woman(String name) {
this.name = name;
}
/**
* Gets the value of name
*
* @return the value of name
*/
public String getName() {
return name;
}
@Override
void accept(Action action) {
action.getWomanResult(this);
}
}
抽象访问者角色:
package com.charon.visitor;
/**
* @className: Action
* @description:
* @author: charon
* @create: 2022-03-27 17:19
*/
public abstract class Action {
/**
* 得到男人的评价结果
* @param man
*/
abstract void getManResult(Man man);
/**
* 得到女人的评价结果
* @param woman
*/
abstract void getWomanResult(Woman woman);
}
具体访问者角色:
package com.charon.visitor;
/**
* @className: FailAction
* @description:
* @author: charon
* @create: 2022-03-27 17:21
*/
public class FailAction extends Action{
@Override
void getManResult(Man man) {
System.out.println(man.getName() + " 给的评价是该表演者表演淘汰。。。。");
}
@Override
void getWomanResult(Woman woman) {
System.out.println(woman.getName() + " 给的评价是该表演者表演淘汰。。。。");
}
}
package com.charon.visitor;
/**
* @className: SuccessAction
* @description:
* @author: charon
* @create: 2022-03-27 17:22
*/
public class SuccessAction extends Action{
@Override
void getManResult(Man man) {
System.out.println(man.getName() + " 给的评价是该表演者表演晋级。。。。");
}
@Override
void getWomanResult(Woman woman) {
System.out.println(woman.getName() + " 给的评价是该表演者表演晋级。。。。");
}
}
结构对象角色:
package com.charon.visitor;
import java.util.ArrayList;
import java.util.List;
/**
* @className: ObjectStructure
* @description:
* @author: charon
* @create: 2022-03-27 17:25
*/
public class ObjectStructure {
/**
* 集合,用于存放表演的评价者
*/
private List<Person> persons = new ArrayList<>();
public void add(Person person){
persons.add(person);
}
public void remove(Person person){
persons.remove(person);
}
/**
* 显示评价结果
* @param action
*/
public void display(Action action){
for (Person person : persons) {
person.accept(action);
}
}
}
测试:
package com.charon.visitor;
/**
* @className: Client
* @description:
* @author: charon
* @create: 2022-03-27 17:29
*/
public class Client {
public static void main(String[] args) {
ObjectStructure structure = new ObjectStructure();
structure.add(new Man("男评委1"));
structure.add(new Man("男评委2"));
structure.add(new Woman("女评委1"));
structure.add(new Woman("女评委2"));
structure.display(new SuccessAction());
}
}
打印:
男评委1 给的评价是该表演者表演晋级。。。。
男评委2 给的评价是该表演者表演晋级。。。。
女评委1 给的评价是该表演者表演晋级。。。。
女评委2 给的评价是该表演者表演晋级。。。。
如上所示,访问者模式的代码就完成了,如果现在需要添加一个待定的操作类型,就只需要添加一个WaitAction就行了:
package com.charon.visitor;
/**
* @className: WaitAction
* @description:
* @author: charon
* @create: 2022-03-27 17:39
*/
public class WaitAction extends Action{
@Override
void getManResult(Man man) {
System.out.println(man.getName() + " 给的评价是该表演者表演待定。。。。");
}
@Override
void getWomanResult(Woman woman) {
System.out.println(woman.getName() + " 给的评价是该表演者表演待定。。。。");
}
}
访问者模式的优点如下:
- 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
- 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
- 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
访问者模式的缺点如下:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
由于访问者模式的这些缺点,导致很多人反对使用访问者模式。
访问者模式的应用场景
当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。
通常在以下情况可以考虑使用访问者模式。
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
- 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。