观察者模式(Observer)

老师上完课了,通知学生们说明天要考试

观察者模式

介绍

定义对象之间的一对多依赖,当一个对象的状态改变时,它的所有依赖都会收到通知,并且自动更新状态;

简单理解,允许有一种订阅机制,有个 A 对象,当 A 对象发生一些事情的时候,会调用一个 notify 的方法,来通知其他订阅 A 对象的对象(B、C),然后 B 和 C 做出一些事情;

可以把 A 比作老师,然后 B、C是学生,A 如果说明天考试,那么 B、C 都会收到通知,然后去准备考试;

观察者模式和发布订阅模式的区别:

  • 观察者模式是松耦合的,被观察者是可以直接通知观察者( A 可以直接通知 B );一般用于应用内部
  • 发布订阅模式是完全解耦的,发布者和订阅者之间还有一个 broker 的存在,中间人 broker 转发发布的消息给订阅者;更多的是跨应用的模式,比如常用的消息中间件( kafka )

实现方式

场景

一个用户摇号,然后把摇号结果发送信息给用户,同时也要推送到 MQ 中。

整体设计

代码实现

先定义核心业务类,摇号服务类:

public class MinibusTargetService {

    /**
     * 模拟摇号
     * @param uId 用户id
     * @return 结果字符串信息
     */
    public String lottery(String uId) {
        return Math.abs(uId.hashCode()) % 2 == 0 ?
                "恭喜你,编码".concat(uId).concat("在本次摇号中签") :
                "很遗憾,编码".concat(uId).concat("在本次摇号未中签或摇号资格已过期");
    }
}

下面开始做 UML 中定义的类了,先来定义基本的事件接口:

public interface EventListener {

    /**
     * 基本事件
     * @param result LotteryResult
     */
    void doEvent(LotteryResult result);
}

事件接口实现类:

发送短信通知

public class MessageEventListener implements EventListener {

    private final Logger logger = LoggerFactory.getLogger(MessageEventListener.class);

    @Override
    public void doEvent(LotteryResult result) {
        // 实际业务逻辑
        logger.info("给用户 {} 发送短信通知:{}", result.getuId(), result.getMsg());
    }
}

推送 MQ:

public class MQEventListener implements EventListener {

    private final Logger logger = LoggerFactory.getLogger(MQEventListener.class);

    @Override
    public void doEvent(LotteryResult result) {
        logger.info("记录用户 {} 摇号结果(MQ)为:{}", result.getuId(), result.getMsg());
    }
}

再定义,事件处理类,这个里面维护了 listeners 这个 map 对象,存放 事件类型 和对应的 事件监听者们

public class EventManager {

    /**
     * 存放 事件类型 和对应的 事件监听者们(list)
     */
    private final Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();

    @SafeVarargs
    public EventManager(Enum<EventType>... operations) {
        for (Enum<EventType> operation : operations) {
            listeners.put(operation, new ArrayList<>());
        }
    }

    public enum EventType {
        /** MQ类型 */
        MQ,
        /** 短信息类型 */
        Message
    }

    /**
     * 订阅
     * @param eventType 事件类型
     * @param eventListener 监听
     */
    public void subscribe(Enum<EventType> eventType, EventListener eventListener) {
        List<EventListener> eventListeners = listeners.get(eventType);
        eventListeners.add(eventListener);
    }

    /**
     * 取消监听
     * @param eventType 事件类型
     * @param eventListener 监听
     */
    public void unsubscribe(Enum<EventType> eventType, EventListener eventListener) {
        List<EventListener> eventListeners = listeners.get(eventType);
        eventListeners.remove(eventListener);
    }

    /**
     * 通知
     * @param eventType 事件类型
     * @param result 结果
     */
    public void notify(Enum<EventType> eventType, LotteryResult result) {
        List<EventListener> eventListeners = listeners.get(eventType);
        for (EventListener eventListener : eventListeners) {
            eventListener.doEvent(result);
        }
    }


}

摇号业务抽象类,在这个类里面使用 EventManager 进行订阅,通知等操作:

public abstract class LotteryService {

    private final EventManager eventManager;

    public LotteryService() {
        eventManager = new EventManager(EventType.MQ, EventType.Message);
        eventManager.subscribe(EventType.MQ, new MQEventListener());
        eventManager.subscribe(EventType.Message, new MessageEventListener());
    }

    public LotteryResult draw(String uId) {
        LotteryResult result = doDraw(uId);
        // 把结果通知推送到MQ
        eventManager.notify(EventType.MQ, result);
        // 把结果以短信息形式通知
        eventManager.notify(EventType.Message, result);
        return result;
    }

    /**
     * 摇号API
     * @param uId 用户id
     * @return LotteryResult
     */
    protected abstract LotteryResult doDraw(String uId);
}

摇号业务实现类,已经是松耦合了,这里面只专注业务方法,不用管繁琐的通知了,在父类已经做了通知:

public class LotteryServiceImpl extends LotteryService {

    private final MinibusTargetService minibusTargetService = new MinibusTargetService();

    @Override
    protected LotteryResult doDraw(String uId) {
        // 摇号
        String lottery = minibusTargetService.lottery(uId);
        // 结果
        return new LotteryResult(uId, lottery, new Date());
    }
}

测试:

@Test
public void testLottery() {
    Gson gson = new Gson();
    LotteryServiceImpl lotteryService = new LotteryServiceImpl();
    LotteryResult result = lotteryService.draw("123123123");
    logger.info("测试结果:{}", gson.toJson(result));
}

结果:

23:47:19.376 [main] INFO  i.s.o.event.listener.MQEventListener - 记录用户 123123123 摇号结果(MQ)为:恭喜你,编码123123123在本次摇号中签
23:47:19.380 [main] INFO  i.s.o.e.l.MessageEventListener - 给用户 123123123 发送短信通知:恭喜你,编码123123123在本次摇号中签
23:47:19.394 [main] INFO  i.sunnyc.observerdemo.test.MainTest - 测试结果:{"uId":"123123123","msg":"恭喜你,编码123123123在本次摇号中签","dateTime":"Mar 12, 2022 11:47:19 PM"}

整体业务时序图

注意已经实现了解耦,业务方法是业务方法那一套,然后业务方法执行完毕后,直接 notify 监听者,他们自己 doEvent即可~


观察者模式(Observer)
https://www.powercheng.fun/articles/8af60e4d/
作者
powercheng
发布于
2022年3月12日
许可协议