0%

设计模式-访问者模式

这个模式,韩老师讲得非常不好。所以本文整理自各种博文。

1.为什么使用?

当需要对一个对象结构类(这个结构由一些不相关的类组成)进行操作扩展,而又不想改变这个对象结构类时,可以考虑使用访问者模式

举个案例,假设现在我们有一个描述商品的对象结构(想象成购物篮),当前装入了书和水果。那么现在这个结构看起来像这个样子:

现在超市由于促销活动,对大于500页的书,打8折活动;水果每买一斤,单位价格下降1%。于是很自然地想到在Book类和Fruit分别添加getPrice方法和buy3Get1()方法,然后实现具体的操作。如下:

这里我只分别对Book和Fruit做了一个操作扩展,考虑如果我们要对Book和Fruit做多个扩展呢?每个扩展我们都要打开实际的Book类或Fruit类来修改吗?这显然违反了ocp原则,不便于扩展和维护

虽然Book和Fruit都是商品, 但是它们在打折行为上是相互独立的,这种情况我们该如何去扩展呢?下面就来介绍如何使用访问者模式解决这个问题。

2.介绍是什么?

第2部分如果没看懂,没关系,粗略过一下,看完实例再看一遍第2部分应该就懂了。

2.1 基本介绍

访问者设计模式是行为设计模式之一。 当我们必须对一组相似类型但的对象执行操作时,可以使用它。 借助访问者模式,我们可以将操作逻辑从对象移动到另一个类

访问者模式由两部分组成:

  1. Visitor类:visitor类定义visit方法,这个方法将被所有对象结构中的元素调用。
  2. 被访问类,即对象结构中的元素,提供一个accpet方法,接受一个Visitor类。

2.2 类图

职责介绍:

  • Client:Client类是访问者设计模式使用者。 它有权访问对象结构对象,并可以指示它们接受访问者者以执行适当的处理。
  • Visitor:这是一个接口或抽象类,用于声明所有可访问类类型的访问操作。
  • ConcreteVisitor:对于每种类型的访问者,必须实现在所有具体访问方法。 每个访问者将负责不同的操作。
  • Visitable:这是一个声明接受操作的接口。 这是使访问者对象可以“访问”对象的入口点。
  • ConcreteVisitable:这些类实现Visitable接口或类并定义接受操作。 使用accept操作将访问者对象传递给该对象。

OK,大概你已经有点晕了,来看实例吧。

3.如何使用?

我们先给出利用访问者模式解决“为什么要使用”中的不易扩展的问题的类图:

Book和Fruit都是商品,所以抽象出一个父类或接口,作为Visitable(被访问者),声明一个accept。

这里就以接口的方式作为讲解:

被访问者接口:

1
2
3
4
// 作为被访问者的接口
public interface Goods {
void accept(Visitor visitor);
}

访问者接口:

1
2
3
4
public interface Visitor {
void saleCountOnBook(Book book);
void saleCountOnFruit(Fruit fruit);
}

也许你会问,Book和Fruit都是Goods,根据依赖倒转原则,不是应该依赖高层抽象吗。不然的话,每增加一个具体的商品,就会增加一个方法。下文会对这个做解释。

