侧边栏壁纸
博主头像
qingtian博主等级

喜欢是一件细水流长的事,是永不疲惫的双向奔赴~!

  • 累计撰写 94 篇文章
  • 累计创建 42 个标签
  • 累计收到 1 条评论

设计模式之观察者模式

qingtian
2020-12-27 / 0 评论 / 0 点赞 / 843 阅读 / 8,821 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2020-12-31,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

设计模式之观察者模式

来源自《重学Java设计模式》链接提取码:ytc3

观察者模式

image-20201226145244598

简单来讲观察者模式,就是当一个行为发生时传递信息给另外一个用户接收做出相应的处理,两者之间没有直接的耦合关联。

除了生活中的场景外,在我们编程开发的时候也会常用到一些观察者模式的组件,例如我们经常使用的MQ服务,虽然Mq服务是有一个通知中心并不是每一个类服务进行通知,但整体上也可以算作是观察者模式的思路设计。再比如可能有做过一些类似事件监听总线,让主线服务于其他辅线业务服务分离,为了使系统降低耦合和增强扩展性,也会使用观察者模式进行处理。

案例场景模拟

image-20201227100021391

在本案例中我们模拟的是每次小客车指标摇号事件通知场景。

可能⼤部分⼈看到这个案例⼀定会想到⾃⼰每次摇号都不中的场景,收到⼀个遗憾的短信通知。当然⽬前的摇号系统并不会给你发短信,⽽是由百度或者⼀些其他插件发的短信。那么假如这个类似的摇号功能如果由你来开发,并且需要对外部的⽤户做⼀些事件通知以及需要在主流程外再添加⼀些额外的辅助流程时该如何处理呢?

如果你有仔细思考过你的核⼼类功能会发现,这⾥⾯有⼀些核⼼主链路,还有⼀部分是辅助功能。⽐如完成了某个⾏为后需要触发MQ给外部,以及做⼀些消息PUSH给⽤户等,这些都不算做是核⼼流程链路,是可以通过事件通知的⽅式进⾏处理。

场景简述

摇号服务接口

/**
 * 小客车指标调控服务
 */
public class MinibusTargetService {

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

}

常规实现

  • 这段代码接口中包括了三部分内容:返回对象(LotteryResult)、定义接口(LotterSAervice)、具体实现(LotteryServiceImpl)。

代码实现

public class LotteryResult {

    private String uId;    // 用户ID
    private String msg;    // 摇号信息
    private Date dateTime; // 业务时间

    public LotteryResult(String uId, String msg, Date dateTime) {
        this.uId = uId;
        this.msg = msg;
        this.dateTime = dateTime;
    }

    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 getDateTime() {
        return dateTime;
    }

    public void setDateTime(Date dateTime) {
        this.dateTime = dateTime;
    }
}
public interface LotteryService {

    LotteryResult doDraw(String uId);

}
public class LotteryServiceImpl implements LotteryService {

    private Logger logger = LoggerFactory.getLogger(LotteryServiceImpl.class);

    private MinibusTargetService minibusTargetService = new MinibusTargetService();

    public LotteryResult doDraw(String uId) {
        // 摇号
        String lottery = minibusTargetService.lottery(uId);
        // 发短信
        logger.info("给用户 {} 发送短信通知(短信):{}", uId, lottery);
        // 发MQ消息
        logger.info("记录用户 {} 摇号结果(MQ):{}", uId, lottery);
        // 结果
        return new LotteryResult(uId, lottery, new Date());
    }

}
  • 从以上的方法实现中可以看到,整体过程包括三部分:摇号、发短信、发MQ消息,而这部分都是按顺序调用的。
  • 除了摇号接口调用,后面的两部分都是非核心主链路功能,而且会随着后续的业务需求发展而不断的调整和扩充,在这样的开发方式下就非常不利于维护。

测试验证

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test() {
        LotteryService lotteryService = new LotteryServiceImpl();
        LotteryResult result = lotteryService.doDraw("2765789109876");
        logger.info("测试结果:{}", JSON.toJSONString(result));
    }

}
12:50:25.664 [main] INFO  o.i.demo.design.LotteryServiceImpl - 给用户 2765789109876 发送短信通知(短信):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
12:50:25.693 [main] INFO  o.i.demo.design.LotteryServiceImpl - 记录用户 2765789109876 摇号结果(MQ):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
12:50:26.032 [main] INFO  org.itstack.demo.design.ApiTest - 测试结果:{"dateTime":1609044625696,"msg":"很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期","uId":"2765789109876"}

