Java设计模式—— 访问者模式

Posted 小小印z

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java设计模式—— 访问者模式相关的知识,希望对你有一定的参考价值。

        访问模式定义如下:表示一个作用于某对象结构中的个元素的操作。它可以在不改变各个元素的类的前提下,定义作用于这些元素的新操作。

适合访问者模式的情景如下:

  • 相对集合中的对象增加一些新的操作
  • 需要对集合中的对象进行很多不同且不相关的操作,而又不想修改对象的类

一、问题的提出

原功能:

public interface IFunc 
    void func();
    void func2();

class Thing implements IFunc

    @Override
    public void func() 
    
    @Override
    public void func2() 
    

现在想要新增一个功能,就需要在接口中增加方法定义fun3(),然后在实现类中重写该方法。那么能不能不修改IFunc、Thing,也能实现呢,这就需要用到访问者模式。

二、访问者模式

        访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接收这个操作的数据结构就可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。

访问者模式涉及的四种角色:

  • IElement:抽象的事物元素功能接口,定义了固定功能方法及可变功能方法接口。(IShape)
  • Element:具体功能的实现类(Triangle)
  • IVisitor:访问者接口,为所有的访问者对象声明一个 visit() 方法,用来代表为对象结构添加的功能,原则上可以代表任意的功能
  • Visitor:具体访问者实现类,实现要真正被添加到对象结构中的功能

考虑这样一个应用:已知三点坐标,编写功能类,求所围三角形的面积和周长。

 如果采用访问者模式结构,应该这样思考:目前以确定要求的是面积和周长,但之后可能回求三角形的重心、垂心坐标、内切圆半径等,因此在设计的时候需要考虑如何屏蔽这些不确定的情况。

(1)定义抽象需求分析接口IShape

对于方法 accept() 它形式上仅是一个方法,但是对访问者模式而言,它却可以表示将来可以求重心、垂心等功能,是一对多的关系,因此IVisitor一般来说是接口或者抽象类,多项功能一定是由IVisitor的子类实现的

public interface IShape 
    float getArea();
    float getLength();
    Object accept(IVisitor visitor);

(2)定义具体功能实现类

public class Triangle implements IShape
    float x1, y1, x2, y2, x3, y3;

    public Triangle(float x1, float y1, float x2, float y2, float x3, float y3) 
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.x3 = x3;
        this.y3 = y3;
    
    //求任意两点的距离
    public float getDist(float u1, float v1, float u2, float v2) 
        return (float) Math.sqrt((u1-u2)*(u1-u2) + (v1-v2)*(v1-v2));
    

    @Override
    public float getArea() 
        float a = getDist(x1,y1,x2,y2);
        float b = getDist(x1,y1,x3,y3);
        float c = getDist(x2,y2,x3,y3);
        float s = (a+b+c)/2;
        return (float) Math.sqrt(s*(s-a)*(s-b)*(s-c));
    

    @Override
    public float getLength() 
        float a = getDist(x1,y1,x2,y2);
        float b = getDist(x1,y1,x3,y3);
        float c = getDist(x2,y2,x3,y3);
        return a+b+c;
    

    @Override
    public Object accept(IVisitor visitor) 
        return visitor.visit(this);
    

(3)定义访问者接口

public interface IVisitor 
    Object visit(Triangle triangle);

这时如果需求发生了变化,基础功能类不用变化,只要定于IVisitor接口的具体功能实现类就可以了。 

例如新需求:求重心坐标 

(4)定义重心坐标实现类

public class Point 
    float x,y;
public class CenterVisitor implements IVisitor
    @Override
    public Object visit(Triangle triangle) 
        Point point = new Point();
        point.x = (triangle.x1 + triangle.x2 + triangle.x3)/3;
        point.y = (triangle.y1 + triangle.y2 + triangle.y3)/3;
        return point;
    

(5)简单测试类

public class Test 
    public static void main(String[] args) 
        IVisitor visitor = new CenterVisitor();
        Triangle triangle = new Triangle(0,0,2,0,0,2);
        //通过访问者对象求新的需求:求重心坐标
        Point point = (Point) triangle.accept(visitor);
        System.out.println(point.x+"\\t"+point.y);
        System.out.println("面积:"+triangle.getArea());
        System.out.println("周长:"+triangle.getLength());
    

测试结果:

0.6666667	0.6666667
面积:2.0000005
周长:6.8284273

总结:

        总体思路就是在 IShape 接口中定义一个可扩展需求的方法 accept() ,在其接口实现类 Triangle 类中重写这个方法,该方法接收一个 IVisitor 的对象,并调用该对象的 visit() 方法处理需求。也就是说accept()方法接收IVistor对象,具体的实现就在该IVistor对象中进行。

        在IVisitor对象中,接收一个 Triangle 对象,拿到Triangle对象的三条边,然后计算出重心的坐标并返回。

        所以当我们再有新的需求的时候,例如求三角形外接圆半径,只需要再定义一个新类实现IVisitor接口,在该类中完成求外接圆半径的功能即可。

