重學 Java 設計模式:實戰觀察者模式「模擬相似小客車指標搖號過程,監聽消息通知用戶中籤場景」


做者:小傅哥
博客:https://bugstack.cn - 原創系列專題文章html

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

知道的越多不知道的就越多java

編程開發這條路上的知識是無窮無盡的,就像之前你敢說精通Java,到後來學到愈來愈多隻想寫了解Java,過了幾年如今可能想說懂一點點Java。當視野和格局的擴大,會讓咱們愈來愈發現原來的見解是多麼淺顯,這就像站在地球看地球和站在宇宙看地球同樣。但正由於胸懷和眼界的提高讓咱們有了更多的認識,也逐漸學會了更多的技能。雖然不知道的愈來愈多,但也所以給本身填充了更多的技術棧,讓本身愈來愈強大。算法

拒絕學習的惰性很可怕編程

如今與之前不同,資料多、途徑廣,在這中間夾雜的廣告也很是多。這就讓不少初學者很難找到本身要的知識,最後看到有人推薦相關學習資料馬上屏蔽、刪除,但同時技術優秀的資料也不能讓須要的人看見了。長此以往把更多的時間精力都放在遊戲、娛樂、影音上,適當的放鬆是能夠的,但每每沉迷之後就很難出來,所以須要作好一些可讓本身成長的計劃,稍有剋制。設計模式

平衡好軟件設計和實現成本的度°微信

有時候一個軟件的架構設計須要符合當前條件下的各項因素,每每不能由於心中想固然的有某個藍圖,就去開始執行。也許雖然你的設計是很是優秀的,可是放在當前環境下很難知足業務的時間要求,當一個業務的基本訴求不能知足後,就很難拉動市場。沒有產品的DAU支撐,最後整個研發的項目也會所以停滯。但研發又不能一團亂麻的寫代碼,所以須要找好一個適合的度,好比能夠搭建良好的地基,實現上可擴展。但在具體的功能上能夠先簡化實現,隨着活下來了再繼續完善迭代。架構

2、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,能夠經過關注公衆號bugstack蟲洞棧,回覆源碼下載獲取(打開獲取的連接,找到序號18)
工程 描述
itstack-demo-design-18-00 場景模擬工程;模擬一個小客車搖號接口
itstack-demo-design-18-01 使用一坨代碼實現業務需求
itstack-demo-design-18-02 經過設計模式優化改造代碼,產生對比性從而學習

3、觀察者模式介紹

觀察者模式,圖片來自 refactoringguru.cn

簡單來說觀察者🕵模式,就是當一個行爲發生時傳遞信息給另一個用戶接收作出相應的處理,二者之間沒有直接的耦合關聯。例如;狙擊手、李雲龍。異步

李雲龍給你豎大拇指

除了生活中的場景外,在咱們編程開發中也會經常使用到一些觀察者的模式或者組件,例如咱們常用的MQ服務,雖然MQ服務是有一個通知中心並非每個類服務進行通知,但總體上也能夠算做是觀察者模式的思路設計。再好比可能有作過的一些相似事件監聽總線,讓主線服務與其餘輔線業務服務分離,爲了使系統下降耦合和加強擴展性,也會使用觀察者模式進行處理。ide

4、案例場景模擬

場景模擬;小客車指標搖號通知場景

在本案例中咱們模擬每次小客車指標搖號事件通知場景(真實的不會由官網給你發消息)函數

可能大部分人看到這個案例必定會想到本身每次搖號都不中的場景,收到一個遺憾的短信通知。固然目前的搖號系統並不會給你發短信,而是由百度或者一些其餘插件發的短信。那麼假如這個相似的搖號功能若是由你來開發,而且須要對外部的用戶作一些事件通知以及須要在主流程外再添加一些額外的輔助流程時該如何處理呢?

基本不少人對於這樣的通知事件類的實現每每比較粗獷,直接在類裏面就添加了。1是考慮🤔這可能不會怎麼擴展,2是壓根就沒考慮😄過。但若是你有仔細思考過你的核心類功能會發現,這裏面有一些核心主鏈路,還有一部分是輔助功能。好比完成了某個行爲後須要觸發MQ給外部,以及作一些消息PUSH給用戶等,這些都不算作是核心流程鏈路,是能夠經過事件通知的方式進行處理。

那麼接下來咱們就使用這樣的設計模式來優化重構此場景下的代碼。

1. 場景模擬工程

itstack-demo-design-18-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── MinibusTargetService.java
  • 這裏提供的是一個模擬小客車搖號的服務接口。

2. 場景簡述

2.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("在本次搖號未中籤或搖號資格已過時");
    }

}
  • 很是簡單的一個模擬搖號接口,與真實公平的搖號是有差異的。

5、用一坨坨代碼實現

這裏咱們先使用最粗暴的方式來實現功能

按照需求須要在原有的搖號接口中添加MQ消息發送以及短消息通知功能,若是是最直接的方式那麼能夠直接在方法中補充功能便可。

1. 工程結構

itstack-demo-design-18-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── LotteryResult.java
                ├── LotteryService.java
                └── LotteryServiceImpl.java
  • 這段代碼接口中包括了三部份內容;返回對象(LotteryResult)、定義接口(LotteryService)、具體實現(LotteryServiceImpl)。

2. 代碼實現

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消息,而這部分都是順序調用的。
  • 除了搖號接口調用外,後面的兩部分都是非核心主鏈路功能,並且會隨着後續的業務需求發展而不斷的調整和擴充,在這樣的開發方式下就很是不利於維護。

