建设部招标网 官方网站,wordpress主题移动,网络软文营销,包装策划与设计专业目录 1.概述2.结构3.案例实现3.1.抽象访问者类3.2.抽象元素类3.3.具体元素类3.4.具体访问者类3.5.对象结构类3.6.测试 4.优缺点5.使用场景6.扩展6.1.分派6.2.动态分配6.3.静态分配6.4.双分派 1.概述
访问者模式 (Visitor Pattern) 是一种行为型设计模式#xff0c;它用于将数… 目录 1.概述2.结构3.案例实现3.1.抽象访问者类3.2.抽象元素类3.3.具体元素类3.4.具体访问者类3.5.对象结构类3.6.测试 4.优缺点5.使用场景6.扩展6.1.分派6.2.动态分配6.3.静态分配6.4.双分派 1.概述
访问者模式 (Visitor Pattern) 是一种行为型设计模式它用于将数据结构和在数据结构上的操作分离开来。访问者模式可以让你在不修改数据结构的情况下定义新的操作。
2.结构
访问者模式包含以下主要角色
抽象访问者 (Visitor) 角色定义了对每一个元素 (Element) 访问的行为它的参数就是可以访问的元素它的方法个数理论上来讲与元素类个数Element的实现类个数是一样的从这点不难看出访问者模式要求元素类的个数不能改变。具体访问者 (ConcreteVisitor) 角色给出对每一个元素类访问时所产生的具体行为。抽象元素 (Element) 角色 定义了一个接受访问者的方法 (accept)其意义是指每一个元素都要可以被访问者访问。具体元素 (ConcreteElement) 角色提供接受访问方法的具体实现而这个具体的实现通常情况下是使用访问者提供的访问该元素类的方法。对象结构 (Object Structure) 角色定义当中所提到的对象结构对象结构是一个抽象表述具体点可以理解为一个具有容器性质或者复合对象特性的类它会含有一组元素 (Element)并且可以迭代这些元素供访问者访问。
3.案例实现
【例】给宠物喂食现在养宠物的人特别多我们就以这个为例当然宠物还分为狗猫等要给宠物喂食的话主人可以喂其他人也可以喂食。类图如下 具体实现代码如下
3.1.抽象访问者类
Person.java
//抽象访问者角色接口
public interface Person {//给宠物猫喂食void feed(Cat cat);//给宠物狗喂食void feed(Dog dog);
}3.2.抽象元素类
Animal.java
//抽象元素角色类
public interface Animal {//接受访问者访问的功能void accept(Person person);
}3.3.具体元素类
Cat.java
//具体元素角色类宠物猫
public class Cat implements Animal{Overridepublic void accept(Person person) {//访问者给宠物猫喂食person.feed(this);System.out.println(宠物猫接受喂食);}
}Dog.java
//具体元素角色类宠物狗
public class Dog implements Animal{Overridepublic void accept(Person person) {//访问者给宠物狗喂食person.feed(this);System.out.println(宠物狗接受喂食);}
}3.4.具体访问者类
Owner.java
//具体访问者角色类(宠物主人)
public class Owner implements Person{Overridepublic void feed(Cat cat) {System.out.println(主人给猫喂食);}Overridepublic void feed(Dog dog) {System.out.println(主人给狗喂食);}
}SomeOne.java
//具体访问者角色类(其他人)
public class SomeOne implements Person{Overridepublic void feed(Cat cat) {System.out.println(其他人给猫喂食);}Overridepublic void feed(Dog dog) {System.out.println(其他人给猫喂食);}
}3.5.对象结构类
Home.java
//对象结构类
public class Home {//声明一个集合对象用来存储元素对象private ListAnimal nodeList new ArrayList();//添加元素public void add(Animal animal){nodeList.add(animal);}public void action(Person person){//遍历集合获取每一个元素让访问者访问每一个元素for (Animal animal : nodeList) {animal.accept(person);}}
}3.6.测试
Client.java
public class Client {public static void main(String[] args) {//创建 Home 对象Home home new Home();//添加元素到 Home 对象中home.add(new Dog());home.add(new Cat());//创建主人对象Owner owner new Owner();//让主人喂食所有的宠物home.action(owner);System.out.println();//创建其他人对象SomeOne someOne new SomeOne();//让其他人喂食所有的宠物home.action(someOne);}
}结果如下
主人给狗喂食
宠物狗接受喂食
主人给猫喂食
宠物猫接受喂食其他人给猫喂食
宠物狗接受喂食
其他人给猫喂食
宠物猫接受喂食4.优缺点
1访问者模式的优点和缺点如下
优点 通过访问者模式可以在不改变元素类的前提下增加新的访问操作从而扩展元素类的功能符合开闭原则。将数据结构和操作解耦可以使得操作随着元素类的变化而变化而不需要修改元素类提高代码的可扩展性和可维护性。在访问者模式中访问者可以在访问元素的同时进行某些其他操作这些其他操作可以是访问者所独有的从而增加灵活性和适应性。 缺点 访问者模式增加了类的数量引入了新的接口和抽象类增加了系统的复杂度。对于不同类型的元素如果新增了访问操作则访问者的接口和实现都需要修改增加了维护难度。
2综上访问者模式适合在访问操作的种类比较固定的情况下使用同时访问者的使用场景也是比较局限的需要根据具体的场景来判断是否使用。
5.使用场景
1访问者模式适用于以下场景
数据结构相对稳定但需要定义新的操作当数据结构的类层次结构相对稳定但需要增加新的操作时可以使用访问者模式。通过引入访问者模式可以在不修改数据结构的情况下定义新的操作。数据结构和操作分离当一个数据结构中的元素类比较固定但需要对这些元素进行不同的操作时可以使用访问者模式。通过将元素类和具体操作分离可以增加灵活性和可扩展性。数据结构中的元素稳定但元素操作多变当一个数据结构中的元素类相对稳定但需要对这些元素进行多种操作时可以使用访问者模式。通过将元素类和操作解耦可以减少元素类的修改提高代码的可维护性和可扩展性。对数据结构的访问需求固定当对一个数据结构的访问需求相对固定但访问方式可以变化时可以使用访问者模式。通过定义不同的访问者可以对数据结构的不同部分进行不同的访问操作。
2需要注意的是访问者模式的使用需要权衡代码的复杂性和可维护性因此在选择使用访问者模式时需要根据具体的需求和场景来判断是否合适。
6.扩展
事实上访问者模式用到了一种名为双分派的技术。
6.1.分派
变量被声明时的类型叫做变量的静态类型有些人又把静态类型叫做明显类型而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map new HashMap()map 变量的静态类型是 Map实际类型是 HashMap 。根据对象的类型而对方法进行的选择就是分派 (Dispatch)分派又分为两种即静态分派和动态分派。
静态分派 (Static Dispatch)发生在编译时期分派根据静态类型信息发生。静态分派对于我们来说并不陌生方法重载就是静态分派。动态分派 (Dynamic Dispatch)发生在运行时期动态分派动态地置换掉某个方法。Java 通过方法的重写支持动态分派。
6.2.动态分配
通过方法的重写支持动态分派。
public class Animal { public void execute() { System.out.println(Animal); }
}public class Dog extends Animal { Override public void execute() { System.out.println(dog); }
}public class Cat extends Animal { Override public void execute() { System.out.println(cat);}
}public class Client { public static void main(String[] args) { Animal a1 new Dog(); a1.execute(); Animal a2 new Cat(); a2.execute(); }
}运行结果如下
dog
cat上面代码的结果大家应该很容易想到这不就是多态吗运行执行的是子类中的方法。Java编译器在编译时期并不总是知道哪些代码会被执行因为编译器仅仅知道对象的静态类型而不知道对象的真实类型而方法的调用则是根据对象的真实类型而不是静态类型。
6.3.静态分配
通过方法重载支持静态分派。
public class Animal {
}public class Dog extends Animal {
}public class Cat extends Animal {
}public class Execute { public void execute(Animal a) { System.out.println(Animal); }public void execute(Dog d) { System.out.println(dog); }public void execute(Cat c) { System.out.println(cat); }
}public class Client { public static void main(String[] args) { Animal a new Animal(); Animal a1 new Dog(); Animal a2 new Cat(); Execute exe new Execute(); exe.execute(a); exe.execute(a1); exe.execute(a2); }
}运行结果如下
animal
animal
animal这个结果可能出乎一些人的意料了为什么呢因为重载方法的分派是根据静态类型进行的这个分派过程在编译时期就完成了。
6.4.双分派
所谓双分派技术就是在选择一个方法的时候不仅仅要根据消息接收者 (receiver) 的运行时区别还要根据参数的运行时区别。
1双分派技术 (Double Dispatch) 是一种多态性的应用它允许在运行时根据两个对象的类型来确定方法的调用
在传统的单分派多态性中方法的调用取决于消息接收者的类型。在双分派技术中方法的调用依赖于两个对象的类型即消息接收者和方法的参数。
2具体来说双分派技术通过多次派发来确定要执行的方法。首先根据消息接收者的类型选择适当的方法版本。然后根据方法的参数类型再次选择适当的方法版本。这种双重派发的方式使得程序能够灵活地根据多个对象的类型进行方法调用从而实现更加动态和灵活的行为。 一个常见的应用场景是访问者模式其中访问者对象根据元素对象和自身的类型来决定要执行的操作方法。通过双分派技术可以在访问者模式中根据元素和访问者的具体类型来选择正确的访问方法。 3下面以图形绘制为例展示双分派技术的应用过程假设有一个图形类库其中定义了图形类 Shape 和绘制器类 Drawer其中 Shape 类有几个子类 Circle、Rect 和 TriangleDrawer 类有几个子类 ColorDrawer、GrayDrawer 和 RedDrawer。现在需要根据不同的图形和绘制器来绘制不同的图形。
使用传统的单分派多态性方式来实现需要为每个图形类和绘制器类的组合定义对应的 draw 方法这个方法的实现是以所有可能的组合为基础实现的类将有很多重复的代码。而双分派技术使用双重派发来避免这些重复的代码具体流程如下
定义 Shape 类的 accept 方法传入一个 Drawer 实例作为参数其中 accept 方法依赖于具体的 Shape 子类。
public abstract class Shape {public abstract void accept(Drawer drawer);
}public class Circle extends Shape {Overridepublic void accept(Drawer drawer) {drawer.drawCircle(this);}
}public class Rect extends Shape {Overridepublic void accept(Drawer drawer) {drawer.drawRect(this);}
}public class Triangle extends Shape {Overridepublic void accept(Drawer drawer) {drawer.drawTriangle(this);}
}定义 Drawer 类的 drawCircle、drawRect 和 drawTriangle 方法这些方法依赖于具体的 Drawer 子类。
public abstract class Drawer {public abstract void drawCircle(Circle circle);public abstract void drawRect(Rect rect);public abstract void drawTriangle(Triangle triangle);
}public class ColorDrawer extends Drawer {Overridepublic void drawCircle(Circle circle) {System.out.println(绘制一个彩色的圆形);}Overridepublic void drawRect(Rect rect) {System.out.println(绘制一个彩色的矩形);}Overridepublic void drawTriangle(Triangle triangle) {System.out.println(绘制一个彩色的三角形);}
}public class GrayDrawer extends Drawer {Overridepublic void drawCircle(Circle circle) {System.out.println(绘制一个灰色的圆形);}Overridepublic void drawRect(Rect rect) {System.out.println(绘制一个灰色的矩形);}Overridepublic void drawTriangle(Triangle triangle) {System.out.println(绘制一个灰色的三角形);}
}public class RedDrawer extends Drawer {Overridepublic void drawCircle(Circle circle) {System.out.println(绘制一个红色的圆形);}Overridepublic void drawRect(Rect rect) {System.out.println(绘制一个红色的矩形);}Overridepublic void drawTriangle(Triangle triangle) {System.out.println(绘制一个红色的三角形);}
}在主程序中根据具体的 Shape 子类和 Drawer 子类来调用对应的方法。
public class Main {public static void main(String[] args) {Shape shape new Circle();Drawer drawer new ColorDrawer();shape.accept(drawer); //输出结果为: 绘制一个彩色的圆形}
}在上述示例中accept 方法根据具体的 Shape 子类调用对应的 Drawer 子类中的方法在 Drawer 类中具体的 drawCircle、drawRect 和 drawTriangle 方法会根据具体的 Shape 子类调用正确的方法。这样通过双重派发技术可以在运行时根据 Shape 子类和 Drawer 子类的具体类型来确定要调用的方法从而避免了大量的重复代码。
4说到这里我们已经明白双分派是怎么回事了但是它有什么效果呢就是可以实现方法的动态绑定我们可以对上面的程序进行修改。双分派实现动态绑定的本质就是在重载方法委派的前面加上了继承体系中覆盖的环节由于覆盖是动态的所以重载就是动态的了。