具体的被访问者,Book和Fruit

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
// Book
public class Book implements Goods{

private double price;
private int page;
private String name;

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public int getPage() {
return page;
}

public void setPage(int page) {
this.page = page;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Book() {
}

@Override
public void accept(Visitor visitor) {
// 下面这句话是访问者模式的关键,它将对Book的操作职责转移到了Visitor类
visitor.saleCountOnBook(this);
}

@Override
public String toString() {
return "Book{" +
"price=" + price +
", page=" + page +
", name='" + name + '\'' +
'}';
}
}
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
public class Fruit implements Goods{

private double weight;
private double pricePerWeight;
private String name;

public double getWeight() {
return weight;
}

public void setWeight(double weight) {
this.weight = weight;
}

public double getPricePerWeight() {
return pricePerWeight;
}

public void setPricePerWeight(double pricePerWeight) {
this.pricePerWeight = pricePerWeight;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Fruit() {
}

@Override
public void accept(Visitor visitor) {
// 同Book,操作职责转移
visitor.saleCountOnFruit(this);
}

@Override
public String toString() {
return "Fruit{" +
"weight=" + weight +
", pricePerWeight=" + pricePerWeight +
", name='" + name + '\'' +
'}';
}
}

一定要注意Book在Fruit实现accept的操作,事实上所有的元素(这里就是每个具体的Goods)都应该做类似的实现,将操作这个元素的职责转移给Visitor。

ok,再看看具体的Visitor是如何实现的。这里对上文“Visitor的方法为什么不依赖Goods高层抽象呢“一问题就做了解释。

每种依赖的对象,内部结构可能是不同的,如Book有getPage方法,而Fruit有getPricePerWeight方法。访问者模式的使用要求之一是:对象结构内部元素固定(或尽可能固定),这样Visitor的方法设计一开始就已经制定好了。

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
public class SaleCountVisitor implements Visitor {
/**
* @param book
* 对大于500页的书,打8折活动
*/
@Override
public void saleCountOnBook(Book book) {
double price = book.getPrice();
if(book.getPage() >= 500){
price = price*0.8;
}
book.setPrice(price);
}

/**
* 水果每买一斤,单位价格下降1%
* @param fruit
*/
@Override
public void saleCountOnFruit(Fruit fruit) {
int weight = (int) fruit.getWeight();
double pricePerWeight = fruit.getPricePerWeight();
for(int i =0;i<weight;i++){
pricePerWeight*=0.99;
}
fruit.setPricePerWeight(pricePerWeight);
}
}

也许有人会书,那我就要依赖高层抽象,然后在内部做RTTI判断(也就是instanceof)后做类型强转,这其实也不是不可以,但是这样会违反单一职责原则,当操作过多时,你的方法会充斥着if else做RTTI判断。

Bukcet没什么好说的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Bucket {
private Goods[] goods;

public Bucket(Goods[] goods) {
this.goods = goods;
}

public void saleCount(Visitor visitor){
for(Goods goods: goods){
goods.accept(visitor);
}
}

@Override
public String toString() {
return "Bucket{" +
"goods=" + Arrays.toString(goods) +
'}';
}
}

Client使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Client {
public static void main(String[] args) {
// 定义商品
Book book = new Book();
book.setPrice(100.0);
book.setPage(600);
book.setName("设计模式");
Fruit fruit = new Fruit();
fruit.setPricePerWeight(10);
fruit.setName("苹果");
fruit.setWeight(5);

// 装入篮中
Goods[] goods = new Goods[]{book,fruit};
Bucket bucket = new Bucket(goods); // 其实bucket略有多余,不过不影响

// 执行打折
Visitor visitor = new SaleCountVisitor();
bucket.saleCount(visitor);

// 打折后商品信息
System.out.println(bucket);
}
}

输出:

1
Bucket{goods=[Book{price=80.0, page=600, name='设计模式'}, Fruit{weight=5.0, pricePerWeight=9.509900498999999, name='苹果'}]}

访问者模式就是这样执行的,我个人看来其核心思想就是一种职责转移,将原本应该在具体商品类做的操作,委托到Visitor中执行。这样实现具体商品和逻辑操作的解耦,后续我们要添加任何操作,都和具体商品无关,我们只用定义操作类实现Visitor接口即可。

4.优缺点

4.1 优点

  • 如果操作逻辑发生变化,那么我们只需要在访问者实现中进行更改,而不必在所有项目类中进行更改。(易维护性)
  • 向系统添加新项目很容易,只需在访问者界面和实现中进行更改,现有项目类别将不会受到影响。(可扩展性)

4.2 缺点

  • 在设计时,我们应该知道visit()方法的返回类型,否则我们将不得不更改接口及其所有实现。
  • 如果访问者接口的实现过多,则很难扩展。

5.使用的注意事项

  • 确认当前层次结构(称为元素层次结构)将相当稳定,并且这些类的公共接口足以满足Visitor类所需要的访问。 如果不满足这些条件,则访问者模式则不太适用。
  • 为每个Element派生类型都在Visior类中创建带有visit(ElementXxx)方法。(不要依赖高层抽象)
  • 在元素层次结构中添加一个“ accept(Visitor)”方法。 每个Element派生类中的实现都应该相同-accept(Visitor v){v.visit(this); }
  • Element层次结构仅耦合到Visitor基类,但是Visitor层次结构耦合到每个Element派生类。 如果元素层次结构的稳定性低,而访客层次结构的稳定性高; 则应该考虑交换两个层次结构的“角色”。
  • 为要在Element对象上执行的每个“操作”创建一个Visitor派生类。 visit()实现将依赖于Element的公共接口。.
  • 客户端创建访问者对象,并通过调用accept()将其传递给Element对象。

参考:

  • Visitor pattern
  • Visitor Design Pattern
  • Visitor Design Pattern From Geek
文章对你有帮助?打赏一下作者吧