3. 測試驗證

3.1 編寫測試類

@Test
public void test() {
    LotteryService lotteryService = new LotteryServiceImpl();
    LotteryResult result = lotteryService.doDraw("2765789109876");
    logger.info("測試結果:{}", JSON.toJSONString(result));
}
  • 測試過程當中提供對搖號服務接口的調用。

3.2 測試結果

22:02:24.520 [main] INFO  o.i.demo.design.LotteryServiceImpl - 給用戶 2765789109876 發送短信通知(短信):很遺憾,編碼2765789109876在本次搖號未中籤或搖號資格已過時
22:02:24.523 [main] INFO  o.i.demo.design.LotteryServiceImpl - 記錄用戶 2765789109876 搖號結果(MQ):很遺憾,編碼2765789109876在本次搖號未中籤或搖號資格已過時
22:02:24.606 [main] INFO  org.itstack.demo.design.ApiTest - 測試結果:{"dateTime":1598764144524,"msg":"很遺憾,編碼2765789109876在本次搖號未中籤或搖號資格已過時","uId":"2765789109876"}

Process finished with exit code 0
  • 從測試結果上是符合預期的,也是日常開發代碼的方式,仍是很是簡單的。

6、觀察者模式重構代碼

接下來使用觀察者模式來進行代碼優化,也算是一次很小的重構。

1. 工程結構

itstack-demo-design-18-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── event
                │    ├── listener
                │    │    ├── EventListener.java
                │    │    ├── MessageEventListener.java
                │    │    └── MQEventListener.java
                │    └── EventManager.java
                ├── LotteryResult.java
                ├── LotteryService.java
                └── LotteryServiceImpl.java

觀察者模式模型結構

觀察者模式模型結構

  • 從上圖能夠分爲三大塊看;事件監聽事件處理具體的業務流程,另外在業務流程中 LotteryService 定義的是抽象類,由於這樣能夠經過抽象類將事件功能屏蔽,外部業務流程開發者不須要知道具體的通知操做。
  • 右下角圓圈圖表示的是核心流程與非核心流程的結構,通常在開發中會把主線流程開發完成後,再使用通知的方式處理輔助流程。他們能夠是異步的,在MQ以及定時任務的處理下,保證最終一致性。

2. 代碼實現

2.1 事件監聽接口定義

public interface EventListener {

    void doEvent(LotteryResult result);

}
  • 接口中定義了基本的事件類,這裏若是方法的入參信息類型是變化的可使用泛型<T>

2.2 兩個監聽事件的實現

短消息事件

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

}
  • 以上是兩個事件的具體實現,相對來講都比較簡單。若是是實際的業務開發那麼會須要調用外部接口以及控制異常的處理。
  • 同時咱們上面提到事件接口添加泛型,若是有須要那麼在事件的實現中就能夠按照不一樣的類型進行包裝事件內容。

2.3 事件處理類

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)。

2.4 業務抽象類接口

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,就會執行什麼事件通知,按需添加。

2.5 業務接口實現類

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

}
  • 如今再看業務流程的實現中能夠看到已經很是簡單了,沒有額外的輔助流程,只有核心流程的處理。

3. 測試驗證

3.1 編寫測試類

@Test
public void test() {
    LotteryService lotteryService = new LotteryServiceImpl();
    LotteryResult result = lotteryService.draw("2765789109876");
    logger.info("測試結果:{}", JSON.toJSONString(result));
}
  • 從調用上來看幾乎沒有區別,可是這樣的實現方式就能夠很是方便的維護代碼以及擴展新的需求。

3.2 測試結果

23:56:07.597 [main] INFO  o.i.d.d.e.listener.MQEventListener - 記錄用戶 2765789109876 搖號結果(MQ):很遺憾,編碼2765789109876在本次搖號未中籤或搖號資格已過時
23:56:07.600 [main] INFO  o.i.d.d.e.l.MessageEventListener - 給用戶 2765789109876 發送短信通知(短信):很遺憾,編碼2765789109876在本次搖號未中籤或搖號資格已過時
23:56:07.698 [main] INFO  org.itstack.demo.design.test.ApiTest - 測試結果:{"dateTime":1599737367591,"msg":"很遺憾,編碼2765789109876在本次搖號未中籤或搖號資格已過時","uId":"2765789109876"}

Process finished with exit code 0
  • 從測試結果上看知足😌咱們的預期,雖然結果是同樣的,但只有咱們知道了設計模式的魅力所在。

7、總結

  • 從咱們最基本的過程式開發以及後來使用觀察者模式面向對象開發,能夠看到設計模式改造後,拆分出了核心流程與輔助流程的代碼。通常代碼中的核心流程不會常常變化。但輔助流程會隨着業務的各類變化而變化,包括;營銷裂變促活等等,所以使用設計模式架設代碼就顯得很是有必要。
  • 此種設計模式從結構上是知足開閉原則的,當你須要新增其餘的監聽事件或者修改監聽邏輯,是不須要改動事件處理類的。可是可能你不能控制調用順序以及須要作一些事件結果的返回繼續操做,因此使用的過程時須要考慮場景的合理性。
  • 任何一種設計模式有時候都不是單獨使用的,須要結合其餘模式共同建設。另外設計模式的使用是爲了讓代碼更加易於擴展和維護,不能由於添加設計模式而把結構處理更加複雜以及難以維護。這樣的合理使用的經驗須要大量的實際操做練習而來。

8、推薦閱讀

相關文章
相關標籤/搜索