Java设计模式百例 - 访问者模式

本文源码见:https://github.com/get-set/get-designpatterns/tree/master/visitor

在访问者模式(Visitor Pattern)中,通过一个访问者类,来封装对数据结构中不同类型元素的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。

例子

我们假设有一些形状,包括三角形、矩形和圆形这三种不同的几何形状。我们知道不同的形状其参数是不同的:

  • 三角形:三条边的长度确定一个三角形;
  • 矩形:长和宽确定一个矩形;
  • 圆形就一个参数——半径。

那么任务来了,我们把一系列类型和参数不同的形状用ArrayList来管理,然后依次遍历,并计算出它们的周长。

一种计算任务

这个任务非常简单,由于用ArrayList来管理,因此需要各个不同形状抽象出统一的接口Shape。这个接口定义一个共同的方法getPerimeter,然后这三种形状都实现这个方法就OK了嘛。其代码如下:

Shape.java

public interface Shape {
    double getPerimeter();
}

Triangle.java(三个属性:三条边的长度)

public class Triangle implements Shape {
    private double edgeA;
    private double edgeB;
    private double edgeC;

    public Triangle(double edgeA, double edgeB, double edgeC) {
        this.edgeA = edgeA;
        this.edgeB = edgeB;
        this.edgeC = edgeC;
    }

    public double getEdgeA() {
        return edgeA;
    }

    public double getEdgeB() {
        return edgeB;
    }

    public double getEdgeC() {
        return edgeC;
    }

    public double getPerimeter() {
        return edgeA + edgeB + edgeC;
    }
}

Rectangle.java(两个属性:长和宽)

public class Rectangle implements Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    public double getLength() {
        return length;
    }

    public double getWidth() {
        return width;
    }

    public double getPerimeter() {
        return (length + width) * 2;
    }
}

Circle.java(一个属性:半径)

public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

齐活儿~ 写个Client交卷了:

Client.java

public class Client {
    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<Shape>();
        shapes.add(new Triangle(1.3, 2.2, 3.1));
        shapes.add(new Circle(1.2));
        shapes.add(new Triangle(2.4, 3.3, 4.2));
        shapes.add(new Ractangle(2.1, 3.2));
        shapes.add(new Circle(5.6));
        
        for (Shape shape : shapes) {
            System.out.println(shape.getPerimeter());
        }
    }
}

这是面向接口编程的最基本用法了吧。不过别忙着高兴,如果这个时候再加一个任务——“求面积”呢?那就在接口里再增加一个getArea方法,然后所有的形状都实现了呗~

好吧,也可以,不过任务还远没有结束,如果还要算出能够包住每个形状的最小的圆的直径呢?以及能够置于形状内的最大的圆的直径呢?等等等等。。。

每次提出新的计算任务后,频繁修改接口及其实现类,这显然是不符合“开闭”原则的,而且明显也是不优雅的。况且,天知道将来还会需要计算什么幺蛾子!

多种计算任务,策略模式

必须要调整一下设计思路以满足灵活性。

我们曾经遇到过对于同一个对象进行不同运算的设计——比如策略模式,无论是计算周长、面积都是不同的策略,我们将不同的策略作为对象传递给形状,就可以得到该策略相应的结果。比如对于矩形:

// 对于矩形,在Rectangle.java
public double accept(Strategy strategy) {
    return strategy.calculate(this);
}

// 所有的策略都实现统一的接口 Strategy
public interface Strategy {
    double calculate(Rectangle rect);
}

// 对于计算周长的策略,在PerimeterStrategy.java
public class PerimeterStrategy implements Strategy {
    public double calculate(Rectangle rect) {
        return 2 * (rect.getLength() + rect.getWidth());
    }
}

// 对于计算面积的策略,在AreaStrategy.java
public class AreaStrategy implements Strategy {
    public double calculate(Rectangle rect) {
        return rect.getLength() * rect.getWidth();
    }
}

不同类型对象的多种计算任务,访问者模式

上边的例子是针对矩形的,那么三角形和圆形也都实现相应的策略类的话,类的数量就很快增长起来了,似乎也不优雅嘛! 其实很简单,因为无论是周长策略还是面积策略,各个形状都要实现,那对于同一种策略打个包不就OK了吗:

Calculator.java

public interface Calculator {
    double ofShape(Triangle triangle);
    double ofShape(Circle circle);
    double ofShape(Square square);
}

Perimeter.java(各种形状周长策略的打包)

public class Perimeter implements Calculator {
    public double ofShape(Triangle triangle) {
        return triangle.getEdgeA() + triangle.getEdgeB() + triangle.getEdgeC();
    }

    public double ofShape(Circle circle) {
        return circle.getRadius() * Math.PI * 2;
    }

