設計模式學習筆記(十九)觀察者模式及應用場景

歸斯君發表於2022-04-09

觀察者模式(Observer Design Pattern),也叫做釋出訂閱模式(Publish-Subscribe Design Pattern)、模型-檢視(Model-View)模式、源-監聽器(Source-Listener)模式、從屬者(Dependents)模式。指在物件之間定義一個一對多的依賴,當一個物件狀態改變的時候,所有依賴的物件都會自動收到通知。

比如說Redis 中的基於頻道的釋出訂閱就是觀察者模式的應用:

image-20220212142801417

一、觀察者模式的介紹

觀察者模式是一種物件行為型模式,下面就來看看觀察者模式的結構及其實現:

1.1 觀察者模式的結構

觀察者模式結構中主要包括觀察目標(Object)和觀察者(Observer)主要結構:

image-20220409112301928

  • Subject:主題抽象類,提供一系列觀察者物件,以及對這些物件的增加、刪除和通知的方法
  • ConcreteSubject:主題具體實現類,實現抽象主題中的通知方法,通知所有註冊過的觀察者物件
  • Observer:觀察者抽象類,包含一個通知響應抽象方法
  • ConcreteObserver1、ConcreteObserver2:觀察者實現類,實現抽象觀察者中的方法,以便在得到目標的更改通知時更新自身的狀態
  • Client:客戶端,對主題及觀察者進行呼叫

1.2 觀察者模式的實現

根據上面的類圖,我們可以實現對應的程式碼。

首先定義一個抽象目標類Subject,其中包括增加、登出和通知觀察者方法

public abstract class Subject {

    protected List<Observer> observerList = new ArrayList<Observer>();

    /**
     * 增加觀察者
     * @param observer 觀察者
     */
    public void add(Observer observer) {
        observerList.add(observer);
    }

    /**
     * 登出觀察者,從觀察者集合中刪除一個觀察者
     * @param observer 觀察者
     */
    public void remove(Observer observer) {
        observerList.remove(observer);
    }

    /**通知觀察者*/
    public abstract void notifyObserver();
}

對應具體的目標類ConcreteSubject

public class ConcreteSubject extends Subject{

    @Override
    public void notifyObserver() {
        System.out.println("遍歷觀察者:");
        for (Observer observer : observerList) {
            observer.response();
        }
    }
}

此外需要定義抽象觀察者Observer,它一般定義為一個介面,宣告一個response()方法,為不同觀察者的響應行為定義相同的介面:

public interface Observer {
    /**宣告響應方法*/
    void response();
}

具體的觀察者實現:

public class ConcreteObserver1 implements Observer{

    @Override
    public void response() {
        System.out.println("我是具體觀察者ConcreteObserver1");
    }
}

public class ConcreteObserver2 implements Observer{

    @Override
    public void response() {
        System.out.println("我是具體觀察者ConcreteObserver2");
    }
}

最後是客戶端測試:

public class Client {
    public static void main(String[] args) {
        Subject concreteSubject = new ConcreteSubject();
        //具體觀察者
        Observer concreteObserver1 = new ConcreteObserver1();
        Observer concreteObserver2 = new ConcreteObserver2();
        concreteSubject.add(concreteObserver1);
        concreteSubject.add(concreteObserver2);
        
        concreteSubject.notifyObserver();
    }
}

測試結果:

遍歷觀察者:
我是具體觀察者ConcreteObserver1
我是具體觀察者ConcreteObserver2

二、觀察者模式的應用場景

在以下情況就可以考慮使用觀察者模式:

  1. 一個物件的改變會導致一個或多個物件發生改變,而並不知道具體有多少物件將會發生改變,也不知道這些物件是誰
  2. 當一個抽象模型有兩個方面,其中的一個方面依賴於另一個方面時,可將這兩者封裝在獨立的物件中以使他們可以各自獨立地改變和複用
  3. 需要在系統中建立一個觸發鏈,使得事件擁有跨域通知(跨越兩種觀察者的型別)

2.1 觀察者模式在java.util包中的應用

觀察者模式在JDK中就有典型應用,比如java.util.Observablejava.util.Observer類。結構如下圖所示:

image-20220409083948434

