定义
defines a one to many dependency between objects so that one object changes state, all of its dependents are notified and updated automatically.
定义对象之间的一对多依赖关系,这样一旦对象改变状态,所有它的依赖对象都会自动收到通知或更新。
实列
生活中,有些现象和观察者模式非常的相似。
比如说,在网购商品时,其中某些商品的价格非常低廉而且你也非常心动,但是它目前处于缺货状态,什么时候补货商家自己也不知道。
此时,商家为了促成交易,会在商品的购买按钮旁边放置一个"到货通知"按钮,如果你选择了到货通知,那么一旦该商品补上库存,那么你就会立即收到订阅通知。
在这个过程中你便是订阅者或者说观察者,但你不是唯一的订阅着,还有其它和你一样的订阅者也都会收到通知,而商家便是发布通知的发布者;如果你不订阅,那么你得不停的登录网站查看商品的到货情况。
如果用观察者模式的话说,商品便是主题,你便是订阅者,一个主题有多个订阅者。
故事
两年前,我入职了一家电商公司并负责订单模块的研发工作。刚开始,订单模块的需求特别简单,只需在订单状态从待支付变成已支付时,通知一下库存模块和物流模块进行扣减库存和发货操作;
后来,很多新模块如用户积分、优惠券、财务的新增都需要在订单状态变化时通知它们,没办法我就只能不停的修改订单类,最后在我离职时订单类的代码已经惨目忍睹了。如下:
public class Order {
protected String status;
public void payed() {
this.status = "支付成功";
//扣减库存
Inventory inventory = new Inventory();
inventory.decrease();
//发货
Delivery delivery = new Delivery();
delivery.deliver();
//增加用户积分
UserPoint userPoint = new UserPoint();
userPoint.increase();
//......
}
}
问题
故事中,订单有一个状态,这个状态会发生变化,一旦状态改变这一事件发生,为了让库存,物流,积分等关注者知晓该事件,它会分别调用这些对象的具体方法。
关注者们分属不同模块,它们类型各异而且数量也是不固定的,这就导致发布者不能以一致的方式调用它们,新增关注者需要修改代码,也不能运行时动态移除关注者。
那事件的发布者订单真的需要耦合这些关注者吗?直接调用它们的方法只是为了通知他们,其实可以以一致的方式调用的,因为入参是相同的,也不依赖返回值,仅仅是方法名不同。
所以,有没有一种方式可以解决一致、扩展,动态的问题,这便是观察者模式。
方案
在观察者模式中,关注主题状态变化的对象被称为订阅者或观察者,它们会实现一个统一的订阅接口,这解决了一致的问题;
订单对象被称之为发布者或主题,它维护着一个订阅者列表但不依赖具体订阅者,并对外提供注册和注销成为订阅者的操作,这解决了动态的问题,
当一致和动态问题解决了之后,新增订阅者只需实现订阅接口以及注册成为订阅者,这就解决了扩展的问题。
这样,发布者发生状态变化时,它一般会将变化的数据包装成一个事件对象,然后遍历订阅者列表将事件通知到每一个订阅者。
应用
接下来,我们使用观察者模式重构一下订单模块。
首先,声明一个统一的订阅者角色接口,它包含一个接收通知的方法。
/**订单订阅者接口*/
public interface OrderSubscriber {
public void notify(Event event);
}
然后,让关注事件的对象都实现该接口。
/**库存订阅者*/
public class Inventory implements OrderSubscriber{
public void decrease(){
System.out.println("库存扣减");
}
@Override
public void notify(Event event) {
System.out.println("收到事件后,触发库存扣减");
decrease();
}
}
/**物流订阅者*/
public class Delivery implements OrderSubscriber{
public void deliver(){
System.out.println("发货");
}
@Override
public void notify(Event event) {
System.out.println("收到事件后,触发发货操作");
deliver();
}
}
现在,我们修改一下订单类Order,让它对外提供一个注册以及注销订阅者身份的方法,这样可以在运行时动态添加或移除订阅者。
public class Order {
protected String status;
protected List orderObservers = new LinkedList<>();
public void register(OrderSubscriber observer){
orderObservers.add(observer);
}
public void unRegister(OrderSubscriber observer){
orderObservers.remove(observer);
}
public void payed() {
this.status = "支付成功";
//通知订阅者
for (OrderSubscriber orderObserver : orderObservers) {
//将变化的状态信息封装成事件
Event event = new Event(status);
orderObserver.notify(event);
}
}
}
最后,我们在看看如何使用订阅者模式。
public class Client {
public static void main(String[] args) {
//发布者
Order order = new Order();
//订单事件的订阅者
OrderSubscriber inventory = new Inventory();
OrderSubscriber delivery = new Delivery();
//注册订阅者
order.register(inventory);
order.register(delivery);
//触发订单更新
order.payed();
}
}
结构
抽象订阅者角色(Subscribe) :它声明了一个通知接口,该接口通常包含一个以事件(Event)为入参的方法,事件中封装了主题状态变化的数据。
具体订阅者角色(ConcreteSubscribe):它是事件的订阅者会被注册到主题的订阅者列表中,负责接收并处理来自发送者的事件。
发布者角色(Publisher) :它既是主题也是事件的发布者,它有一个会发生变化的状态和一个指向订阅者的引用列表,当状态发生变化时,变化数据会被封装成一个事件并发送给列表中的每一个订阅者。
/**抽象订阅者*/
public interface Subscriber {
public void notify(Event event);
}
/**具体订阅者A*/
public class ConcreteSubscribeA implements Subscriber{
protected void doSomeThing(){}
@Override
public void notify(Event event) {
doSomeThing();
}
}
/**具体订阅者B*/
public class ConcreteSubscribeA implements Subscriber{
protected void doSomeThing(){}
@Override
public void notify(Event event) {
doSomeThing();
}
}
/**发布者*/
public class Publisher {
/**会变化的状态*/
protected String status;
/**订阅者列表*/
protected List subscribers = new LinkedList<>();
/**注册*/
public void register(Subscriber observer){
subscribers.add(observer);
}
/**注销*/
public void unRegister(Subscriber observer){
subscribers.remove(observer);
}
/**改变状态*/
public void changeStatus(){
this.status="changeStatus";
for (Subscriber subscriber : subscribers) {
Event event = new Event(status);
subscriber.notify(event);
}
}
}
public class Client {
public static void main(String[] args) {
Publisher publisher = new Publisher();
ConcreteSubscribeA subscribeA = new ConcreteSubscribeA();
ConcreteSubscribeB subscribeB = new ConcreteSubscribeB();
//注册订阅者
publisher.register(subscribeA);
publisher.register(subscribeB);
//触发状态变化并通知订阅者
publisher.changeStatus();
}
}
总结
当一个事件存在多个不同类型的订阅者时,为了避免发布者和订阅者耦合,首先为事件的通知定义一个同一接口,然后让发布者提供一种机制允许订阅者关注和取关事件。
这样,可以使发布者以一致的方式将事件发送给订阅者,也能运行时动态注册和注销订阅者,还能在不修改发布类的前提下扩展订阅者。