Java设计模式:四、行为型模式-06:观察者模式

文章目录

  • 一、定义:观察者模式
  • 二、模拟场景:观察者模式
    • [2.1 观察者模式](#2.1 观察者模式)
    • [2.2 引入依赖](#2.2 引入依赖)
    • [2.3 工程结构](#2.3 工程结构)
    • [2.4 模拟摇号](#2.4 模拟摇号)
      • [2.4.1 摇号服务接口](#2.4.1 摇号服务接口)
      • [2.4.2 摇号返回结果类](#2.4.2 摇号返回结果类)
  • 三、违背方案:观察者模式
    • [3.0 引入依赖](#3.0 引入依赖)
    • [3.1 工程结构](#3.1 工程结构)
    • [3.2 添加摇号接口和实现](#3.2 添加摇号接口和实现)
      • [3.2.1 摇号服务接口](#3.2.1 摇号服务接口)
      • [3.2.2 摇号服务接口实现类](#3.2.2 摇号服务接口实现类)
    • [3.3 单元测试](#3.3 单元测试)
  • 四、改善代码:观察者模式
    • [4.0 引入依赖](#4.0 引入依赖)
    • [4.1 工程结构](#4.1 工程结构)
    • [4.2 观察者模式结构图](#4.2 观察者模式结构图)
    • [4.3 添加事件监听和管理器](#4.3 添加事件监听和管理器)
      • [4.3.1 定义事件监听接口](#4.3.1 定义事件监听接口)
      • [4.3.2 短信息事件监听接口实现](#4.3.2 短信息事件监听接口实现)
      • [4.3.3 MQ发送事件监听接口实现](#4.3.3 MQ发送事件监听接口实现)
      • [4.3.4 事件处理器](#4.3.4 事件处理器)
    • [4.4 摇号抽象类及其实现](#4.4 摇号抽象类及其实现)
      • [4.4.1 业务抽象类](#4.4.1 业务抽象类)
      • [4.4.2 业务接口实现类](#4.4.2 业务接口实现类)
    • [4.5 单元测试](#4.5 单元测试)
  • 五、总结:观察者模式

一、定义:观察者模式

  • **观察者模式:**当一个行为发生时传递信息给另外一个用户接收做出相应的处理,两者之间没有直接的耦合关联。
  • 除了生活中的场景外,在我们编程开发中也会常用到一些观察者的模式或者组件。例如:
    • 经常使用的 MQ 服务:虽然 MQ 服务是有一个通知中心并不是每一个类服务进行通知,但整体上也可以算作是观察者模式的思路设计。
    • 类似事件监听总线:让主线服务与其他辅线业务分离,为了使系统降低耦合和增强扩展性,也会使用观察者模式进行处理。

二、模拟场景:观察者模式

2.1 观察者模式

  • 模拟每次小客车指标摇号事件通知场景
  • 可能大部分人看到这个案例一定会想到自己每次摇号都不中的场景,收到一个遗憾的短信通知。当然目前的摇号系统不会给你发短信,而是由百度或者一些其他插件发的短信。
  • 那么假如这个类似的摇号功能由你来开发,并且需要对外部的用户做一些事件通知以及需要在主流外再添加一些额外的辅助流程时该如何处理呢?
    • 基本很多人对于这样的通知事件类的实现往往比较粗犷,直接在类里面添加就可以了。
      • ①考虑这可能不会怎么扩展,②是压根就没考虑过扩展。
    • 但如果你有仔细思考过你的核心类功能会发现,这里面有一些核心主链路,还有一部分是辅助功能。
      • 比如完成了某个行为后需要触发 MQ 给外部,以及做一些消息 PUSH 给用户等。
      • 这些都不算做是核心流程链路,是可以通过事件通知的方式进行处理。

2.2 引入依赖

pom.xml

xml 复制代码
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
    <!-- LOGGING begin -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.5</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.5</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.0.9</version>
        <exclusions>
            <exclusion>
                <artifactId>slf4j-api</artifactId>
                <groupId>org.slf4j</groupId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

2.3 工程结构

bash 复制代码
design-19.0-0
|------src
	|------main
		|--java
			|--com.lino.design
				|-LotteryResult.java
				|-MinibusTargetService.java

2.4 模拟摇号

2.4.1 摇号服务接口

MinibusTargetService.java

java 复制代码
package com.lino.design;

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

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

2.4.2 摇号返回结果类

LotteryResult.java

java 复制代码
package com.lino.design;

import java.util.Date;

/**
 * @description: 摇号结果类
 */
public class LotteryResult {

    /**
     * 用户ID
     */
    private String uId;
    /**
     * 摇号信息
     */
    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;
    }
}

三、违背方案:观察者模式

按照需求需要在原有的摇号接口中添加 MQ 消息发送以及短信息通知功能。

如果是最直接的方式那么可以直接在方法中补充功能即可。

3.0 引入依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>com.lino</groupId>
        <artifactId>design-19.0-0</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

3.1 工程结构

bash 复制代码
design-19.0-1
|------src
	|------main
		|--java
			|--com.lino.design
				|-LotteryService.java
				|-LotteryServiceImpl.java
		|--test
			|--com.lino.design.test
				|-ApiTest.java

3.2 添加摇号接口和实现

3.2.1 摇号服务接口

LotteryService.java

java 复制代码
package com.lino.design;

/**
 * @description: 摇号接口
 */
public interface LotteryService {

    /**
     * 摇号
     *
     * @param uId 用户编号
     * @return 摇号结果
     */
    LotteryResult doDraw(String uId);
}

3.2.2 摇号服务接口实现类

LotteryServiceImpl.java

java 复制代码
package com.lino.design;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;

/**
 * @description: 摇号接口实现类
 */
public class LotteryServiceImpl implements LotteryService {

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

    private MinibusTargetService minibusTargetService = new MinibusTargetService();

    @Override
    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 消息,而这部分都是顺序调用的。
  • 除了 摇号 接口调用外,后面的两部分都是非核心主链路功能,而且会随着后续的业务需求发展而不断的调整和扩充,在这样的开发方式下就非常不利于维护。

3.3 单元测试

ApiTest.java

java 复制代码
package com.lino.design.test;

import com.alibaba.fastjson.JSON;
import com.lino.design.LotteryResult;
import com.lino.design.LotteryService;
import com.lino.design.LotteryServiceImpl;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @description: 单元测试
 */
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));
    }
}
  • 测试过程中提供对摇号服务接口的调用

测试结果

java 复制代码
17:06:14.931 [main] INFO  com.lino.design.LotteryServiceImpl - 给用户 2765789109876 发送短信通知(短信):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
17:06:14.931 [main] INFO  com.lino.design.LotteryServiceImpl - 记录用户 2765789109876 摇号结果(MQ):很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期
17:06:15.122 [main] INFO  com.lino.design.test.ApiTest - 测试结果:{"dateTime":1675760774946,"msg":"很遗憾,编码2765789109876在本次摇号未中签或摇号资格已过期","uId":"2765789109876"}

四、改善代码:观察者模式

4.0 引入依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>com.lino</groupId>
        <artifactId>design-19.0-0</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

4.1 工程结构

jsx 复制代码
design-19.0-2
|------src
	|------main
		|--java
			|--com.lino.design
				|--event
				|		|--listener
				|		|		|--EventListener.java
				|		|		|--MessageEventListener.java
				|		|		|--MQEventListener.java
				|		|--EventManager.java
				|-LotteryService.java
				|-LotteryServiceImpl.java
		|--test
			|--com.lino.design.test
				|-ApiTest.java

4.2 观察者模式结构图

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

4.3 添加事件监听和管理器

4.3.1 定义事件监听接口

EventListener.java

java 复制代码
package com.lino.design.event.listener;

import com.lino.design.LotteryResult;

/**
 * @description: 事件监听接口
 */
public interface EventListener {

    /**
     * 监听事件
     *
     * @param result 摇号结果
     */
    void doEvent(LotteryResult result);
}
  • 接口定义了基本的事件类,这里如果方法的入参信息类型是变化的,可以使用泛型 T

4.3.2 短信息事件监听接口实现

MessageEventListener.java

java 复制代码
package com.lino.design.event.listener;

import com.lino.design.LotteryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @description: 短消息事件监听实现
 */
public class MessageEventListener implements EventListener {

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

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

4.3.3 MQ发送事件监听接口实现

MQEventListener.java

java 复制代码
package com.lino.design.event.listener;

import com.lino.design.LotteryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @description: 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());
    }
}

4.3.4 事件处理器

EventManager.java

java 复制代码
package com.lino.design.event;

import com.lino.design.LotteryResult;
import com.lino.design.event.listener.EventListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @description: 事件处理器
 */
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

4.4 摇号抽象类及其实现

4.4.1 业务抽象类

LotteryService.java

java 复制代码
package com.lino.design;

import com.lino.design.event.EventManager;
import com.lino.design.event.listener.MQEventListener;
import com.lino.design.event.listener.MessageEventListener;

/**
 * @description: 摇号抽象类
 */
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;
    }

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

4.4.2 业务接口实现类

LotteryServiceImpl.java

java 复制代码
package com.lino.design;

import java.util.Date;

/**
 * @description: 摇号服务实现
 * @author: lingjian
 * @createDate: 2023/2/6 17:02
 */
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());
    }
}
  • 业务流程的实现,没有额外的辅助流程,只有核心流程的处理。

4.5 单元测试

ApiTest.java

java 复制代码
package com.lino.design.test;

import com.alibaba.fastjson.JSON;
import com.lino.design.LotteryResult;
import com.lino.design.LotteryService;
import com.lino.design.LotteryServiceImpl;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @description: 单元测试
 */
public class ApiTest {

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

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

测试结果

java 复制代码
19:45:45.314 [main] INFO  c.l.d.event.listener.MQEventListener - 记录用户 1000000101010019 摇号结果(MQ):恭喜你,编码1000000101010019在本次摇号中签
19:45:45.319 [main] INFO  c.l.d.e.l.MessageEventListener - 给用户 1000000101010019 发送短信通知(短信):恭喜你,编码1000000101010019在本次摇号中签
19:45:45.398 [main] INFO  com.lino.design.test.ApiTest - 测试结果:{"dateTime":1675770345311,"msg":"恭喜你,编码1000000101010019在本次摇号中签","uId":"1000000101010019"}

五、总结:观察者模式

  • 从基本的过程式开发,到使用观察者模式面向对象开发,可以看到使用设计模式改造后,拆分出来核心流程与辅助流程的代码。
    • 代码中的核心流程一般不会发生经常变化,辅助流程会随着业务的变化而变化,包括营销、裂变和促活等。
    • 因此使用设计模式编码就显得非常有必要。
  • 观察者模式从结构上满足开闭原则,当需要新增其他的监听事件或修改监听逻辑时,不需要改动事件处理类。
  • 观察者模式可能不能控制调用顺序以及需要做一些事件结果的返回操作,所以在使用的过程时需要考虑场景的适用性。
相关推荐
渊渟岳2 分钟前
掌握设计模式--装饰模式
设计模式
忒可君29 分钟前
C# winform 报错:类型“System.Int32”的对象无法转换为类型“System.Int16”。
java·开发语言
斌斌_____44 分钟前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@1 小时前
Spring如何处理循环依赖
java·后端·spring
一个不秃头的 程序员1 小时前
代码加入SFTP JAVA ---(小白篇3)
java·python·github
丁总学Java1 小时前
--spring.profiles.active=prod
java·spring
上等猿2 小时前
集合stream
java
java1234_小锋2 小时前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i2 小时前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
林的快手2 小时前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode