本文最后更新于:2023-04-27T19:33:22+08:00
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模式较为简单,它的逻辑并不复杂,只是思想上的小改变,这里就不提供示例代码了。