设计模式(4)-访问数据结构与简单化

Vistor

简介

在程序中会有很多数据结构,其中保存了许多元素,我们需要对这些元素进行处理。对于一个类来说,访问其中元素的代码通常是放在表示数据结构的类中。但是如果处理有多种的时候,每增加一种处理,都需要再去修改表示数据结构的类。而在Visitor模式中,数据结构与处理被分离开来。我们通过编写一个表示访问者的类来访问数据结构中的元素,将各个元素的处理交给访问者。这样当需要增加新处理的时候,只需要编写新的访问者,然后让数据结构接受访问者的访问即可。

在Vistor模式中有如下一些角色:

  • Vistor:该角色负责对数据结构中的每个具体元素声明一个用于访问某个元素的visit方法,该方法的实现由具体的ConcreteVisitor角色完成
  • ConcreteVistor:该角色负责实现Visitor角色所定义的接口,实现其中所有的visit方法,确定该如何实现不同的访问
  • Element:该角色是Visitor角色的访问对象,其中声明了接受访问者的accept方法。该方法接收一个Visitor角色参数
  • ConcreteElement:该角色负责具体实现Element角色所定义的接口
  • ObjectStructure:ObjectStructur角色负责处理Element角色的集合,由于ConcreteVisitor角色为每个Element角色都准备了处理方法,因此我们可以轻易地实现Element的遍历访问

示例程序

假设我们现在有两个类BookMovie,分别代表书籍和电影,其中分别有不同的属性,我们现在希望取访问这些属性,但是是通过visitor模式进行。首先定义一个Visitor类,这是一个抽象类,其中声明了对应的访问方法,包括对Book以及Movie的访问。

1
2
3
4
5
public abstract class Visitor {
public abstract void visit(Book book);

public abstract void visit(Movie movie);
}

之后我们需要定义一个Element接口,这个接口表示能够在Visitor模式中被visitor访问。这个接口中只有一个accept方法,用于接受对应的Visitor对象,完成访问操作。

1
2
3
public interface Element {
void accept(Visitor visitor);
}

然后我们可以完成Book与Movie这两个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Book implements Element {
private final String name;
private final String author;

public Book(String name, String author) {
this.name = name;
this.author = author;
}

public String getName() {
return name;
}

public String getAuthor() {
return author;
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Movie implements Element {
private final String name;
private final Integer year;

public Movie(String name, Integer year) {
this.name = name;
this.year = year;
}

public String getName() {
return name;
}

public Integer getYear() {
return year;
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

可以看到这里的Book和Movie都实现了Element接口。由于访问操作在Visitor对象中提供,所以这里的accept的实现都是简单的进行方法调用,然后将自身对象this作为参数进行传入即可。

接下来,我们就可以定义Visitor具体的子类,在其中完成对对象具体的访问操作。这里子类的名称为ShowVisitor,意为访问操作是用于展示对象:

1
2
3
4
5
6
7
8
9
10
11
public class ShowVisitor extends Visitor {
@Override
public void visit(Book book) {
System.out.println("now visit one book named " + book.getName() + ", whose author is " + book.getAuthor());
}

@Override
public void visit(Movie movie) {
System.out.println("now visit one movie named " + movie.getName() + ", released in " + movie.getYear());
}
}

至此,整个Visitor模式的逻辑就已经完成了,可以用如下代码进行测试。在测试代码中,不同的对象通过接收相应的Visitor对象,来进行访问操作。可以看到在这个过程中,数据结构与实际的访问操作进行了分离。

1
2
3
4
5
6
7
8
9
@Test
public void test() {
Book book = new Book("book1", "author1");
Movie movie = new Movie("movie1", 2023);
Visitor visitor = new ShowVisitor();

book.accept(visitor);
movie.accept(visitor);
}

说明

Visitor模式的目的是将处理从数据结构中分离出来。数据结构完成的是元素集合等的关联,而保存数据结构和以数据结构为基础进行处理是两种不同的东西,分离之后我们可以更加有条理地完成管理。同时Visitor模式提高了组件的独立性,在增加处理模式的时候我们不需要去修改原先代表数据结构的类,而只需要新增新的Visitor类的实现即可,这样使得功能扩展和修改变得更加有条理,易于管理。关于功能扩展和修改有一个著名的开闭原则。

开闭原则(The Open-Close Principle,OCP),它的核心观点可以用一句话来概括,就是“对扩展开放,对修改关闭”。

对扩展开放指的是在设计类的时候,通常需要考虑到将来可能会扩展类。对修改关闭则是说需要能够在不用修改现有类的前提下进行类的扩展。因为如果每次扩展类的时候都需要修改现有代码,这样就太麻烦了。在不修改现有代码的前提下进行扩展,这就是开闭原则

但是另一方面,由于在Visitor模式中,对数据结构中元素进行处理的任务被交给了Visitor类,这意味着Element角色必须要向Visitor角色公开足够多的信息。访问者只有从数据结构中获取了足够多的信息之后才能工作。在这种意义上,数据结构需要小心地平衡信息的公开以及处理。

Chain of Responsibility

简介

Chain of Responsibility,含义为责任链。直观地说,就是将多个对象组织成一条责任链,当有需求需要进行处理的时候,就从头开始,依次判断是否能够进行处理。对于责任链上的每一个对象来说,它都是从前面收到需求,如果自己能够处理这个需求,那么就进行处理,如果不能处理,就沿着责任链交给下一个对象进行处理,直到没有下一个对象,则表示无法处理这个需求。使用Chain of Responsibitly模式弱化了请求方和处理方之间的关联关系,让各自都成为可独立复用的组件。同时,使用责任链设计模式的程序,它能够应对的需求时可扩展的,我们可以根据情况的不同增加或者修改负责处理的对象。

在责任链设计模式中出现了如下角色:

  • Handler:该角色定义了处理请求的接口,同时持有另一个或者说下一个Handler对象。如果自己无法处理请求,就将请求转给下一对象。向外暴露support方法接口,用于支持问题解决。
  • ConcreteHandler:该角色实现了Handler角色,是处理请求的具体角色。

示例程序

在示例程序中,我们将模拟一条责任链的构成和运行。首先我们构造一个表示问题或者说需求的类Trouble,每个问题有一个编号ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Trouble {
private int number;

public Trouble(int number) {
this.number = number;
}

public int getNumber() {
return number;
}

@Override
public String toString() {
return "[Trouble " + number + "]";
}
}

之后我们定义一个类表示用于解决需求的类Support,这个类是抽象类,其他所有具体的解决需求的类都需要继承该类。在这个抽象类中,有如下几个需要注意的点。首先,每个Support对象内部还会持有一个Support对象,这表示在该对象沿着责任链的下一个对象。之后,我们声明了抽象的resolve方法,它接收一个Trouble对象,这个方法表示该对象应该如何处理对应的Trouble,返回值表示是否成功处理。而Support暴露出来用于支持Trouble处理的方法就是support,这也是整个责任链模式的核心所在。如果当前对象能够处理这个Trouble,就让它进行处理,如果不能,就交给下一个对象进行处理。

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
public abstract class Support {
private String name;
private Support next;

public Support(String name) {
this.name = name;
}

public Support setNext(Support next) { // 设置返回值为Support方便后续的链式调用
this.next = next;
return next;
}

@Override
public String toString() {
return "[" + name + "]";
}

protected abstract boolean resolve(Trouble trouble);

protected void done(Trouble trouble) {
System.out.println(trouble + " is resolved by " + this + ".");
}

protected void fail(Trouble trouble) {
System.out.println(trouble + " cannot be resolved.");
}

public final void support(Trouble trouble) {
if (resolve(trouble)) {
done(trouble);
} else if (this.next != null) {
next.support(trouble);
} else {
fail(trouble);
}
}

}

之后,我们就可以完成Support的实现类了,不同的实现类处理Trouble的逻辑也不同。

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
// 什么问题都不能解决
public class NoSupport extends Support {
public NoSupport(String name) {
super(name);
}

@Override
protected boolean resolve(Trouble trouble) {
return false;
}
}

// 只能解决序号小于limit的问题
public class LimitSupport extends Support {
private final int limit;

public LimitSupport(String name, int limit) {
super(name);
this.limit = limit;
}

@Override
protected boolean resolve(Trouble trouble) {
return trouble.getNumber() < limit;
}
}

// 只能解决奇数序号问题
public class OddSupport extends Support {
public OddSupport(String name) {
super(name);
}

@Override
protected boolean resolve(Trouble trouble) {
return trouble.getNumber() % 2 == 1;
}
}

// 只能解决特定序号问题
public class SpecialSupport extends Support {
private int number;

public SpecialSupport(String name, int number) {
super(name);
this.number = number;
}

@Override
protected boolean resolve(Trouble trouble) {
return trouble.getNumber() == this.number;
}
}

那么最后,我们可以完成一段测试代码来查看对应的效果。在测试代码中我们首先定义了多个Support对象,它们的具体类各有不同,能够处理的Trouble也各不相同。之后我们利用setNext的链式调用构造了一条责任链,最后生成了一系列Trouble,模拟解决过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test() {
Support alice = new NoSupport("Alice");
Support bob = new LimitSupport("Bob", 100);
Support charlie = new SpecialSupport("Charlie", 429);
Support diana = new LimitSupport("Diana",200);
Support elmo = new OddSupport("Elmo");
Support fred = new LimitSupport("Fred", 300);

alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo).setNext(fred);

for (int i = 0; i < 500; i+=33) {
alice.support(new Trouble(i));
}
}

最终的输出效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Trouble 0] is resolved by [Bob].
[Trouble 33] is resolved by [Bob].
[Trouble 66] is resolved by [Bob].
[Trouble 99] is resolved by [Bob].
[Trouble 132] is resolved by [Diana].
[Trouble 165] is resolved by [Diana].
[Trouble 198] is resolved by [Diana].
[Trouble 231] is resolved by [Elmo].
[Trouble 264] is resolved by [Fred].
[Trouble 297] is resolved by [Elmo].
[Trouble 330] cannot be resolved.
[Trouble 363] is resolved by [Elmo].
[Trouble 396] cannot be resolved.
[Trouble 429] is resolved by [Charlie].
[Trouble 462] cannot be resolved.
[Trouble 495] is resolved by [Elmo].

说明

Chain of Responsibility模式的最大优点就是它弱化了发出请求的角色(Client)和处理请求的角色(ConcreteHandler)之间的关系。Client角色只需要向第一个ConcreteHandler角色发出请求,之后请求会在责任链中进行传播,直到某个ConcreteHandler角色处理了该请求。不过也是因为Chain of Responsibility模式一直在推卸责任,直到找到合适的处理请求的对象,这一方面提高了程序的灵活性,另一方面导致了延迟的处理。这是一个需要权衡的问题。实际上,如果请求和处理者之间的对应关系是能够确定的,而且需要非常快的响应时间,此时不使用Chain of Responsibility模式会更好。

Facade

说明

在进行复杂程序开发的时候,我们会涉及到许许多多的类,这些类之间相互关联,导致程序结构变得越来越复杂。在使用这些类之前,我们需要格外注意它们之间的关系,这导致在开发时会增加许多复杂度。而Facade模式就能够帮助我们将这种情况进行简单化。Facade源自一个法语单词,意思为建筑物的正面。使用Facade模式,我们可以将互相关联在一起的错综复杂的类整理出高层API,其中Facade角色让系统对外只有一个简单接口,而内部实际类的关系和调用则由Facade角色来完成。Facade模式本质上就是对一个复杂系统进行包装,向外暴露简单的API,对使用者隐藏内部的复杂逻辑。

Mediator

说明

在一个复杂程序的运行过程中,一种常见的情况是许多不同的类都需要对某种状态进行维护或者更改,根据不同情况做出不同反应。如果将状态控制的代码分散在各个相关类中,会导致使用和调试变得非常困难,而Mediator模式则可以帮助我们简单化这种情况。Mediator意为仲裁者,它来决定程序中如何进行状态的维护以及后续行为的定义。每个相关类发现状态更新,则报告给仲裁者,由仲裁者来决定如何进行下一步的行为。Mediator模式本质上就是将分散的控制逻辑统一集中在一个类中,便于管理和调试。当其他相关类需要使用到其中的某种逻辑时,就通知仲裁者即可。


设计模式(4)-访问数据结构与简单化
http://example.com/2023/04/12/设计模式-4-访问数据结构与简单化/
作者
EverNorif
发布于
2023年4月12日
许可协议