Process finished with exit code 0

使用观察者模式

观察者模式结构模型

image-20201227125311872

  • 从上图可以分为三大块看:事件监听事件处理具体的业务流程,另外在业务流程中LotteryService定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。
  • 右下角圆圈表示的是核心流程与非核心流程的结构,一般在开发中会把主线流程开发完成后,再使用通知的方式处理辅助流程。他们可以是异步的,再MQ以及定时任务的处理下,保证最终一致性。

代码实现

事件监听接口定义

public interface EventListener {

    void doEvent(LotteryResult result);

}
  • 接口中定义了基本的事件类,这里如果方法的入参信息类型是变化的可以使用泛型<T>

两个监听事件的实现

短信事件

public class MessageEventListener implements EventListener {

    private 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 Logger logger = LoggerFactory.getLogger(MQEventListener.class);

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

}
  • 以上是两个事件的具体实现,相对来说都比较简单。如果是实际的业务开发那么会需要调用外部接口以及控制异常的处理。
  • 同时我们上面提到的事件接口添加泛型,如果有需要那么再事件的实现中就可以按照不同类型进行包装事件内容。

事件处理类

public class EventManager {

    Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();

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

    public enum EventType {
        MQ, Message
    }

    /**
     * 订阅
     * @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);
        }
    }

}
  • 整个处理的实现上提供了三个主要方法:订阅(subscribe)、取消订阅(unsubscribe)、通知(notify)。这三个方法分别用于对监听事件的添加和使用。
  • 另外因为事件有不同的类型,这里使用了枚举的方式进行处理,也可以方便让外部再规定下使用事件,而不至于乱传信息(EventType.MQEventType.message)。

业务抽象类接口

public abstract class LotteryService {

    private EventManager eventManager;

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

    public LotteryResult draw(String uId) {
        LotteryResult lotteryResult = doDraw(uId);
        // 需要什么通知就给调用什么方法
        eventManager.notify(EventManager.EventType.MQ, lotteryResult);
        eventManager.notify(EventManager.EventType.Message, lotteryResult);
        return lotteryResult;
    }

    protected abstract LotteryResult doDraw(String uId);

}
  • 这种使用抽象类的方式定义实现方法,可以再方法中扩展需要的额外调用。并提供抽象类abstract LotteryResult doDraw(String uId),让类的继承者实现。
  • 同时方法的定义使用的是protected,也就是保证将来外部调用方不会调用此方法,只有调用到draw(String uId),才能让我们完成事件通知。
  • 此种方法的实现就是在抽象类中写好一个基本的方法,在方法中完成新增逻辑的同时,再增加抽象类的使用。而这个抽象类的定义会由继承者实现。
  • 另外在构造函数中提供了对事件的定义:eventManager.subscribe(EventManager.EventType.MQ,new MQEventListener())
  • 在使用的时候也是使用枚举的方式进行通知使用,传来什么类型EventManager.EventType.MQ,就会执行什么事件通知,按需添加。

接口业务实现类

public class LotteryServiceImpl extends LotteryService {

    private MinibusTargetService minibusTargetService = new MinibusTargetService();

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

}

测试验证

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test() {
        LotteryService lotteryService = new LotteryServiceImpl();
        LotteryResult result = lotteryService.draw("2765789109876");
        logger.info("测试结果:{}", JSON.toJSONString(result));
    }

}

测试结果

14:30:00.339 [main] INFO  o.i.d.d.e.listener.MQEventListener - 记录用户 2765789109876 摇号结果(MQ):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
14:30:00.343 [main] INFO  o.i.d.d.e.l.MessageEventListener - 给用户 2765789109876 发送短信通知(短信):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
14:30:00.501 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:{"dateTime":1609050600336,"msg":"很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期","uId":"2765789109876"}

Process finished with exit code 0

总结

  • 从我们最基本的过程式开发以及后来使用观察者模式面向对象开发,可以看到设计模式改造后,拆分出了核心流程与辅助流程的代码。一般代码中的核心流程不会经常变化。但辅助流程会随着业务的各种变化而变化,包括:营销裂变促活等等,因此使用设计模式架构代码就显得非常有必要。
  • 此种设计模式从结构上是满足开闭原则的,当你需要新增其他的监听事件或者修改监听逻辑,是不需要改动事件处理类的。但是可能你不能控制调用顺序以及需要做一些事件结果的返回继续操作。
0

评论区