新增需求:求外接圆半径

直接新增一个 IVisitor 的实现类 BanJIngVisitor 

public class BanJIngVisitor implements IVisitor
    @Override
    public Object visit(Triangle triangle) 
        //得到三边长
        double a = Math.sqrt(Math.pow(triangle.x2 - triangle.x1, 2) + Math.pow(triangle.y2 - triangle.y1, 2));
        double b = Math.sqrt(Math.pow(triangle.x3 - triangle.x2, 2) + Math.pow(triangle.y3 - triangle.y2, 2));
        double c = Math.sqrt(Math.pow(triangle.x1 - triangle.x3, 2) + Math.pow(triangle.y1 - triangle.y3, 2));
        //求半周长
        double s = (a + b + c) / 2;
        //求面积
        double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));
        //求外接圆半径
        double radius = (a * b * c) / (4 * area);
        return radius;
    

测试:

public class Test 
    public static void main(String[] args) 
        IVisitor visitor = new CenterVisitor();
        Triangle triangle = new Triangle(0,0,2,0,0,2);
        //通过访问者对象求新的需求:求重心坐标
        Point point = (Point) triangle.accept(visitor);
        System.out.println(point.x+"\\t"+point.y);
        System.out.println("面积:"+triangle.getArea());
        System.out.println("周长:"+triangle.getLength());

        //求外接圆半径
        IVisitor visitor1 = new BanJIngVisitor();
        double radius = (double) triangle.accept(visitor1);
        System.out.println("外接圆半径:"+ radius);
    

结果:

0.6666667	0.6666667
面积:2.0000005
周长:6.8284273
外接圆半径:1.4142135623730956

java设计模式之访问者模式

访问者模式

访问者模式概述

定义:

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

访问者模式类图

访问者模式结构

访问者模式包含以下主要角色:

  • 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
  • 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

访问者模式案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

类图如下:

代码如下:

创建抽象访问者接口

public interface Person 
    void feed(Cat cat);

    void feed(Dog dog);

创建不同的具体访问者角色(主人和其他人),都需要实现 Person接口

public class Owner implements Person 

    @Override
    public void feed(Cat cat) 
        System.out.println("主人喂食猫");
    

    @Override
    public void feed(Dog dog) 
        System.out.println("主人喂食狗");
    


public class Someone implements Person 
    @Override
    public void feed(Cat cat) 
        System.out.println("其他人喂食猫");
    

    @Override
    public void feed(Dog dog) 
        System.out.println("其他人喂食狗");
    

定义抽象节点 – 宠物

public interface Animal 
    void accept(Person person);

定义实现Animal接口的 具体节点(元素)

public class Dog implements Animal 

    @Override
    public void accept(Person person) 
        person.feed(this);
        System.out.println("好好吃,汪汪汪!!!");
    


public class Cat implements Animal 

    @Override
    public void accept(Person person) 
        person.feed(this);
        System.out.println("好好吃,喵喵喵!!!");
    

定义对象结构,此案例中就是主人的家

public class Home 
    private List<Animal> nodeList = new ArrayList<Animal>();

    public void action(Person person) 
        for (Animal node : nodeList) 
            node.accept(person);
        
    

    //添加操作
    public void add(Animal animal) 
        nodeList.add(animal);
    


测试类

public class Client 
    public static void main(String[] args) 
        Home home = new Home();
        home.add(new Dog());
        home.add(new Cat());

        Owner owner = new Owner();
        home.action(owner);

        Someone someone = new Someone();
        home.action(someone);
    

访问者模式的优缺点

1,优点:

  • 扩展性好

    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好

    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为

    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

2,缺点:

  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则

    访问者模式依赖了具体类,而没有依赖抽象类。

访问者模式的使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。

  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

访问者模式扩展

访问者模式用到了一种双分派的技术。

1,分派:

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

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 a = new Dog();
        a.execute();
        
        Animal a1 = new Cat();
        a1.execute();
    

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。

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);
    

运行结果:

这个结果可能出乎一些人的意料了,为什么呢?

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

4,双分派:

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

public class Animal 
    public void accept(Execute exe) 
        exe.execute(this);
    


public class Dog extends Animal 
    public void accept(Execute exe) 
        exe.execute(this);
    


public class Cat extends Animal 
    public void accept(Execute exe) 
        exe.execute(this);
    


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 d = new Dog();
        Animal c = new Cat();

        Execute exe = new Execute();
        a.accept(exe);
        d.accept(exe);
        c.accept(exe);
    

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。

运行结果如下:

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。

以上是关于Java设计模式—— 访问者模式的主要内容,如果未能解决你的问题,请参考以下文章

java设计模式之访问者模式

java设计模式之访问者模式

Java 设计模式系列(二三)访问者模式(Vistor)

Java单体应用 - 架构模式 - 03.设计模式-25.访问者模式

每天一个java设计模式(二十三) - 访问者模式

java 流程式代码适合啥设计模式