一、观察者模式介绍
观察者模式(observer pattern)的原始定义是:定义对象之间的一对多依赖关系,这样当一
个对象改变状态时,它的所有依赖项都会自动得到通知和更新。
观察者模式它是用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动
通知其他对象,其他对象将相应的作出反应。
在观察者模式中发生改变的对象称为“观察目标”,而被通知的对象称为“观察者”,
一个观察目标可以应对多个观察者,而且这些观察者之间可以没有任何相互联系,可以
根据需要增加和删除观察者,使得系统更易于扩展。
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,
或者 一些产品的设计思路,
如:发布-订阅(Publish/Subscribe)模式,模型-视图(Model-View)模式、
源-监听(Source-Listener) 模式等
二、观察者模式原理
观察者模式结构中通常包括: 观察目标和观察者两个继承层次结构。
观察者模式结构图如下:
观察者模式中包含以下角色:
1)Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在
一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个
接口,可以增加和删除观察者对象
2) ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察
者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送
通知。
3)Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到
主题更改通知时更新自己。
4)ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到
主题更改通知时更新自身的状态。在具体观察者中维护一个指向具体目标
对象的引用,它存储具体观察者的有关状态,这些状态需要与具体目标保
持一致。
观察者模式用代码表示如下:
/**
* 抽象观察者
*/
public interface Observer {
//为不同观察者的更新(响应)行为定义相同的接口,不同的观察者对该方法有不同的实现
public void update();
}
/*******************************************************
* 具体观察者
*
* @author lbf
*******************************************************/
public class ConcreteObserverOne implements Observer{
@Override
public void update() {
//获取消息通知,执行业务代码
System.out.println("ConcreteObserverOne 得到通知!");
}
}
/*******************************************************
* 具体观察者
*
* @author lbf
*
*******************************************************/
public class ConcreteObserverTwo implements Observer{
@Override
public void update() {
//获取消息通知,执行业务代码
System.out.println("ConcreteObserverTwo 得到通知!");
}
}
/**
* 抽象被观察者
*/
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
/*******************************************************
* 具体被观察者
*
* @author lbf
*
*******************************************************/
public class ConcreteSubject implements Subject{
//定义集合,存储所有观察者对象
private ArrayList<Observer> observers = new ArrayList<>();
//注册方法,向观察者集合中增加一个观察者
@Override
public void attach(Observer observer) {
observers.add(observer);
}
//注销方法,用于从观察者集合中删除一个观察者
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
//通知方法
@Override
public void notifyObservers() {
//遍历观察者集合,调用每一个观察者的响应方法
for (Observer obs : observers) {
obs.update();
}
}
}
//测试
public static void main(String[] args) {
//创建目标类(被观察者)
ConcreteSubject subject = new ConcreteSubject();
//注册观察者类,可以注册多个
subject.attach(new ConcreteObserverOne());
subject.attach(new ConcreteObserverTwo());
//具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
subject.notifyObservers();
}
三、观察者模式应用示例
以一个摇号服务为例,来看下观察者模式的使用;
摇号结束,需要通过短信告知用户摇号结果,还需要想MQ中保存用户本次摇号的信息。
示例代码一:不使用设计模式实现
/*******************************************************
* 模拟一个摇号服务
* 不使用设计模式实现
*
* @author lbf
*
*******************************************************/
public class DrawHouseService {
//摇号抽签
public String lots(String uId){
if(uId.hashCode() % 2 == 0){
return "恭喜ID为: " + uId + " 的用户,在本次摇号中中签! !";
}else{
return "很遗憾,ID为: " + uId + "的用户,您本次未中签! !";
}
}
}
/*******************************************************
*
* @author lbf
*
*******************************************************/
public class LotteryResult {
private String uId; // 用户id
private String msg; // 摇号信息
private Date dataTime; // 业务时间
public LotteryResult(String uId, String result, Date date) {
}
//get&set.....
}
/**
* 开奖服务接口
*/
public interface LotteryService {
//摇号相关业务
public LotteryResult lottery(String uId);
}
/*******************************************************
* 开奖服务实现
*
* @author lbf
*
*******************************************************/
public class LotteryServiceImpl implements LotteryService{
//注入摇号服务
private DrawHouseService houseService = new DrawHouseService();
@Override
public LotteryResult lottery(String uId) {
//摇号
String result = houseService.lots(uId);
//发短信
System.out.println("发送短信通知用户ID为: " + uId + ",您的摇号结果如下: " + result);
//发送MQ消息
System.out.println("记录用户摇号结果(MQ), 用户ID:" + uId + ",摇号结果:" + result);
return new LotteryResult(uId,result,new Date());
}
}
//测试
public static void main(String[] args) {
LotteryService ls = new LotteryServiceImpl();
String result = String.valueOf(ls.lottery("1234567887654322"));
System.out.println(result);
}
示例代码二:使用观察者模式实现
使用“观察者模式” 优化示例一的代码,摇号业务中,摇号、发短信、发MQ消息是一
个顺序调用的过程,但是除了摇号这个核心功能以外, 发短信与记录信息到MQ的操作
都不是主链路的功能,需要单独抽取出来,这样才能保证在后面的开发过程中保证代码
的可扩展性和可维护性。流程图如下:
示例代码如下:
/**
* 事件监听接口
* 抽象观察者
*/
public interface EventListener {
void doEvent(LotteryResult result);
}
/*******************************************************
* 短信发送事件
* 具体观察者
*
* @author lbf
*
*******************************************************/
public class MessageEventListener implements EventListener{
@Override
public void doEvent(LotteryResult result) {
System.out.println("发送短信通知用户ID为: " + result.getuId() +
",您的摇号结果如下: " + result.getMsg());
}
}
/*******************************************************
* MQ消息发送事件
* 具体观察者
*
* @author lbf
*
*******************************************************/
public class MQEventListener implements EventListener{
@Override
public void doEvent(LotteryResult result) {
System.out.println("记录用户摇号结果(MQ), 用户ID:" + result.getuId() +
",摇号结果:" + result.getMsg());
}
}
/*******************************************************
* 事件处里
*
* @author lbf
*
*******************************************************/
public class EventManager {
public enum EventType{
MQ,Message
}
//监听器集合
Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();
public EventManager(Enum<EventType>... operations) {
for (Enum<EventType> operation : operations) {
this.listeners.put(operation,new ArrayList<>());
}
}
/**
* 订阅
* @param eventType 事件类型
* @param listener 监听
*/
public void subscribe(Enum<EventType> eventType, EventListener listener){
List<EventListener> users = listeners.get(eventType);
users.add(listener);
}
/**
* 取消订阅
* @param eventType 事件类型
* @param listener 监听
*/
public void unsubscribe(Enum<EventType> eventType,EventListener listener){
List<EventListener> users = listeners.get(eventType);
users.remove(listener);
}
/**
* 通知
* @param eventType 事件类型
* @param result 结果
*/
public void notify(Enum<EventType> eventType, LotteryResult result){
List<EventListener> users = listeners.get(eventType);
for (EventListener listener : users) {
listener.doEvent(result);
}
}
}
/*******************************************************
* 开奖服务抽象类
* 抽象被观察者
*
* @author lbf
*
*******************************************************/
public abstract class LotteryService {
//EventManager 包含注册的观察者
private EventManager eventManager;
public LotteryService(){
//设置事件类型
eventManager = new EventManager(EventManager.EventType.MQ, EventManager.EventType.Message);
//订阅
eventManager.subscribe(EventManager.EventType.Message,new MessageEventListener());
eventManager.subscribe(EventManager.EventType.MQ,new MQEventListener());
}
public LotteryResult lotteryAndMsg(String uId){
LotteryResult result = lottery(uId);
//发送通知
eventManager.notify(EventManager.EventType.Message,result);
eventManager.notify(EventManager.EventType.MQ,result);
return result;
}
public abstract LotteryResult lottery(String uId);
}
/*******************************************************
* 开奖服务
* 具体被观察者
*
* @author lbf
*
*******************************************************/
public class LotteryServiceImpl extends LotteryService{
//注入摇号服务
private DrawHouseService houseService = new DrawHouseService();
@Override
public LotteryResult lottery(String uId) {
//摇号
String result = houseService.lots(uId);
return new LotteryResult(uId,result,new Date());
}
}
//测试
public class Test {
public static void main(String[] args) {
LotteryService ls = new LotteryServiceImpl();
LotteryResult result = ls.lotteryAndMsg("1234567887654322");
System.out.println(result);
}
}
四、观察者模式总结
1、观察者模式优点
1)降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
2)被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
2、观察者模式缺点
1)如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
2)如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致
系统崩溃
3、观察者模式适用场景
1)当一个对象状态的改变需要改变其他对象时。比如,商品库存数量发生变化时,需要
通知商品详情页、购物车等系统改变数量。
2)一个对象发生改变时只想要发送通知,而不需要知道接收者是谁。比如,订阅微信公
众号的文章,发送者通过公众号发送,订阅者并不知道哪些用户订阅了公众号。
3)需要创建一种链式触发机制时。比如,在系统中创建一个触发链,A 对象的行为将影
响 B 对象,B 对象的行为将影响 C 对象……这样通过观察者模式能够很好地实现。
4)微博或微信朋友圈发送的场景。这是观察者模式的典型应用场景,一个人发微博或
朋友圈,只要是关联的朋友都会收到通知;一旦取消关注,此人以后将不会收到相关通知。
5)需要建立基于事件触发的场景。比如,基于 Java UI 的编程,所有键盘和鼠标事件都由它
的侦听器对象和指定函数处理。当用户单击鼠标时,订阅鼠标单击事件的函数将被调用,
并将所有上下文数据作为方法参数传递给它。