备忘录
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
备忘录模式(Memento),主要用于捕获一个对象的内部状态,以便在将来的某个时候恢复此状态。
其实我们使用的几乎所有软件都用到了备忘录模式。最简单的备忘录模式就是保存到文件,打开文件。对于文本编辑器来说,保存就是把TextEditor
类的字符串存储到文件,打开就是恢复TextEditor
类的状态。对于图像编辑器来说,原理是一样的,只是保存和恢复的数据格式比较复杂而已。Java的序列化也可以看作是备忘录模式。
在使用文本编辑器的时候,我们还经常使用Undo、Redo这些功能。这些其实也可以用备忘录模式实现,即不定期地把TextEditor
类的字符串复制一份存起来,这样就可以Undo或Redo。
标准的备忘录模式有这么几种角色:
- Memonto:存储的内部状态;
- Originator:创建一个备忘录并设置其状态;
- Caretaker:负责保存备忘录。
实际上我们在使用备忘录模式的时候,不必设计得这么复杂,只需要对类似TextEditor
的类,增加getState()
和setState()
就可以了。
我们以一个文本编辑器TextEditor
为例,它内部使用StringBuilder
允许用户增删字符:
public class TextEditor {
private StringBuilder buffer = new StringBuilder();
public void add(char ch) {
buffer.append(ch);
}
public void add(String s) {
buffer.append(s);
}
public void delete() {
if (buffer.length() > 0) {
buffer.deleteCharAt(buffer.length() - 1);
}
}
}
为了支持这个TextEditor
能保存和恢复状态,我们增加getState()
和setState()
两个方法:
public class TextEditor {
...
// 获取状态:
public String getState() {
return buffer.toString();
}
// 恢复状态:
public void setState(String state) {
this.buffer.delete(0, this.buffer.length());
this.buffer.append(state);
}
}
对这个简单的文本编辑器,用一个String
就可以表示其状态,对于复杂的对象模型,通常我们会使用JSON、XML等复杂格式。
小结
备忘录模式是为了保存对象的内部状态,并在将来恢复,大多数软件提供的保存、打开,以及编辑过程中的Undo、Redo都是备忘录模式的应用。
观察者
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式(Observer)又称发布-订阅模式(Publish-Subscribe:Pub/Sub)。它是一种通知机制,让发送通知的一方(被观察方)和接收通知的一方(观察者)能彼此分离,互不影响。
要理解观察者模式,我们还是看例子。
假设一个电商网站,有多种Product
(商品),同时,Customer
(消费者)和Admin
(管理员)对商品上架、价格改变都感兴趣,希望能第一时间获得通知。于是,Store
(商场)可以这么写:
public class Store {
Customer customer;
Admin admin;
private Map<String, Product> products = new HashMap<>();
public void addNewProduct(String name, double price) {
Product p = new Product(name, price);
products.put(p.getName(), p);
// 通知用户:
customer.onPublished(p);
// 通知管理员:
admin.onPublished(p);
}
public void setProductPrice(String name, double price) {
Product p = products.get(name);
p.setPrice(price);
// 通知用户:
customer.onPriceChanged(p);
// 通知管理员:
admin.onPriceChanged(p);
}
}
我们观察上述Store
类的问题:它直接引用了Customer
和Admin
。先不考虑多个Customer
或多个Admin
的问题,上述Store
类最大的问题是,如果要加一个新的观察者类型,例如工商局管理员,Store
类就必须继续改动。
因此,上述问题的本质是Store
希望发送通知给那些关心Product
的对象,但Store
并不想知道这些人是谁。观察者模式就是要分离被观察者和观察者之间的耦合关系。
要实现这一目标也很简单,Store
不能直接引用Customer
和Admin
,相反,它引用一个ProductObserver
接口,任何人想要观察Store
,只要实现该接口,并且把自己注册到Store
即可:
public class Store {
private List<ProductObserver> observers = new ArrayList<>();
private Map<String, Product> products = new HashMap<>();
// 注册观察者:
public void addObserver(ProductObserver observer) {
this.observers.add(observer);
}
// 取消注册:
public void removeObserver(ProductObserver observer) {
this.observers.remove(observer);
}
public void addNewProduct(String name, double price) {
Product p = new Product(name, price);
products.put(p.getName(), p);
// 通知观察者:
observers.forEach(o -> o.onPublished(p));
}
public void setProductPrice(String name, double price) {
Product p = products.get(name);
p.setPrice(price);
// 通知观察者:
observers.forEach(o -> o.onPriceChanged(p));
}
}
就是这么一个小小的改动,使得观察者类型就可以无限扩充,而且,观察者的定义可以放到客户端:
// observer:
Admin a = new Admin();
Customer c = new Customer();
// store:
Store store = new Store();
// 注册观察者:
store.addObserver(a);
store.addObserver(c);
甚至可以注册匿名观察者:
store.addObserver(new ProductObserver() {
public void onPublished(Product product) {
System.out.println("[Log] on product published: " + product);
}
public void onPriceChanged(Product product) {
System.out.println("[Log] on product price changed: " + product);
}
});
用一张图画出观察者模式:
┌─────────┐ ┌───────────────┐
│ Store │─ ─ ─>│ProductObserver│
└─────────┘ └───────────────┘
│ ▲
│
│ ┌─────┴─────┐
▼ │ │
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Product │ │ Admin │ │Customer │ ...
└─────────┘ └─────────┘ └─────────┘
观察者模式也有很多变体形式。有的观察者模式把被观察者也抽象出接口:
public interface ProductObservable { // 注意此处拼写是Observable不是Observer!
void addObserver(ProductObserver observer);
void removeObserver(ProductObserver observer);
}
对应的实体被观察者就要实现该接口:
public class Store implements ProductObservable {
...
}
有些观察者模式把通知变成一个Event对象,从而不再有多种方法通知,而是统一成一种:
public interface ProductObserver {
void onEvent(ProductEvent event);
}
让观察者自己从Event对象中读取通知类型和通知数据。
广义的观察者模式包括所有消息系统。所谓消息系统,就是把观察者和被观察者完全分离,通过消息系统本身来通知:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Messaging System
│ │
┌──────────────────┐
┌──┼>│Topic:newProduct │──┼─┐ ┌─────────┐
│ └──────────────────┘ ├───>│ConsumerA│
┌─────────┐ │ │ ┌──────────────────┐ │ │ └─────────┘
│Producer │───┼───>│Topic:priceChanged│────┘
└─────────┘ │ │ └──────────────────┘ │
│ ┌──────────────────┐ ┌─────────┐
└──┼>│Topic:soldOut │──┼─────>│ConsumerB│
└──────────────────┘ └─────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
消息发送方称为Producer,消息接收方称为Consumer,Producer发送消息的时候,必须选择发送到哪个Topic。Consumer可以订阅自己感兴趣的Topic,从而只获得特定类型的消息。
使用消息系统实现观察者模式时,Producer和Consumer甚至经常不在同一台机器上,并且双方对对方完全一无所知,因为注册观察者这个动作本身都在消息系统中完成,而不是在Producer内部完成。
此外,注意到我们在编写观察者模式的时候,通知Observer是依靠语句:
observers.forEach(o -> o.onPublished(p));
这说明各个观察者是依次获得的同步通知,如果上一个观察者处理太慢,会导致下一个观察者不能及时获得通知。此外,如果观察者在处理通知的时候,发生了异常,还需要被观察者处理异常,才能保证继续通知下一个观察者。
思考:如何改成异步通知,使得所有观察者可以并发同时处理?
有的童鞋可能发现Java标准库有个java.util.Observable
类和一个Observer
接口,用来帮助我们实现观察者模式。但是,这个类非常不!好!用!实现观察者模式的时候,也不推荐借助这两个东东。
小结
观察者模式,又称发布-订阅模式,是一种一对多的通知机制,使得双方无需关心对方,只关心通知本身。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY