观察者模式
一. 说明
观察者模式常用于对象间存在一对多关系时当一个对象被修改,需要自动通知它的依赖对象。这个对象就叫做被观察者,这些依赖对象就是它的观察者。它也是行为型模式的一种,也是发布/订阅模型。
观察者模式通常解决的是对象状态改变需要通知给其他依赖对象的场景。主要是通过在被观察者内部维护一个观察者的List集合,当被观察者接收到某个事件(方法被调用,字段被更改等)时,将事件通知给它的所有观察者。
二.应用场景
- 在GUI编程中,所有的按钮等UI控件绑定的事件就是一个观察者模式
- MQ的消息发布订阅就是一种观察者思想
- 拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价
- 游戏业务中的游戏结束结算处理
三.代码示例
1. 简单例子
我们先用简单的例子表述一下观察者模式,以拍卖为例,主持人报价后所有竞拍者得到最新的报价。
先创建一个观察者接口,它具有接收报价的方法
public interface Observer {
void received(int maxPrice);
}
创建竞拍者类,它是观察者的实现类
@Data
public class Bidder implements Observer {
private String name;
public Bidder(String name) {
this.name = name;
}
@Override
public void received(int maxPrice) {
System.out.println(name + ":收到最高价:" + maxPrice);
}
}
创建主持人类,它是被观察者,内部维护所有观察者的集合。主持人在报价后,所有观察者接收到最新报价。
@Data
public class Host {
private int maxPrice;
private final List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void quote(int maxPrice) {
this.maxPrice = maxPrice;
System.out.println("主持人: 当前最高价:" + maxPrice);
observers.forEach(observer -> observer.received(maxPrice));
}
}
编写测试代码
public static void main(String[] args) {
Bidder b1 = new Bidder("张三");
Bidder b2 = new Bidder("李四");
Bidder b3 = new Bidder("王五");
Bidder b4 = new Bidder("赵六");
Host host = new Host();
host.addObserver(b1);
host.addObserver(b2);
host.addObserver(b3);
host.addObserver(b4);
host.quote(100000);
}
2. 使用JDK接口实现观察者模式
JDK中提供了实现观察者模式的接口,在java.util包中提供了Observable类和Observer接口,其中要求,被观察者需要继承Observable类,观察则需要实现Observer接口
。
现在我们通过JDK提供的接口,对上述简单的示例代码进行改造。
将竞拍者类实现JDK自带的Observer接口,实现它的update()方法。
@Data
public class Bidder implements Observer {
private String name;
public Bidder(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
received((Integer) arg);
}
private void received(int maxPrice) {
System.out.println(name + ":收到最高价:" + maxPrice);
}
}
将主持人继承JDK自带的Observable类,它将具有添加、删除Observer等方法和notifyObservers方法
@Data
public class Host extends Observable {
private int maxPrice;
public void quote(int maxPrice) {
//setChanged被调用才能通知给所有观察者
setChanged();
this.maxPrice = maxPrice;
System.out.println("主持人: 当前最高价:" + maxPrice);
notifyObservers(maxPrice);
}
}
测试代码
public static void main(String[] args) {
Bidder b1 = new Bidder("张三");
Bidder b2 = new Bidder("李四");
Bidder b3 = new Bidder("王五");
Bidder b4 = new Bidder("赵六");
Host host = new Host();
host.addObserver(b1);
host.addObserver(b2);
host.addObserver(b3);
host.addObserver(b4);
host.quote(100000);
}
在被观察者类中,想要通知所有观察者某个状态的变更,必须调用setChanged()方法将被观察者父类中的changed设置为true,否则将无法实现通知效果。
3. 实际开发示例
我们在使用SpringBoot开发游戏后端业务时,使用了观察者模式改造我们的游戏结算业务。
具体的业务场景是:在多个用户进行一场游戏结束后,客户端会上报该场对局的比赛demo文件到服务端,服务端解密解析该文件后会组合成整个游戏数据的对象,投递给RocketMQ,消费者接收MQ消息后做一系列的结算处理,例如:给用户加金币奖励,结算排行榜的排名,给游戏地图增加游玩热度等等逻辑。这一系列的结算逻辑互不影响,并且有先后顺序。
最初没有重构的简化后的代码是这样的:
/**
* 模拟消费者
*/
@Component
public class ConsumerListener {
@Resource
private GoldService goldService;
@Resource
private RankService rankService;
@Resource
private MapService mapService;
public void messageListener(MatchDTO matchDTO){
//奖励用户游玩金币
goldService.giftGold(matchDTO);
//刷新排行榜
rankService.refresh(matchDTO);
//增加地图热度
mapService.mapHot(matchDTO);
}
}
@Service
public class GoldService {
public void giftGold(MatchDTO matchDTO) {
try {
System.out.println("---------结算金币奖励-----------");
matchDTO.getPlayers().forEach(uid -> {
System.out.println(uid +"赠送10金币");
});
}catch (Exception e){
System.out.println("---------结算金币奖励出错-----------");
}
}
}
@Service
public class RankService {
public void refresh(MatchDTO matchDTO){
try {
System.out.println("---------结算排行榜-----------");
System.out.println("刷新排行榜");
}catch (Exception e){
System.out.println("---------结算排行榜出错-----------");
}
}
}
@Service
public class MapService {
public void mapHot(MatchDTO matchDTO){
try {
System.out.println("---------结算地图热度-----------");
System.out.println(matchDTO.getMapName() + "地图增加5点热度");
}catch (Exception e){
System.out.println("---------结算地图热度出错-----------");
}
}
}
比赛数据传输类
@Data
public class MatchDTO implements Serializable {
private String matchId;
private String model;
private String mapName;
private Long playTime;
private List<Long> players;
}
测试代码
public static void test(String[] args){
List<Long> players = new ArrayList<>();
players.add(10001L);
players.add(10002L);
players.add(10003L);
players.add(10004L);
MatchDTO matchDTO = new MatchDTO();
matchDTO.setMatchId("1");
matchDTO.setMapName("狙击基地");
matchDTO.setModel("普通模式");
matchDTO.setPlayTime(60 * 60L);
matchDTO.setPlayers(players);
ConsumerListener consumerListener = (ConsumerListener) applicationContext.getBean("consumerListener");
consumerListener.messageListener(matchDTO);
}
这里我们可以看出,这是最初面向业务开发的结果,目前只有三个结算业务,看起来代码量还好。如果后续新增扩展其他的结算业务,就是无脑的在消费方法里新增各个service的方法调用,每个业务方法都需要进行异常捕获避免某个业务出错影响到整个结算流程。每一次扩展结算业务,都需要更改消费者里的代码,不符合开闭原则。
我们利用观察者模式进行重构。
第一步我们将通知实体对象抽象化,建立实体基类的接口,让其可以有另外的比赛数据对象实现
public interface BaseDTO {
}
@Data
public class MatchDTO implements BaseDTO {
private String matchId;
private String model;
private String mapName;
private Long playTime;
private List<Long> players;
}
第二步,创建观察者接口,它具有结算处理的方法,每一个观察者实现它都需要实现结算处理方法
public interface Observer extends Ordered {
void handle(BaseDTO baseDTO);
}
@Service
public class GoldHandler implements Observer {
@Override
public void handle(BaseDTO baseDTO) {
MatchDTO matchDTO = (MatchDTO) baseDTO;
System.out.println("---------结算金币奖励-----------");
matchDTO.getPlayers().forEach(uid -> {
System.out.println(uid +"赠送10金币");
});
}
@Override
public int getOrder() {
return 0;
}
}
@Service
public class RankHandler implements Observer {
@Override
public void handle(BaseDTO baseDTO) {
System.out.println("---------结算排行榜-----------");
System.out.println("刷新排行榜");
}
@Override
public int getOrder() {
return 1;
}
}
@Service
public class MapHandler implements Observer {
@Override
public void handle(BaseDTO baseDTO) {
MatchDTO matchDTO = (MatchDTO) baseDTO;
System.out.println("---------结算地图热度-----------");
System.out.println(matchDTO.getMapName() + "地图增加5点热度");
}
@Override
public int getOrder() {
return 2;
}
}
第三步编写观察者工厂,也是被观察者类
@Component
public class ObserverFactory {
private final List<Observer> observers = new ArrayList<>();
@Resource
private Set<Observer> observerSet;
@PostConstruct
public void init(){
observerSet.forEach(observer -> {
observers.add(observer);
});
//实现了Ordered可以使用此方法排序,order值小的优先级更高
AnnotationAwareOrderComparator.sort(observers);
}
//通知所有观察者
public void notifyObservers(BaseDTO baseDTO){
observers.forEach(observer -> {
try {
observer.handle(baseDTO);
}catch (Exception e){
e.printStackTrace();
}
});
}
}
第四步更改消费者代码
@Component
public class ConsumerListener {
@Resource
private ObserverFactory observerFactory;
public void messageListener(MatchDTO matchDTO) {
observerFactory.notifyObservers(matchDTO);
}
}
编写测试代码
public static void test(String[] args){
List<Long> players = new ArrayList<>();
players.add(10001L);
players.add(10002L);
players.add(10003L);
players.add(10004L);
MatchDTO matchDTO = new MatchDTO();
matchDTO.setMatchId("1");
matchDTO.setMapName("狙击基地");
matchDTO.setModel("普通模式");
matchDTO.setPlayTime(60 * 60L);
matchDTO.setPlayers(players);
ConsumerListener consumerListener = (ConsumerListener) applicationContext.getBean("consumerListener");
consumerListener.messageListener(matchDTO);
}
整个重构完成,后续我们需要新增结算业务,只需要新增一个观察者Handler即可,删除某个结算业务,只需要把具体观察者Handler从spring容器中移除即可,整个消费者代码是无需变更的。
当然的,我们还有一种做法就是在消费者中通过Spring发布一个结算自定义事件,然后每个Handler监听处理该事件,这种事件的发布订阅也是观察者模式的实现。
这里只是为了演示如何使用观察者模式进行一次小的重构,在这个例子的实际应用中,整个结算业务是异常复杂多样的,甚至是跨服务结算,设计模式也都是多个组合使用的,这里不方便演示。
四. 总结
观察者模式满足开闭原则,我们添加、修改或者删除观察者,整个模型是不需要更改的,只需要变动具体某个观察者。在软件开发中这种模式也是经常使用的,例如在某项业务里通常是具有主流程和辅流程的,主流程一般是不经常改变的,而辅流程是经常变化的,所以这些经常变化的部分我们可以判断是否符合观察者模式的特性,从而利用它进行重构。