设计模式中的访问者模式
访问者模式是GOF的23中设计模式中比较复杂的一种模式,最近在项目的开发中使用到了访问者模式的,依据项目的代码,来对该模式进行总结
访问者模式的定义:
访问者模式表示一个作用于某对象结构中国的各元素的操作,它使你可以在不改变各元素的前提下,定义作用于这些元素的新操作
定义比较抽象,我举一个我们的使用场景的例子,在我们的业务场景中,项目分为两类,院内项目和国拨项目.这个分类是稳定的,我们对外提供的接口,需要用户以各种自定义的方式来查询自己需要的项目,这些个查询操作是不固定的,有可能会动态地增加,根据开放--封闭原则,我们将经常变化的部分(就是对项目的查询操作)和不变的部分(项目的结构)进行分离,者就是访问者模式的很典型的应用场景
使用访问者模式,必须定义两个类层次,一个对应于接受操作的元素(在我们的项目中,就是"项目"这一层次),另一个对应于定义对元素的操作的访问者(访问者模式中的visitor,在我们的项目中,也就是各种各样的查询和筛选操作).给访问者类层次增加一个新的子类,就可以创建一个新的查询操作,为项目增加新的功能
访问者模式的适用性
在以下的情况下使用访问者模式
-
一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作
-
需要对一个对象结构中的对象进行很多的不同且不相关的操作,而你想避免让这些操作污染这些对象的类,Visitor可以让你将相关的操作集中起来定义到一个类中,当该对象结构被很多应用共享的时候,用visitor模式让每个应用仅包含需要用到的操作
-
定义对象结构的类很少改变,但是经常需要在此结构上定义新的操作,改变对象结构类需要重新定义所有的访问者的接口,这可能需要很大的代价,如果对象结构经常改变,那么,可能还是在这些类中定义这些操作比较好
访问者模式的类图
访问者模式的示例代码
首先是Element
public interface Element {
void accept(Visitor visitor);
}
ElementA:
public class ElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public void doOtherThing(){
System.out.println("I am ElementA");
}
}
ElementB:
public class ElementB implements Element {
@Override
public void accept(Visitor visitor) {
}
public void doSomething(){
System.out.println("我是ElementB");
}
}
接下来是Visitor的层次结构:
Visitor:
public interface Visitor {
void visit(ElementA element);
void visit(ElementB element);
}
ConcreteVisitorA:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConcreteVisitorA implements Visitor {
private static Logger logger = LoggerFactory.getLogger(ConcreteVisitorA.class);
@Override
public void visit(ElementA element) {
logger.info("使用ConcreteVisitorA来访问ElementA");
element.doOtherThing();
}
@Override
public void visit(ElementB element) {
logger.info("使用ConcreteVisitorA来访问ElementB");
element.doSomething();
}
}
ConcreteVisitorB:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConcreteVisitorB implements Visitor {
private static Logger logger = LoggerFactory.getLogger(ConcreteVisitorB.class);
@Override
public void visit(ElementA element) {
logger.info("使用ConcreteVisitorB来访问ElementA");
element.doOtherThing();
}
@Override
public void visit(ElementB element) {
logger.info("使用ConcreteVisitorB来访问ElementB");
element.doSomething();
}
}
ObjectStruct:
public class ObjectStruct {
private List<Element> elementList;
public void addElement(Element element) {
elementList.add(element);
}
public List<Element> getElementList() {
return elementList;
}
private void init() {
//防止其他的线程修改数据,使用CopyonWriteArrayList,修改的时候,会在副本上修改
elementList = new CopyOnWriteArrayList<>();
elementList.add(new ElementA());
elementList.add(new ElementB());
elementList.add(new ElementB());
}
public ObjectStruct() {
init();
}
}
客户端:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class VisitorClient {
private static Logger logger = LoggerFactory.getLogger(VisitorClient.class);
public static void main(String[] args) {
ObjectStruct objectStruct = new ObjectStruct();
Visitor visitor = new ConcreteVisitorA();
objectStruct.getElementList().forEach(item -> item.accept(visitor));
Visitor visitor1 = new ConcreteVisitorA();
objectStruct.getElementList().forEach(item -> item.accept(visitor1));
Visitor visitor2 = new Visitor() {
@Override
public void visit(ElementA element) {
logger.info("使用新的Visitor来访问元素A");
}
@Override
public void visit(ElementB element) {
logger.info("使用新的Visitor来访问元素B");
}
};
objectStruct.getElementList().forEach(item -> item.accept(visitor2));
}
}
访问者模式的缺点
-
打破了封装,在Visitor里边依赖了Element层次结构中的实现类,这就是根据实现类编程,而不是根据接口编程
-
要求Element的层次结构固定,如果想要在Element的层次结构上添加新的子类,需要改动所有的Visitor,改动非常大
分派
在接触访问者模式的时候,遇到了分派的概念,访问者模式是"伪动态双分派",怎么理解这么一句话呢?
首先,我们来了解一下分派的概念:
分派的概念:
变量被声明时的类型叫做变量的静态类型(Static Type) 又叫明显类型(Apparent Type)。变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。
根据对象的类型而对方法进行的选择,就是分派(Dispatch)。
根据分派发生的时期,可以将分派分为两种,即静态分派和动态分派。
静态分派和动态分派
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。方法重载(Overload)就是静态分派的最典型的应用。(所谓的:编译时多态)
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。面向对象的语言利用动态分派来实现方法置换产生的多态性。(所谓的:运行时多态)
也就是,运行的时候,根据参数的类型,选择合适的重载方法,就是动态分派
单分派和多分派:
方法的宗量:
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少宗量,可以把分派划分为单分派和多分派(双分派是多分派的一种形式)单分派是根据一个宗量来对方法进行选择,多分派则是根据多个宗量来对方法进行选择
以下是一个来自<深入理解Java虚拟机>上的例子:
public class Dispatch {
static class QQ {
}
static class _360 {
}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choice qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choice 360");
}
}
public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choice qq");
}
@Override
public void hardChoice(_360 arg) {
System.out.println("son choice 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
在编译器阶段,编译器对方法的选择过程,也就是静态分派的过程,这时候,选择目标方法的依据有两点,一是静态类型是Father 还是Son,二是方法参数是QQ还是_360,这次选择的结果,产生了两条invokevirtual虚拟机指令,这两条指令的参数分别为常量池中指向Father.hardChoice(QQ);和Father.hardChoice(360)方法的符号引用,因为是根据静态类型和方法参数两个宗量来进行方法的选择,所以,Java语言的静态分派属于多分派
运行期间,虚拟机对方法的选择,也就是动态分派的过程,执行到 son.hardChoice(new QQ());这句代码对应的虚拟机指令的时候,由于编译期已经确定,目标方法的签名必须为 hardChoice(QQ),虚拟机不会关心传递过来的参数的类型是哪一个QQ的实现,参数的静态类型和实际类型都不会对方法的选择产生影响,唯一可以影响虚拟机对方法选择的因素只有此方法的接收者的实际类型是Father还是Son,因为只有一个选择的依据(就是接收者的实际的类型) 所以,Java语言的动态分派属于单分派
上边的访问者模式的代码
objectStruct.getElementList().forEach(item -> item.accept(visitor1));
item.accept(visitor); 所有的item接收的方法的类型都是visitor类型,方法的参数类型不会影响虚拟机对方法的选择,虚拟机具体是调用ElementA的accept()方法还是调用ElementB的accept()方法,是由item的实际的类型来决定的,在此完成了一次动态单分派
而 accept(visitor)方法的实现
visitor.visit(this)
在运行时根据this的具体类型来选择是调用visitor的 visit(ElementA element)方法还是调用visit(ElementB element)方法,在此完成了一次动态的单分派
两次动态单分派结合起来,就完成了一次伪动态双分派,先确定了调用哪个Element的accept()方法,然后再确定了调用Visitor中的针对哪个Element的visit()方法,这就是伪动态双分派
在accept的方法实现中,传递this进去 具体的visitor根据this的类型,又完成了一次分派,找到了需要调用的方法