行为模式之访问者模式

Posted by Night Field's Blog on May 31, 2020

1 概述

访问者模式(Visitor Pattern)是一种行为模式,不常用。它可以将作用在对象上的算法逻辑,与对象本身分离开来。

2 访问者模式

当需要对一组相似类型的对象执行操作时,我们可以将操作逻辑分别维护在每个对象内部,但这违背了单一职责原则访问者模式就是来应对这种情况的:将所有的算法逻辑移动到一个新的类—-访问者(Visitor)中,统一维护,如果其中的逻辑发生了变化,那么我们只需要在访问者实现中进行更改,而不用影响到原对象。同时,在访问者模式中,扩展变得很容易,增加新的对象以及操作逻辑,只需要在访问者中做添加即可。

3 案例

来看一个例子。购物结算时,需要统计所有商品的价格,同时还要考虑到商品的折扣,不同的商品,优惠政策也不一样。我们用访问者模式,来统一处理结算的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public interface Visitable {
    int accept(Visitor visitor);
}
public interface Fruit extends Visitable {
    int getPricePerKg();
    int getWeight();
}
// 水果类只需要维护自身的属性如单价,重量等信息,无需关心结算方式
public class Apple implements Fruit {
    private int pricePerKg;
    private int weight;
    public Apple(int pricePerKg, int weight) {
        this.pricePerKg = pricePerKg;
        this.weight = weight;
    }
    @Override
    public int getPricePerKg() {
        return pricePerKg;
    }
    @Override
    public int getWeight() {
        return weight;
    }
    @Override
    public int accept(Visitor visitor) {
        return visitor.visit(this);
    }
}
public class Orange implements Fruit {
    private int pricePerKg;
    private int weight;
    public Orange(int pricePerKg, int weight) {
        this.pricePerKg = pricePerKg;
        this.weight = weight;
    }
    @Override
    public int getPricePerKg() {
        return pricePerKg;
    }
    @Override
    public int getWeight() {
        return weight;
    }
    @Override
    public int accept(Visitor visitor) {
        return visitor.visit(this);
    }
}
public class Banana implements Fruit {
    private int pricePerKg;
    private int weight;
    public Banana(int pricePerKg, int weight) {
        this.pricePerKg = pricePerKg;
        this.weight = weight;
    }
    @Override
    public int getPricePerKg() {
        return pricePerKg;
    }
    @Override
    public int getWeight() {
        return weight;
    }
    @Override
    public int accept(Visitor visitor) {
        return visitor.visit(this);
    }
}

public interface Visitor {
    int getTotalCost(Fruit... fruits);
    int visit(Apple apple);
    int visit(Orange orange);
    int visit(Banana banana);
}
// 访问者类维护具体的算法逻辑
public class FruitVisitor implements Visitor {
    @Override
    public int getTotalCost(Fruit... fruits) {
        int cost = 0;
        for (Fruit fruit : fruits) {
            cost += fruit.accept(this);
        }
        return cost;
    }
    @Override
    public int visit(Apple apple) {
        // 苹果打八折
        int pricePerKg = apple.getPricePerKg();
        if (pricePerKg > 10) {
            pricePerKg *= 0.8;
        }
        int cost = pricePerKg * apple.getWeight();
        System.out.println(apple.getWeight() + "kg apples costs $" + cost);
        return cost;
    }
    @Override
    public int visit(Orange orange) {
        // 橘子满2千克,单价减2元
        int pricePerKg = orange.getPricePerKg();
        if (orange.getWeight() > 2) {
            pricePerKg -= pricePerKg - 2;
        }
        int cost = pricePerKg * orange.getWeight();
        System.out.println(orange.getWeight() + "kg oranges costs $" + cost);
        return cost;
    }
    @Override
    public int visit(Banana banana) {
        // 香蕉没有折扣
        int cost = banana.getPricePerKg() * banana.getWeight();
        System.out.println(banana.getWeight() + "kg bananas costs $" + cost);
        return cost;
    }
}

输出:

1
2
3
4
1kg apples costs $9
3kg oranges costs $6
2kg bananas costs $16
Total cost: 31

通过将getTotalCost()的逻辑从Fruit中抽离出来,放到独立的类Visitor中单独维护,使得代码满足了单一职责原则。对于折扣算法的修改,都不会影响到原有的Fruit对象,达到了对象与算法解耦的目的。 JDK中,一般以Visitor结尾的类,都运用了访问者模式,如FileVisitorAnnotationValueVisitor

4 总结

访问者模式可以让对象与算法逻辑分离,使程序更易于修改和扩展。当然,缺点是访问者接口的实现过多,会使得访问者变得很庞杂。虽然这种模式在实际中不多见,但在合适的场景中,恰当地运用可以有效降低系统复杂度。

文中例子的github地址