定义:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
类型:行为类模式
类图:
访问者模式可能是行为类模式中最复杂的一种模式了,但是这不能成为我们不去掌握它的理由。我们首先来看一个简单的例子,代码如下:
- class A {
- public void method1(){
- System.out.println("我是A");
- }
- public void method2(B b){
- b.showA(this);
- }
- }
- class B {
- public void showA(A a){
- a.method1();
- }
- }
我们主要来看一下在类A中,方法method1和方法method2的区别在哪里,方法method1很简单,就是打印出一句“我是A”;方法method2稍微复杂一点,使用类B作为参数,并调用类B的showA方法。再来看一下类B的showA方法,showA方法使用类A作为参数,然后调用类A的method1方法,可以看到,method2方法绕来绕去,无非就是调用了一下自己的method1方法而已,它的运行结果应该也是“我是A”,分析完之后,我们来运行一下这两个方法,并看一下运行结果:
- public class Test {
- public static void main(String[] args){
- A a = new A();
- a.method1();
- a.method2(new B());
- }
- }
运行结果为:
我是A
我是A
看懂了这个例子,就理解了访问者模式的90%,在例子中,对于类A来说,类B就是一个访问者。但是这个例子并不是访问者模式的全部,虽然直观,但是它的可扩展性比较差,下面我们就来说一下访问者模式的通用实现,通过类图可以看到,在访问者模式中,主要包括下面几个角色:
- 抽象访问者:抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法中的参数定义哪些对象是可以被访问的。
- 访问者:实现抽象访问者所声明的方法,它影响到访问者访问到一个类后该干什么,要做什么事情。
- 抽象元素类:接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。抽象元素一般有两类方法,一部分是本身的业务逻辑,另外就是允许接收哪类访问者来访问。
- 元素类:实现抽象元素类所声明的accept方法,通常都是visitor.visit(this),基本上已经形成一种定式了。
- 结构对象:一个元素的容器,一般包含一个容纳多个不同类、不同接口的容器,如List、Set、Map等,在项目中一般很少抽象出这个角色。
访问者模式的通用代码实现
- abstract class Element {
- public abstract void accept(IVisitor visitor);
- public abstract void doSomething();
- }
- interface IVisitor {
- public void visit(ConcreteElement1 el1);
- public void visit(ConcreteElement2 el2);
- }
- class ConcreteElement1 extends Element {
- public void doSomething(){
- System.out.println("这是元素1");
- }
- public void accept(IVisitor visitor) {
- visitor.visit(this);
- }
- }
- class ConcreteElement2 extends Element {
- public void doSomething(){
- System.out.println("这是元素2");
- }
- public void accept(IVisitor visitor) {
- visitor.visit(this);
- }
- }
- class Visitor implements IVisitor {
- public void visit(ConcreteElement1 el1) {
- el1.doSomething();
- }
- public void visit(ConcreteElement2 el2) {
- el2.doSomething();
- }
- }
- class ObjectStruture {
- public static List<Element> getList(){
- List<Element> list = new ArrayList<Element>();
- Random ran = new Random();
- for(int i=0; i<10; i++){
- int a = ran.nextInt(100);
- if(a>50){
- list.add(new ConcreteElement1());
- }else{
- list.add(new ConcreteElement2());
- }
- }
- return list;
- }
- }
- public class Client {
- public static void main(String[] args){
- List<Element> list = ObjectStruture.getList();
- for(Element e: list){
- e.accept(new Visitor());
- }
- }
- }
访问者模式的优点
- 符合单一职责原则:凡是适用访问者模式的场景中,元素类中需要封装在访问者中的操作必定是与元素类本身关系不大且是易变的操作,使用访问者模式一方面符合单一职责原则,另一方面,因为被封装的操作通常来说都是易变的,所以当发生变化时,就可以在不改变元素类本身的前提下,实现对变化部分的扩展。
- 扩展性良好:元素类可以通过接受不同的访问者来实现对不同操作的扩展。
访问者模式的适用场景
假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,为了避免这些操作污染这个对象,则可以使用访问者模式来把这些操作封装到访问者中去。
假如一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。
但是,访问者模式并不是那么完美,它也有着致命的缺陷:增加新的元素类比较困难。通过访问者模式的代码可以看到,在访问者类中,每一个元素类都有它对应的处理方法,也就是说,每增加一个元素类都需要修改访问者类(也包括访问者类的子类或者实现类),修改起来相当麻烦。也就是说,在元素类数目不确定的情况下,应该慎用访问者模式。所以,访问者模式比较适用于对已有功能的重构,比如说,一个项目的基本功能已经确定下来,元素类的数据已经基本确定下来不会变了,会变的只是这些元素内的相关操作,这时候,我们可以使用访问者模式对原有的代码进行重构一遍,这样一来,就可以在不修改各个元素类的情况下,对原有功能进行修改。
总结
正如《设计模式》的作者GoF对访问者模式的描述:大多数情况下,你并需要使用访问者模式,但是当你一旦需要使用它时,那你就是真的需要它了。当然这只是针对真正的大牛而言。在现实情况下(至少是我所处的环境当中),很多人往往沉迷于设计模式,他们使用一种设计模式时,从来不去认真考虑所使用的模式是否适合这种场景,而往往只是想展示一下自己对面向对象设计的驾驭能力。编程时有这种心理,往往会发生滥用设计模式的情况。所以,在学习设计模式时,一定要理解模式的适用性。必须做到使用一种模式是因为了解它的优点,不使用一种模式是因为了解它的弊端;而不是使用一种模式是因为不了解它的弊端,不使用一种模式是因为不了解它的优点。
java的动态绑定
所谓的动态绑定就是指程执行期间(而不是在编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。java继承体系中的覆盖就是动态绑定的,看一下如下的代码:
- class Father {
- public void method(){
- System.out.println("This is Father's method");
- }
- }
- class Son1 extends Father{
- public void method(){
- System.out.println("This is Son1's method");
- }
- }
- class Son2 extends Father{
- public void method(){
- System.out.println("This is Son2's method");
- }
- }
- public class Test {
- public static void main(String[] args){
- Father s1 = new Son1();
- s1.method();
- Father s2 = new Son2();
- s2.method();
- }
- }
运行结果如下:
This is Son1's method
This is Son2's method
通过运行结果可以看到,尽管我们引用的类型是Father类型的,但是运行时却是调用的它实际类型(也就是Son1和Son2)的方法,这就是动态绑定。在java语言中,继承中的覆盖就是是动态绑定的,当我们用父类引用实例化子类时,会根据引用的实际类型调用相应的方法。
java的静态绑定
相对于动态绑定,静态绑定就是指在编译期就已经确定执行哪一个方法。在java中,方法的重载(方法名相同而参数不同)就是静态绑定的,重载时,执行哪一个方法在编译期就已经确定下来了。看一下代码:
- class Father {}
- class Son1 extends Father{}
- class Son2 extends Father{}
- class Execute {
- public void method(Father father){
- System.out.println("This is Father's method");
- }
- public void method(Son1 son){
- System.out.println("This is Son1's method");
- }
- public void method(Son2 son){
- System.out.println("This is Son2's method");
- }
- }
- public class Test {
- public static void main(String[] args){
- Father father = new Father();
- Father s1 = new Son1();
- Father s2 = new Son2();
- Execute exe = new Execute();
- exe.method(father);
- exe.method(s1);
- exe.method(s2);
- }
- }
运行结果如下:
This is Father's method
This is Father's method
This is Father's method
在这里,程序在编译的时候就已经确定使用method(Father father)方法了,不管我们在运行的时候传入的实际类型是什么,它永远都只会执行method(Father father)这个方法。也就是说,java的重载是静态绑定的。
instanceof操作符与转型
有时候,我们希望在使用重载的时候,程序能够根据传入参数的实际类型动态地调用相应的方法,也就是说,我们希望java的重载是动态的,而不是静态的。但是由于java的重载不是动态绑定,我们只能通过程序来人为的判断,我们一般会使用instanceof操作符来进行类型的判断。我们要对method(Father father)进行修改,在方法体中判断运行期间的实际类型,修改后的method(Father father)方法如下:
- public void method(Father father){
- if(father instanceof Son1){
- method((Son1)father);
- }else if(father instanceof Son2){
- method((Son2)father);
- }else if(father instanceof Father){
- System.out.println("This is Father's method");
- }
- }
请注意,我们必须把判断是否是父类的条件(也就是判断是否为Father类的条件)放到最后,否则将一律会被判断为Father类,达不到我们动态判断的目的。修改代码后,程序就可以动态地根据参数的实际类型来调用相应的方法了。运行结果如下:
This is Father's method
This is Son1's method
This is Son2's method
但是这种实现方式有一个明显的缺点,它是伪动态的,仍然需要我们来通过程序来判断类型。假如Father有100个子类的话,还是这样来实现显然是不合适的。必须通过其他更好的方式实现才行,我们可以使用双分派方式来实现动态绑定。
用双分派实现动态绑定
首先,什么是双分派?还记得23种设计模式(9):访问者模式中一开始举的例子吗?
类A中的方法method1和method2的区别就是,method2是双分派。我们可以看一下java双分派的特点:首先要有一个访问类B,类B提供一个showA(A a) 方法,在方法中,调用类A的method1方法,然后类A的method2方法中调用类B的showA方法并将自己作为参数传给showA。双分派的核心就是这个this对象。说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改,代码如下:
- class Father {
- public void accept(Execute exe){
- exe.method(this);
- }
- }
- class Son1 extends Father{
- public void accept(Execute exe){
- exe.method(this);
- }
- }
- class Son2 extends Father{
- public void accept(Execute exe){
- exe.method(this);
- }
- }
- class Execute {
- public void method(Father father){
- System.out.println("This is Father's method");
- }
- public void method(Son1 son){
- System.out.println("This is Son1's method");
- }
- public void method(Son2 son){
- System.out.println("This is Son2's method");
- }
- }
- public class Test {
- public static void main(String[] args){
- Father father = new Father();
- Father s1 = new Son1();
- Father s2 = new Son2();
- Execute exe = new Execute();
- father.accept(exe);
- s1.accept(exe);
- s2.accept(exe);
- }
- }
可以看到我们修改的地方,在Father,Son1,Son2中分别加入一个双分派的方法。调用的时候,原本是调用Execute的method方法,现在改为调用Father的accept方法。运行结果如下:
This is Father's method
This is Son1's method
This is Son2's method
运行结果符合我们的预期,实现了动态绑定。双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了,与使用instanceof操作符的效果是一样的(用instanceof操作符可以实现重载方法动态绑定的原因也是因为instanceof操作符是动态的)。但是与使用instanceof操作符实现动态绑定相比,双分派方式的可扩展性要好的多。
相关推荐
- **访问者模式**(Visitor):定义一个作用于某对象结构中的各元素的操作。 ### 使用设计模式的实际例子 以**组合模式**为例,假设我们需要设计一个系统来管理各种类型的电脑产品。我们可以定义一个抽象的`...
设计模式 的分类 总体来说设计模式分为三大类: 创建型模式(5): ...策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
本文将重点介绍标题中提到的五种设计模式:建造者模式、代理模式、观察者模式、策略模式以及状态模式。 **1. 建造者模式(Builder)** 建造者模式是一种创建型设计模式,它允许我们分步骤构建复杂对象,而无需暴露...
GOF(GoF)23种设计模式,是由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides四位大神在他们的著作《设计模式:可复用面向对象软件的基础》中提出的,这些模式分为创建型、结构型和行为型三大类。...
设计模式主要分为三大类: 1.创建型模式:工厂模式、抽象...4.行为型模式:模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
设计模式分为创建型、结构型和行为型三类,总共包含23种模式。在C++中实现这些模式,不仅可以提升代码的复用性,还能提高软件的质量和可维护性。 创建型模式包括: - Factory模式:定义一个用于创建对象的接口,让...
《C++ 23种设计模式1》是关于软件工程中设计模式的深入解析,主要聚焦于C++语言的实现。设计模式是经过时间和实践验证的解决方案,它们针对常见的编程问题提供了一套标准的模板,使得开发者能够更高效地编写可复用、...
《C# 23种设计模式》是一本深入解析C#编程中常用设计模式的权威指南,对于想要提升代码质量、提高软件可维护性的开发者来说,是不可或缺的参考资料。书中详细介绍了23种经典的设计模式,这些模式是软件工程实践中...
- 访问者模式:定义一个访问者接口,用来访问聚合对象的元素,同时给这些元素增加新的操作而不影响其结构。 这23种设计模式是软件开发中的宝贵财富,学习和理解它们能帮助开发者写出更高效、易于维护的代码。在...
JAVA 设计模式可以分为三种:创建模式、结构模式和行为模式。 1. 创建模式 创建模式是指在创建对象时使用的模式,包括 Factory(工厂模式)、Singleton(单例模式)、Builder(建造者模式)、Prototype(原型模式...
在Java编程中,有23种经典的GoF(Gang of Four)设计模式,它们被分为三大类:创建型、结构型和行为型。本资源集合了这些模式的详细解释与源码分析,旨在帮助开发者深入理解和应用设计模式。 1. 创建型模式...
20. 访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作,使得可以在不改变该对象结构的前提下定义新的操作。 21. 状态模式(State Pattern):允许一个对象在其内部状态改变时改变其行为,...
包括责任链模式、命令模式、解释器模式、迭代器模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式。它们可以帮助我们更好地管理复杂系统的动态行为。 每一种设计模式都有其特定的适用...
17. **访问者模式**:在不改变集合元素的前提下,为一个集合中的每个元素增加新的操作。 18. **模态模式**:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被...
访问者模式(Visitor Pattern)是一种行为型设计模式,它允许我们向现有的类层次结构中添加新的功能,而无需修改这些类。该模式通过将这些功能封装成独立的类(即访问者),然后让这些访问者访问并操作这些现有类的...
3. **行为型模式**(Behavioral Patterns):关注对象之间的责任分配和通信,包括策略模式(Strategy)、模板方法模式(Template Method)、迭代器模式(Iterator)、观察者模式(Observer)、访问者模式(Visitor)...
java的设计模式大体上分为三大类: ...行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
- **访问者模式**:在不改变集合元素的前提下,为集合中的元素增加新的操作。 - **备忘录模式**:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复对象的先前状态。 -...
java23种设计模式,每一种模式都有详细的讲解,很全面,如果你想深入了解一下java23种设计模式,这会非常适合你的哦!上传乃为百度云连接,失效请留言。 内容: 001策略模式! N. B8 ~' D! f9 j+ g0 I 002观察者模式 ...