观察者(Observer)

Intent

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

主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。

Class Diagram

主题(Subject)具有注册和移除观察者、并通知所有观察者的功能,主题是通过维护一张观察者列表来实现这些操作的。

观察者(Observer)的注册功能需要调用主题的 registerObserver() 方法。

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

Implementation

天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。

public interface Subject {
    void registerObserver(Observer o);
​
    void removeObserver(Observer o);
​
    void notifyObserver();
}
public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;
​
    public WeatherData() {
        observers = new ArrayList<>();
    }
​
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObserver();
    }
​
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }
​
    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }
​
    @Override
    public void notifyObserver() {
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }
}
public interface Observer {
    void update(float temp, float humidity, float pressure);
}
public class StatisticsDisplay implements Observer {
​
    public StatisticsDisplay(Subject weatherData) {
        weatherData.registerObserver(this);
    }
​
    @Override
    public void update(float temp, float humidity, float pressure) {
        System.out.println("StatisticsDisplay.update: " + temp + " " + humidity + " " + pressure);
    }
}
public class CurrentConditionsDisplay implements Observer {
​
    public CurrentConditionsDisplay(Subject weatherData) {
        weatherData.registerObserver(this);
    }
​
    @Override
    public void update(float temp, float humidity, float pressure) {
        System.out.println("CurrentConditionsDisplay.update: " + temp + " " + humidity + " " + pressure);
    }
}
public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
​
        weatherData.setMeasurements(0, 0, 0);
        weatherData.setMeasurements(1, 1, 1);
    }
}

Business scenario

模拟摇号通知

小客车摇号,摇到号后进行事件通知,例如MQ消息处理、短信、应用通知等等

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

观察者模式实现

  • 从上图可以分为三大块看;事件监听、事件处理、具体的业务流程,另外在业务流程中Lotteryservice定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。

  • 右下角圆圈图表示的是核心流程与非核心流程的结构,一般在开发中会把主线流程开发完成后,再使用通知的方式处理辅助流程。他们可以是异步的,在MQ以及定时任务的处理下,保证最终一致性

事件监听实现

public interface EventListener {
​
    void doEvent(LotteryResult result);
​
}
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 MessageEventListener implements EventListener {
​
    private Logger logger = LoggerFactory.getLogger(MessageEventListener.class);
​
    @Override
    public void doEvent(LotteryResult result) {
        logger.info("给用户 {} 发送短信通知(短信):{}", 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);
        }
    }
​
}

摇号模拟

摇号结果

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 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);
​
}

摇号实现类

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("123");
        logger.info("测试结果:{}", JSON.toJSONString(result));
    }
​
}

Summary

观察者的有点类似于发布和订阅,发布和订阅在我们开发中经常用到。通过观察者可以将核心业务跟一些辅助业务区分开,这样核心流程不会变,也方便添加辅助流程。