目录
一、观察者模式介绍
1.1 观察者模式定义
1.2 观察者模式原理
1.2.1 观察者模式类图
1.2.2 模式角色说明
1.2.3 示例代码
二、观察者模式的应用
2.1 需求说明
2.2 需求实现
2.2.1 未使用设计模式
2.2.1.1 具体实现
2.2.2 适用观察者模式
2.2.2.1 优化调整说明
2.2.2.1 具体实现
2.2.2.1.1 事件监听接口
2.2.2.1.2 短信发送事件
2.2.2.1.3 MQ消息发送事件
2.2.2.1.4 事件处理类
2.2.2.1.5 开奖服务接口
2.2.2.1.6 开奖服务实现
2.2.2.1.7 测试类
三、观察者模式总结
3.1 观察者模式的优点
3.2 观察者模式的缺点
3.3 观察者模式常见的使用场景
3.4 JDK 中对观察者模式的支持
观察者模式(observer pattern)的原始定义是:定义对象之间的一对多依赖关系,这样当一个对象改变状态时,它的所有依赖项都会自动得到通知和更新。
解释一下上面的定义: 观察者模式它是用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应的作出反应。
在观察者模式中发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以应对多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。
观察者模式的别名有发布-订阅(Publish/Subscribe)模式,模型-视图(ModelView)模式、源-监听(Source-Listener) 模式等。
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者 一些产品的设计思路,都有这种模式的影子。
现在我们常说的基于事件驱动的架构,其实也是观察者模式的一种最佳实践。当我们观察某一个对象时,对象传递出的每一个行为都被看成是一个事件,观察者通过处理每一个事件来完成自身的操作处理。
生活中也有许多观察者模式的应用,比如 汽车与红绿灯的关系,'红灯停,绿灯行',在这个过程中交通信号灯是汽车的观察目标,而汽车是观察者。
在观察者模式中有如下角色:
抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个
接口,可以增加和删除观察者对象。
该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要与具体目标保持一致。
package main.java.cn.test.observer.V1;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:02:40
* @description 抽象观察者
*/
public interface Observer {
//update方法: 为不同观察者的更新(响应)行为定义相同的接口,不同的观察者对该方法有不同的实现
public void update();
}
package main.java.cn.test.observer.V1;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:03:10
* @description 具体观察者
*/
public class ConcreteObserverOne implements Observer {
@Override
public void update() {
//获取消息通知,执行业务代码
System.out.println("ConcreteObserverOne 得到通知!");
}
}
package main.java.cn.test.observer.V1;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:03:49
* @description 具体观察者
*/
public class ConcreteObserverTwo implements Observer {
@Override
public void update() {
//获取消息通知,执行业务代码
System.out.println("ConcreteObserverTwo 得到通知!");
}
}
package main.java.cn.test.observer.V1;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:04:25
* @description 抽象目标类
*/
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
package main.java.cn.test.observer.V1;
import java.util.ArrayList;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:05:03
* @description 具体目标类
*/
public class ConcreteSubject implements Subject {
//定义集合,存储所有观察者对象
private ArrayList 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();
}
}
}
package main.java.cn.test.observer.V1;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:06:52
* @description 测试类
*/
public class Test {
public static void main(String[] args) {
//创建目标类(被观察者)
ConcreteSubject subject = new ConcreteSubject();
//注册观察者类,可以注册多个
subject.attach(new ConcreteObserverOne());
subject.attach(new ConcreteObserverTwo());
//具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
subject.notifyObservers();
}
}
接下来我们使用观察模式,来实现一个买房摇号的程序。摇号结束,需要通过短信告知用户摇号结果,还需要想MQ中保存用户本次摇号的信息。
package main.java.cn.test.observer.V2;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:23:16
* @description 开奖服务接口
*/
public interface LotteryService {
//摇号相关业务
public LotteryResult lottery(String uId);
}
package main.java.cn.test.observer.V2;
import java.util.Date;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:23:53
* @description 开奖服务
*/
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());
}
}
package main.java.cn.test.observer.V2;
import java.util.Date;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:22:13
* @description 摇号结果
*/
public class LotteryResult {
private String uId; // 用户id
private String msg; // 摇号信息
private Date dataTime; // 业务时间
public LotteryResult(String uId, String msg, Date dataTime) {
this.uId = uId;
this.msg = msg;
this.dataTime = dataTime;
}
public String getuId() {
return uId;
}
public void setuId(String uId) {
this.uId = uId;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Date getDataTime() {
return dataTime;
}
public void setDataTime(Date dataTime) {
this.dataTime = dataTime;
}
@Override
public String toString() {
return "LotteryResult{" +
"uId='" + uId + '\'' +
", msg='" + msg + '\'' +
", dataTime=" + dataTime +
'}';
}
}
package main.java.cn.test.observer.V2;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:21:39
* @description 模拟买房摇号服务
*/
public class DrawHouseService {
//摇号抽签
public String lots(String uId) {
if (uId.hashCode() % 2 == 0) {
return "恭喜ID为: " + uId + " 的用户,在本次摇号中中签! !";
} else {
return "很遗憾,ID为: " + uId + "的用户,您本次未中签!!";
}
}
}
package main.java.cn.test.observer.V2;
/**
* @author ningzhaosheng
* @date 2024/1/14 18:26:36
* @description 测试类
*/
public class Test {
public static void main(String[] args) {
LotteryService ls = new LotteryServiceImpl();
LotteryResult result = ls.lottery("1234567887654322");
System.out.println(result.toString());
}
}
上面的摇号业务中,摇号、发短信、发MQ消息是一个顺序调用的过程,但是除了摇号这个核心功能以外,发短信与记录信息到MQ的操作都不是主链路的功能,需要单独抽取出来,这样才能保证在后面的开发过程中保证代码的可扩展性和可维护性。
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.LotteryResult;
/**
* @author ningzhaosheng
* @date 2024/1/15 10:59:49
* @description 事件监听接口
*/
public interface EventListener {
void doEvent(LotteryResult result);
}
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.LotteryResult;
/**
* @author ningzhaosheng
* @date 2024/1/15 11:00:26
* @description 短信发送事件
*/
public class MessageEventListener implements EventListener {
@Override
public void doEvent(LotteryResult result) {
System.out.println("发送短信通知用户ID为: " + result.getuId() + ",您的摇号结果如下: " + result.getMsg());
}
}
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.LotteryResult;
/**
* @author ningzhaosheng
* @date 2024/1/15 11:01:06
* @description MQ消息发送事件
*/
public class MQEventListener implements EventListener {
@Override
public void doEvent(LotteryResult result) {
System.out.println("记录用户摇号结果(MQ), 用户ID:" + result.getuId() + ",摇号结果:" + result.getMsg());
}
}
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.LotteryResult;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author ningzhaosheng
* @date 2024/1/15 11:01:46
* @description 事件处理类
*/
public class EventManager {
public enum EventType {
MQ, Message
}
//监听器集合
Map, List> listeners = new
HashMap<>();
public EventManager(Enum... operations) {
for (Enum operation : operations) {
this.listeners.put(operation, new ArrayList<>());
}
}
/**
* 订阅
*
* @param eventType 事件类型
* @param listener 监听
*/
public void subscribe(Enum eventType,
EventListener listener) {
List users = listeners.get(eventType);
users.add(listener);
}
/**
* 取消订阅
*
* @param eventType 事件类型
* @param listener 监听
*/
public void unsubscribe(Enum
eventType, EventListener listener) {
List users = listeners.get(eventType);
users.remove(listener);
}
/**
* 通知
*
* @param eventType 事件类型
* @param result 结果
*/
public void notify(Enum eventType,
LotteryResult result) {
List users = listeners.get(eventType);
for (EventListener listener : users) {
listener.doEvent(result);
}
}
}
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.LotteryResult;
/**
* @author ningzhaosheng
* @date 2024/1/15 11:03:21
* @description 开奖服务接口
*/
public abstract class LotteryService {
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);
}
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.DrawHouseService;
import main.java.cn.test.observer.V2.LotteryResult;
import java.util.Date;
/**
* @author ningzhaosheng
* @date 2024/1/15 11:05:14
* @description 开奖服务
*/
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());
}
}
package main.java.cn.test.observer.V3;
import main.java.cn.test.observer.V2.LotteryResult;
/**
* @author ningzhaosheng
* @date 2024/1/15 11:07:14
* @description 测试类
*/
public class Test {
public static void main(String[] args) {
LotteryService ls = new LotteryServiceImpl();
LotteryResult result = ls.lotteryAndMsg("1234567887654322");
System.out.println(result);
}
}
比如,商品库存数量发生变化时,需要通知商品详情页、购物车等系统改变数量。
比如,订阅微信公众号的文章,发送者通过公众号发送,订阅者并不知道哪些用户订阅了公众号。
比如,在系统中创建一个触发链,A 对象的行为将影响 B 对象,B 对象的行为将影响 C 对象……这样通过观察者模式能够很好地实现。
这是观察者模式的典型应用场景,一个人发微博或朋友圈,只要是关联的朋友都会收到通知;一旦取消关注,此人以后将不会收到相关通知。
比如,基于 Java UI 的编程,所有键盘和鼠标事件都由它的侦听器对象和指定函数处理。当用户单击鼠标时,订阅鼠标单击事件的函数将被调用,并将所有上下文数据作为方法参数传递给它。
JDK中提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持。
该接口中声明了一个方法,它充当抽象观察者,其中声明了一个update方法。
void update(Observable o, Object arg);
充当观察目标类(被观察类) , 在该类中定义了一个Vector集合来存储观察者对象.下面是它最重要的 3 个方法。
void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。
void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。
void setChange() 方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。
用户可以直接使用Observer接口和Observable类作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,使用JDK中提供的这两个类可以更加方便的实现观察者模式。
好了,本次分享就到这里,欢迎大家继续阅读《设计模式》专栏其他设计模式内容,如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!