zoukankan      html  css  js  c++  java
  • 【趣味设计模式系列】之【访问者模式】

    1. 简介

    访问者模式(Visitor Pattern):表示一个作用在某对象结构中的元素的操作,它可以在不改变类的元素的前提下,定义作用于这些元素的新操作。这是《设计模式-可复用面向对象软件的基础》中的定义。换句通俗的话,就是类的结构元素不变,可以根据访问者重新定义元素的操作

    2. 示例

    2.1 水果套餐例子

    假如有个水果套餐,是苹果、香蕉、橘子的组合,套餐内的水果种类一般不改变,需要对该购买套餐的消费者实行优惠,个人总价打9折,公司团购打8折,要求在不改变原有套餐内部元素与内部方法的情况下,根据外部访问者的变化,重新定义新的价格算法,如图所示

    类图如下:

    FruitPackage水果接口,接受访问者的方法accept,计算价格的方法getPrice

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:16
     * @Desc: 水果套餐
     */
    interface FruitPackage {
        void accept(Visitor v);
        double getPrice();
    }
    

    套餐内的每个元素苹果、橘子、香蕉分别实现水果套餐接口FruitPackage,内部accept方法各种去访问对应的元素,并把当前元素的实例this传进去,Apple类

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:16
     * @Desc: 苹果
     */
    public class Apple implements FruitPackage {
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visitApple(this);
        }
    
        @Override
        public double getPrice() {
            return 30;
        }
    }
    
    

    Orange类

    package com.wzj.visitor.example1;
    
    import com.wzj.proxy.v8.Discount;
    
    /**
     * @Author: wzj
     * @Date: 2020/8/4 20:45
     * @Desc: 橘子
     */
    public class Orange implements FruitPackage {
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visitOrange(this);
        }
    
        @Override
        public double getPrice() {
            return 50;
        }
    }
    
    

    Banana类

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:16
     * @Desc: 香蕉
     */
    public class Banana implements FruitPackage {
    
        @Override
        public void accept(Visitor visitor) {
            visitor.visitBanana(this);
        }
    
        @Override
        public double getPrice() {
            return 40;
        }
    }
    
    

    定义访问者接口,内部依次定义访问每个元素的方法

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:14
     * @Desc: 访问者接口
     */
    public interface Visitor {
    
        void visitApple(Apple apple);
    
        void visitOrange(Orange orange);
    
        void visitBanana(Banana banana);
    
    }
    
    

    个人访问者PersonelVisitor类

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:40
     * @Desc: 个人访问者--一律9折
     */
    public class PersonelVisitor implements Visitor{
    
        double totalPrice = 0.0;
    
    
        @Override
        public void visitApple(Apple apple) {
            totalPrice += apple.getPrice() * 0.9;
        }
    
        @Override
        public void visitOrange(Orange orange) {
            totalPrice += orange.getPrice() * 0.9;
        }
    
        @Override
        public void visitBanana(Banana banana) {
            totalPrice += banana.getPrice() * 0.9;
        }
    }
    
    

    团购访问者GroupVisitor类

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:41
     * @Desc:  团购访问者--一律8折
     */
    public class GroupVisitor implements Visitor{
    
        double totalPrice = 0.0;
    
    
        @Override
        public void visitApple(Apple apple) {
            totalPrice += apple.getPrice() * 0.8;
        }
    
        @Override
        public void visitOrange(Orange orange) {
            totalPrice += orange.getPrice() * 0.8;
        }
    
        @Override
        public void visitBanana(Banana banana) {
            totalPrice += banana.getPrice() * 0.8;
        }
    }
    
    

    具体水果套餐类ConcretePackage,把包含三个水果的元素组装起来

    package com.wzj.visitor.example1;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:51
     * @Desc: 具体套餐,苹果、香蕉、橘子
     */
    public class ConcretePackage implements FruitPackage{
        Apple apple;
        Orange orange;
        Banana banana;
    
        public ConcretePackage(Apple apple, Orange orange, Banana banana) {
            this.apple = apple;
            this.orange = orange;
            this.banana = banana;
        }
    
        public void accept(Visitor visitor) {
            this.apple.accept(visitor);
            this.orange.accept(visitor);
            this.banana.accept(visitor);
        }
    
        @Override
        public double getPrice() {
            return apple.getPrice() + orange.getPrice() + banana.getPrice();
        }
    }
    
    

    客户端类Client

    package com.wzj.visitor.example1;
    
    import org.aspectj.weaver.ast.Or;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 20:57
     * @Desc:
     */
    public class Client {
        public static void main(String[] args) {
            Apple apple = new Apple();
            Orange orange = new Orange();
            Banana banana = new Banana();
            //个人套餐价格
            PersonelVisitor p = new PersonelVisitor();
            new ConcretePackage(apple, orange, banana).accept(p);
            System.out.println("个人套餐价格:" + p.totalPrice);
            //公司套餐价格
            GroupVisitor g = new GroupVisitor();
            new ConcretePackage(apple, orange, banana).accept(g);
            System.out.println("公司套餐价格:" + g.totalPrice);
        }
    }
    
    

    最后运行结果

    个人套餐价格:108.0
    公司套餐价格:96.0
    

    2.2 台式机组装例子

    假如有个台式机组装,为简化起见,是组装元素由固定的三部分组成,CPU、内存条、主板,现对个人来访者总价打9折,公司团购来访者总价打8折,要求在不改变原有套餐内部元素与内部方法的情况下,根据外部访问者的变化,重新定义新的价格算法,如图所示

    类图设计

    电脑部件类ComputerPart

    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:15
     * @Desc: 电脑部件
     */
    public interface ComputerPart {
    
        void accept(Visitor v);
        double getPrice();
    }
    
    

    CPU类、Memory类、Board类如下:

    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:13
     * @Desc: CPU
     */
    public class CPU implements ComputerPart{
        @Override
        public void accept(Visitor v) {
            v.visitCpu(this);
        }
    
        @Override
        public double getPrice() {
            return 1000;
        }
    }
    
    
    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:13
     * @Desc: 内存条
     */
    public class Memory implements ComputerPart{
        @Override
        public void accept(Visitor v) {
            v.visitMemory(this);
        }
    
        @Override
        public double getPrice() {
            return 500;
        }
    }
    
    
    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:13
     * @Desc: CPU
     */
    public class Board implements ComputerPart{
        @Override
        public void accept(Visitor v) {
            v.visitBoard(this);
        }
    
        @Override
        public double getPrice() {
            return 800;
        }
    }
    
    

    个人访问者PersonelVisitor类

    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:21
     * @Desc: 个人购买9折
     */
    public class PersonelVisitor implements Visitor {
        double totalPrice = 0.0;
    
        @Override
        public void visitCpu(CPU cpu) {
            totalPrice += cpu.getPrice() * 0.9;
        }
    
        @Override
        public void visitMemory(Memory memory) {
            totalPrice += memory.getPrice() * 0.9;
        }
    
        @Override
        public void visitBoard(Board board) {
            totalPrice += board.getPrice() * 0.9;
        }
    }
    
    

    团购访问者GroupVisitor类

    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:21
     * @Desc:  公司团购8折
     */
    public class GroupVisitor implements Visitor {
        double totalPrice = 0.0;
    
        @Override
        public void visitCpu(CPU cpu) {
            totalPrice += cpu.getPrice() * 0.8;
        }
    
        @Override
        public void visitMemory(Memory memory) {
            totalPrice += memory.getPrice() * 0.8;
        }
    
        @Override
        public void visitBoard(Board board) {
            totalPrice += board.getPrice() * 0.8;
        }
    }
    
    

    电脑类Computer类

    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:24
     * @Desc:  电脑
     */
    public class Computer {
    
        CPU cpu;
        Memory memory;
        Board board;
    
        public Computer(CPU cpu, Memory memory, Board board) {
            this.cpu = cpu;
            this.memory = memory;
            this.board = board;
        }
    
        public void acccept(Visitor v) {
            this.cpu.accept(v);
            this.memory.accept(v);
            this.board.accept(v);
        }
    }
    
    

    客户端Client类

    package com.wzj.visitor.example2;
    
    /**
     * @Author: wzj
     * @Date: 2020/9/2 21:24
     * @Desc:
     */
    public class Client {
        public static void main(String[] args) {
            CPU cpu = new CPU();
            Memory memory = new Memory();
            Board board = new Board();
            PersonelVisitor p = new PersonelVisitor();
            new Computer(cpu, memory, board).acccept(p);
            System.out.println("个人套餐价格:" + p.totalPrice);
            GroupVisitor g = new GroupVisitor();
            new Computer(cpu, memory, board).acccept(g);
            System.out.println("公司套餐价格:" + g.totalPrice);
        }
    }
    
    

    结果:

    个人套餐价格:2070.0
    公司套餐价格:1840.0
    

    3. 应用场景分析

    访问者模式一般用在特定的场景中,在经典的四人帮写的【设计模式】一书中,举了编译器的例子,如果需要将源程序表示一个抽象语法树,编译器需要对抽象语法树实施某些操作,如类型检查、代码优化、优化格式打印,大多数操作对于不同节点进行不同处理,但是对于编译器来说,节点类的集合对于给定的语言,内部结构很少变化,如下图

    编译器对使用访问者对程序进行类型检查,它将创建一个TypeCheckingVisitor对象,并以这个对象为传入参数,在抽象语法树上调用accept方法,每一个节点在实现accept方法时会回调访问者:一个赋值节点AssignmentNode对象会回调visitAssignment(this)方法,一个变量引用节点VariableRefNode对象会调用visitVariableRef(this)方法。

    笔者在【趣味设计模式系列】之【代理模式4--ASM框架解析】里面分析的ASM框架也是运用的访问者模式,几个核心类的关系如下图

    成员变量节点FieldNode类、方法节点MethodNode类,拥有接收访问者的方法accept,通过传入的具体的访问者ClassAdapter类或ClassWriter类,方法内部各自调用具体访问者访问自己部件的方法,对应途中的访问属性visitField,访问方法visitMethod。

    4. 总结

    4.1 双分派技术

    讲到访问者模式,大部分书籍或者资料都会讲到 Double Dispatch,中文翻译为双分派。为什么支持双分派的语言就不需要访问者模式。

    既然有 Double Dispatch,对应的就有 Single Dispatch。所谓 Single Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的编译时类型来决定。所谓 Double Dispatch,指的是执行哪个对象的方法,根据对象的运行时类型来决定;执行对象的哪个方法,根据方法参数的运行时类型来决定。

    如何理解“Dispatch”这个单词呢?在面向对象编程语言中,我们可以把方法调用理解为一种消息传递,也就是“Dispatch”。一个对象调用另一个对象的方法,就相当于给它发送一条消息。这条消息起码要包含对象名、方法名、方法参数。如何理解“Single”“Double”这两个单词呢?“Single”“Double”指的是执行哪个对象的哪个方法,跟几个因素的运行时类型有关。我们进一步解释一下。Single Dispatch 之所以称为“Single”,是因为执行哪个对象的哪个方法,只跟“对象”的运行时类型有关。Double Dispatch 之所以称为“Double”,是因为执行哪个对象的哪个方法,跟“对象”和“方法参数”两者的运行时类型有关。

    Java 支持多态特性,代码可以在运行时获得对象的实际类型(也就是前面提到的运行时类型),然后根据实际类型决定调用哪个方法。尽管 Java 支持函数重载,但 Java 设计的函数重载的语法规则是,并不是在运行时,根据传递进函数的参数的实际类型,来决定调用哪个重载函数,而是在编译时,根据传递进函数的参数的声明类型(也就是前面提到的编译时类型),来决定调用哪个重载函数。也就是说,具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟参数的运行时类型无关。所以,Java 语言只支持 Single Dispatch。
    举个例子来具体说明一下,代码如下所示:

    
    public class ParentClass {
      public void f() {
        System.out.println("I am ParentClass's f().");
      }
    }
    
    public class ChildClass extends ParentClass {
      public void f() {
        System.out.println("I am ChildClass's f().");
      }
    }
    
    public class SingleDispatchClass {
      public void polymorphismFunction(ParentClass p) {
        p.f();
      }
    
      public void overloadFunction(ParentClass p) {
        System.out.println("I am overloadFunction(ParentClass p).");
      }
    
      public void overloadFunction(ChildClass c) {
        System.out.println("I am overloadFunction(ChildClass c).");
      }
    }
    
    public class DemoMain {
      public static void main(String[] args) {
        SingleDispatchClass demo = new SingleDispatchClass();
        ParentClass p = new ChildClass();
        demo.polymorphismFunction(p);//执行哪个对象的方法,由对象的实际类型决定
        demo.overloadFunction(p);//执行对象的哪个方法,由参数对象的声明类型决定
      }
    }
    
    //代码执行结果:
    I am ChildClass's f().
    I am overloadFunction(ParentClass p).
    

    这也回答了为什么支持 Double Dispatch 的语言不需要访问者模式。

    访问者模式允许在不改变类的情况下,有效的增加新的操作,这是一种很著名的技术,意味着执行的操作取决于请求的种类与接受者类型,accept方法是一个双分派操作, 取决于visitor类型与Node节点类型,使得访问者可以对每一种类型的请求执行不用的操作。

    4.2 优点

    • 容易增加新的操作
      如果有复杂对象结构,需要增加新的操作,只需要增加新的访问者定义新操作即可。
    • 集中相关操作分离无关操作
      相关行为集中在访问者中,无关行为被分离到各自访问者的关子类中,所有与算法相关的数据结构都被隐藏在访问者中。

    4.3 缺点

    • 具体元素对访问者公布,破坏封装;
    • 访问者依赖具体元素,而非依赖抽象,破坏了依赖倒置原则,导致具体元素的增加、删除、修改比较困难。

    综上,访问者模式一般都用在类似于编译器等比较窄却很专业的场景中,如果自己非要使用,适合类的结构元素不变的情况下,需要重新定义元素操作。


    附:githup源码下载地址:https://github.com/wuzhujun2006/design-patterns

  • 相关阅读:
    一个很实用的css3兼容工具很多属性可以兼容到IE6
    html5 canvas 填充渐变形状
    找到任何形状的中心-总结篇
    html canvas非正方旋转和缩放...写的大多是正方的有人表示一直看正方的看厌了
    把jQuery的类、插件封装成seajs的模块的方法
    那些年实用但被我忘掉javascript属性.onresize
    总有一些实用javascript的元素被人遗忘在角落-slice
    jquery(入门篇)无缝滚动
    html5 canvas旋转+缩放
    今天看到这篇新闻之后,决定休息一下咯
  • 原文地址:https://www.cnblogs.com/father-of-little-pig/p/13603202.html
Copyright © 2011-2022 走看看