    public double ofShape(Square square) {
        return square.getEdge() * 4;
    }
}

Area.java(各种形状面积策略的打包)

public class Area implements Calculator {
    public double ofShape(Triangle triangle) {
        double a = triangle.getEdgeA(), b = triangle.getEdgeB(), c = triangle.getEdgeC();
        double p = (a + b + c) / 2;
        return Math.sqrt(p * (p - a) *  (p - b) * (p - c));
    }

    public double ofShape(Circle circle) {
        return Math.PI * circle.getRadius() * circle.getRadius();
    }

    public double ofShape(Square square) {
        return Math.pow(square.getEdge(), 2);
    }
}

两种策略的打包都实现自Calculator接口,以后还有啥计算需求,起个类实现这个接口就可以了。

那对于各个形状,刚才的代码也要稍微调整一下:

Shape.java

public interface Shape {
    // double getPerimeter();
    // 对于不同的计算策略来者不拒
    double accept(Calculator calculator);
}

Triangle.java(三个属性:三条边的长度)

public class Triangle implements Shape {
    private double edgeA;
    private double edgeB;
    private double edgeC;

    public Triangle(double edgeA, double edgeB, double edgeC) {
        this.edgeA = edgeA;
        this.edgeB = edgeB;
        this.edgeC = edgeC;
    }

    public double getEdgeA() {
        return edgeA;
    }

    public double getEdgeB() {
        return edgeB;
    }

    public double getEdgeC() {
        return edgeC;
    }

//    public double getPerimeter() {
//        return edgeA + edgeB + edgeC;
//    }

    // 方法接受策略对象为参数,方法内将自身作为参数再传给策略的方法
    public double accept(Calculator calculator) {
        return calculator.ofShape(this);
    }
}

accept方法中,接受策略对象为参数,方法内将自身作为参数再传给策略的具体方法,这种方式叫做“双重分派”,高大上的名字往往不好记也不好理解,哈哈,其实不记也罢,通过这种巧妙的回调方式实现不同策略对不同类型对象的计算任务。

我们再测试一下:

Client.java

public class Client {
    public static void main(String[] args) {
        // 一个含有5个元素的List,包含三种不同的形状
        List<Shape> shapes = new ArrayList<Shape>();
        shapes.add(new Triangle(1.3, 2.2, 3.1));
        shapes.add(new Circle(1.2));
        shapes.add(new Triangle(2.4, 3.3, 4.2));
        shapes.add(new Rectangle(2.1, 3.2));
        shapes.add(new Circle(5.6));

        // 计算周长和面积的不同策略(访问者)
        Perimeter perimeter = new Perimeter();
        Area area = new Area();

        // 将周长和面积的计算策略传入(接受不同访问者的访问)
        for (Shape shape : shapes) {
            System.out.printf("周长 : %5.2f\t 面积 : %5.2f\n", shape.accept(perimeter), shape.accept(area));
        }
    }
}

将不同的策略对象传递给各个元素,从而对不同类型的元素进行不同策略的计算。是不是感觉代码优雅了不少呢_

测试结果:

周长 :  6.60   面积 :  1.20
周长 :  7.54   面积 :  4.52
周长 :  9.90   面积 :  3.95
周长 : 10.60   面积 :  6.72
周长 : 35.19   面积 : 98.52

总结

上边的例子就是应用了访问者模式。为啥叫访问者模式呢?

其实例子中的策略就相当于依次访问各个元素的访问者,每个元素可以接受(accept)不同访问者作为参数,从而交由访问者做出不同的操作。

我们也可以看出访问者模式的应用场景具有如下特点:

  • 通常用于处理数据结构中不同类型元素的遍历处理问题。这里所说的数据结构比如例子中的列表,或者数组、Map、Stack、Set,甚至复杂的树,重点不在于数据结构,而在于不同类型的元素放到一起,要“因材施教”。
  • 即使对于每种类型的元素,也有不同的“访问方式”,将不同的“访问方式”作为不同的对象传递给元素。“访问者”相当于对各种类型的元素的同一种“访问方式”的打包。
  • 使用到了双重分派,在accept方法中,接受策略对象为参数,方法内将自身作为参数再传给策略的具体方法。

所以,这是一个 m x n 的问题,多种元素对应多种“访问方式”。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 转自:http://blog.csdn.net/jackfrued/article/details/4492194...
    王帅199207阅读 8,498评论 3 93
  • 知识太确定,往往不适合每个人的 环境。而自己在具体环境中实践得到的知识和教训,用以指导下一步的行动,往往很有用。虽...
    昔时横波目阅读 493评论 0 0
  • 关键词:问 1、你认为什么样的人是销售精英?你觉得销售精英应该具备什么样的心态? 买好货、收好款;销售的产品不一样...
    启厦阅读 190评论 0 0