我們可以通過實現具體的ConcreteObserver和具體的ConcreteObservable完成觀察者模式流程

2.2 觀察者模式在MVC中的應用

MVC(Modew-View-Controller)架構中也應用了觀察者模式,其中模型(Model)可以對應觀察者模式中的觀察目標,而檢視(View)對應於觀察者,控制器(Controller)就是中介者模式的應用:

image-20220409091533004

三、觀察者模式實戰

在本案例中模擬北京小客車指標搖號事件的通知場景(來源於《重學Java設計模式》)

image-20220409092520707

對於通知事件,可以將其分成三個部分:事件監聽事件處理具體的業務流程,如下圖所示:

image-20220409095032686

對於和核心流程和非核心流程的結構,非核心流程可以是非同步的,在MQ以及定時任務的處理下,能夠最終保證一致性。

具體程式碼實現

  1. 事件監聽介面及具體實現

這個部分就相當於觀察者(Observer)的角色

在介面中定義基本事件類方法doEvent()

public interface EventListener {

    void doEvent(LotteryResult result);

}

監聽事件的具體實現MessageEventListener(短訊息事件)和MQEventListener(MQ傳送事件)

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 MQEventListener implements EventListener{

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

    @Override
    public void doEvent(LotteryResult result) {
        logger.info("記錄使用者 {} 搖號結果(MQ):{}", result.getuId(), result.getMsg());
    }
}
  1. 事件處理類

該部分就相當於主題(Object)部分

對於不同的事件型別(MQ和Message)進行列舉處理,並提供三個方法:subscribe()unsubscribe()notify()用於對監聽事件的註冊和使用:

public class EventManager {

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

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

    public enum EventType {
        MQ,
        Message
    }

    /**
     * 訂閱
     * @param eventType 事件型別
     * @param listener  監聽
     */
    public void subscribe(Enum<EventType> eventType, EventListener listener) {
        List<EventListener> eventListeners = listeners.get(eventType);
        eventListeners.add(listener);
    }

    /**
     * 取消訂閱
     * @param eventType 事件型別
     * @param listener 監聽
     */
    public void unsubscribe(Enum<EventType> eventType, EventListener listener) {
        List<EventListener> eventListeners = listeners.get(eventType);
        eventListeners.remove(listener);
    }

    /**
     * 通知
     * @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);
        }
    }
}
  1. 業務抽象類介面及其實現

使用抽象類的方式實現方法,好處是可以在方法中擴充套件額外的呼叫,並提供抽象方法doDraw,讓繼承者去實現具體邏輯

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());
    }
}
  1. 其他的類

搖號服務介面:

/**
 * 小客車指標調控服務
 */
public class MinibusTargetService {

    /**
     * 模擬搖號,但不是搖號演算法
     *
     * @param uId 使用者編號
     * @return 結果
     */
    public String lottery(String uId) {
        return Math.abs(uId.hashCode()) % 2 == 0 ? "恭喜你,編碼".concat(uId).concat("在本次搖號中籤") : "很遺憾,編碼".concat(uId).concat("在本次搖號未中籤或搖號資格已過期");
    }

}

事件資訊返回類:

public class LotteryResult {

    private String uId;
    private String msg;
    private Date dateTime;

    //get set constructor... 
}
  1. 測試類
public class ApiTest {

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

    @Test
    public void test() {
        LotteryServiceImpl lotteryService = new LotteryServiceImpl();
        LotteryResult result = lotteryService.draw("1234567");
        logger.info("搖號結果:{}", JSON.toJSONString(result));
    }
}

測試結果:

11:43:09.284 [main] INFO  c.e.d.event.listener.MQEventListener - 記錄使用者 1234567 搖號結果(MQ):恭喜你,編碼1234567在本次搖號中籤
11:43:09.288 [main] INFO  c.e.d.e.l.MessageEventListener - 給使用者 1234567 傳送簡訊通知(簡訊):恭喜你,編碼1234567在本次搖號中籤
11:43:09.431 [main] INFO  ApiTest - 搖號結果:{"dateTime":1649475789279,"msg":"恭喜你,編碼1234567在本次搖號中籤","uId":"1234567"}

參考資料

《重學Java設計模式》

《設計模式》

http://c.biancheng.net/view/1390.html

相關文章