设计模式(5)-状态管理

Observer

简介

Observer意为观察者。在Observer模式中,当观察对象的状态发生变化的时候,会通知给观察者,而观察者再根据状态进行相应的处理。在Observer模式中有如下角色:

  • Subject:该角色表示被观察的对象。在该角色中主要声明了相关方法,包括注册观察者、删除观察者、获取当前状态等
  • ConcreteSubject:该角色表示具体的被观察对象,当自身状态发生改变之后,它会通知所有已经注册了的Observer角色
  • Observer:该角色表示观察者。在该角色中声明了update方法,用于接收Subject角色状态变化的通知,同时进行相应的处理
  • ConcreteObserver:该角色表示具体的观察者。实现了Observer角色中的相应方法

实际上,Observer角色并非主动地去观察,而是被动地接收来自观察对象的通知。在这种角度上理解,Observer模式可能被称为发布-订阅模式更加合适。由观察对象进行信息的发布,而Observer角色完成信息的订阅

示例程序

在示例程序中,我们考虑这样一个场景,我们首先有一个随机数值生成对象,它作为一个被观察的对象存在。同时我们实现两个不同的观察者,用于观察这个数值生成对象,当数值变化的时候将数值进行显示,当然不同的观察者显示方式不一样。

首先我们完成一个抽象类NumberGenerator,它表示数值生成对象,也是被观察对象。作为数值生成对象,它声明了两个抽象方法,getNumber用于获取当前状态下的数值,execute方法用于执行数值生成。而作为被观察对象,它首先持有一个列表对象,用于保存已经注册了的观察者,同时提供相关的addObserver以及deleteObserver方法,用于观察者的增加和删除。最后还实现了一个notifyObservers方法,用来通知所有观察者状态发生变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class NumberGenerator {
private final List<Observer> observers = new ArrayList<>();

public abstract int getNumber();

public abstract void execute();

public void addObserver(Observer observer) {
observers.add(observer);
}

public void deleteObserver(Observer observer) {
observers.remove(observer);
}

public void notifyObservers() {
for (Observer next : observers) {
next.update(this);
}
}
}

于是我们就可以完成确切的子类RandomNumberGenerator,它继承了NumberGenerator,并且可以完成数值的随机生成。在每次完成数值生成时,都会调用notifyObservers方法,通常所有注册了的Observer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RandomNumberGenerator extends NumberGenerator {
private final Random random = new Random();
private int number;

@Override
public int getNumber() {
return this.number;
}

@Override
public void execute() {
for (int i = 0; i < 20; i++) {
number = random.nextInt(50);
notifyObservers();
}
}
}

接下来我们关注Observer观察者部分。首先实现Observer接口,其中只有一个抽象方法update,用于接收来自被观察者的状态更新。

1
2
3
public interface Observer {
void update(NumberGenerator generator);
}

我们这里实现了Observer的两个不同实现类,其中DigitObserver会将生成的数值打印出来,而GraphObserver会将生成的数值以*的形式进行表示,代码如下:

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 DigitObserver implements Observer {
@Override
public void update(NumberGenerator generator) {
System.out.println("DigitObserver: " + generator.getNumber());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

public class GraphObserver implements Observer {
@Override
public void update(NumberGenerator generator) {
System.out.println("GraphObserver:");
int count = generator.getNumber();
for (int i = 0; i < count; i++) {
System.out.print("*");
}
System.out.println();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

最后,我们就可以完成测试代码,如下所示。程序启动之后,每次数值状态的更改都会通知两个Observer,它们也会相应做出反应。

1
2
3
4
5
6
7
8
9
@Test
public void test() {
RandomNumberGenerator generator = new RandomNumberGenerator();
Observer observer1 = new DigitObserver();
Observer observer2 = new GraphObserver();
generator.addObserver(observer1);
generator.addObserver(observer2);
generator.execute();
}

Memento

简介

在一些情况下,我们需要将对象的状态进行恢复,例如一些带有撤销功能的编辑器。我们需要事先保存实例的相关状态信息,然后在撤销的时候,根据所把保存的信息将实例恢复至原来的状态。想要恢复实例,通常的想法是我们需要能够自由访问实例内部结构的权限,但是这样做的话,容易导致依赖实例内部结构的代码分散在程序的各个部分,难以维护,导致破坏了面向对象中的封装性。而Memento模式就是用来解决这种问题的,Memento有备忘录的意思,它通过引入表示实例状态的角色,来防止在保存和恢复实例时对象的封装性遭到破坏。

在Memento模式中有如下角色:

  • Originator:Originator为生成者角色。该角色会在保存自己最新状态的时候生成Memento角色,同时它也可以接受一个Memento角色,然后将自己恢复成对应的状态
  • Memento:Memento为状态角色。该角色会将Originator角色内部的信息进行整合和保存。不过虽然Memento角色保存了Originator角色的信息,它并不会公开这些信息,或者说公开的信息非常有限。在Memento角色中提供两类接口,分别是宽接口和窄接口。
    • wide interface:宽接口。Memento角色提供的宽接口可以用于获取对象的所有状态信息,由于宽接口能够暴露所有内部信息,因此能够调用的应该只有Originator角色
    • narraw interface:窄接口。Memento角色提供的窄接口可以用于获取对象内部的有限信息,能够被外部角色进行调用
  • Caretaker:Caretaker为负责人,它是一个外部角色。该角色会根据情况来决定是否需要保存当前Originator角色的状态,当需要保存的时候,则会通过Originator角色,Originator角色在接收到通知之后会生成Memento角色并将其返回给Caretake角色。同时Caretaker角色无法访问Memento角色内部的所有信息,它只是将Originator角色生成的Menento角色当作一个黑盒进行保存

利用Memento模式,我们可以实现撤销undo、重做redo、历史记录history、快照snapshot等功能。在这个模式中,尤其需要注意各个角色之间的调用关系以及可见关系。

示例程序

在示例程序中,我们来模拟一个收集水果和获取金钱的投骰子游戏。游戏的主人公通过投骰子来决定下一个状态,根据骰子点数不同,发生的事件也不同,分别可能获取金钱,金钱减半以及获取水果。如果获取了金钱,我们会将当前的状态进行保存,而如果金钱损失太多,我们则会恢复之前的状态。

首先我们创建一个Memento类,用于保存主人公的状态。在这个类中,分别有两个属性,金钱和当前水果。尤其注意这里不同方法的作用范围。构造函数、getFruits方法使用的是默认作用范围,只能被同一个package下的对象访问,而无法被外部访问,这里就对应上面提到的Memento提供的宽API。而getMoney方法使用了public进行修饰,可以被外部访问到,则对应Momento角色提供的窄API。注意这里的宽窄对应的是暴露内部信息的多少,而不是方法作用范围的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Memento {
int money;
ArrayList<String> fruits = new ArrayList<>();

Memento(int money) {
this.money = money;
}

public int getMoney() {
return this.money;
}

void addFruit(String fruit) {
fruits.add(fruit);
}

List<String> getFruits() {
return (List<String>) fruits.clone();
}
}

之后,我们可以完成Originator角色。这里对应的是Gamer类,用于表示游戏主人公。整个游戏的关键运行逻辑都实现在bet方法中。而状态保存和状态恢复的方法则分别对应createMemento()restoreMemento()方法。

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
public class Gamer {
private int money;
private List<String> fruits = new ArrayList<>();
private final Random random = new Random();
private static final String[] fruitsName = {"apple", "grape", "banana", "orange"};

public Gamer(int money) {
this.money = money;
}

public int getMoney() {
return this.money;
}

private String getFruit() {
String prefix = "";
if (random.nextBoolean()) {
prefix = "delicious-";
}
return prefix + fruitsName[random.nextInt(fruitsName.length)];
}

public void bet() {
int dice = random.nextInt(6) + 1;
if (dice == 1) {
money += 100;
System.out.println("money added.");
} else if (dice == 2) {
money /= 2;
System.out.println("money cut in half");
} else if (dice == 6) {
String fruit = getFruit();
System.out.println("get fruit: " + fruit);
fruits.add(fruit);
} else {
System.out.println("nothing happened");
}
}

public Memento createMemento() {
Memento memento = new Memento(money);
for (String fruit : fruits) {
if (fruit.startsWith("delicious-")) {
memento.addFruit(fruit);
}
}
return memento;
}

public void restoreMemento(Memento memento) {
this.money = memento.money;
this.fruits = memento.getFruits();
}

@Override
public String toString() {
return "[Money = " + money + ", fruits = " + fruits + "]";
}
}

接下来我们就可以完成测试代码了。实际上在这里,我们的测试代码就相当于 Caretaker角色,它根据当前的状态来判断是否需要进行状态存储以及状态更新。

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
@Test
void test() {
Gamer gamer = new Gamer(100);
Memento memento = gamer.createMemento();
for (int i = 0; i < 100; i++) {
System.out.println("====" + i);
System.out.println("now status:" + gamer);

gamer.bet();

System.out.println("now player's money is " + gamer.getMoney());
if (gamer.getMoney() > memento.getMoney()) {
System.out.println("money add, save status.");
memento = gamer.createMemento();
} else if (gamer.getMoney() < memento.getMoney() / 2) {
System.out.println("money cut, restore former status.");
gamer.restoreMemento(memento);
}

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

说明

在Memento模式中,尤其需要注意的就是各个角色之间的调用关系以及方法的可见性。在这里我们引入Caretaker角色的目的是为了进行职责分担。Caretake角色的职责是决定何时拍摄快照、何时撤销以及保存Memento角色,而Originator角色的职责是生成Memento角色以及使用接收到的Memento角色来恢复自己的状态。有了这样的职责分担,如果有对应需求变更的时候,就可以非常方便的进行修改。这也是设计模式带给我们的便利之处。

State

简介

在面向对象编程中,我们使用类来表示对象,但是类对应的东西可能是真实存在的,也可能只是一个概念。例如在接下来的State设计模式中,我们就使用类来表示状态。用类来表示状态,我们就能够通过切换类来方便地改变对象的状态,如果需要增加新的状态,只需要增加新的状态类即可。而在使用到状态的地方,也只需要动态切换持有的表示状态的类即可。State模式较为简单,它的逻辑并不复杂,只是思想上的小改变,这里就不提供示例代码了。


设计模式(5)-状态管理
http://example.com/2023/04/17/设计模式-5-状态管理/
作者
EverNorif
发布于
2023年4月17日
许可协议