設計模式之觀察者模式

[toc]

設計模式之觀察者模式

## 1. 再談設計原則html

1.1 可維護性(Maintainability):

  • 可擴展性;
  • 靈活性;
  • 可插拔;

1.2 可複用性(Reuseability):

  • 代碼複用;
  • 算法複用;
  • 數據結構複用;

1.3 可維護性與可複用性的關係:

image.png

1.4 六大設計原則

1. 開閉原則-OCP(open-close-Principle)

軟件系統的設計, 應對擴展開放, 對修改關閉;

2. 里氏代換原則-LSP(Liskov-Substitution-Principle)

全部基類出現的地方, 均可以被子類替換;

3. 依賴倒置原則-DIP(Dependence-Inversion-Principle)

應該依賴於抽象而非具體;

4. 接口隔離原則-ISP(Interface-Segregation-Principle)

多個專門的接口好於單一的總接口;

5.合成/聚合複用原則-CARP(Composite/Aggregate-Reuse-Principle)

使用合成/聚合的方式複用, 而不是繼承;
聚合: 更強的合成;

6. 迪米特法則-LoD(Law-of-Demeter)

若非必要,不要暴露(最少知道原則);

2. 觀察者模式

2.1 始於回調(回調模式)

public class App {
    // 主業務流程
    public void doBusiness(ICallback callback){
        System.out.println("主流程工做...");
        callback.call();
    }
}

public interface ICallback {
    void call();
}

// 0. 回調操做隨主流程執行
@Test
void callbackTest() {
    App app = new App();
    app.doBusiness(() -> {
        System.out.println("回調操做...");
    });
}

image.png

2.2 觀察者模式-類圖

Subject/OneSubject: 被觀察對象(發佈者)
Observer: 觀察者(訂閱者)java

  1. 觀察者接口(訂閱者)算法

    /**
     * 觀察者(訂閱者)
     */
    public interface Observer {
    
     void update(String msg);
    }
  2. 觀察者/訂閱者1(實現類)spring

    /**
     * 觀察者/訂閱者1
     * @author nieweijun
     * @since 2021/6/1 22:09
     */
    public class OneObserver implements Observer{
     @Override
     public void update(String msg) {
         System.out.println("OneObserver收到消息:["+msg+"]");
     }
    }
  3. 觀察者實現類2shell

    /**
     * 觀察者/訂閱者1
     * @author nieweijun
     * @since 2021/6/1 22:09
     */
    public class OneObserver implements Observer{
     @Override
     public void update(String msg) {
         System.out.println("OneObserver收到消息:["+msg+"]");
     }
    }
  4. 被觀察者設計模式

    /**
     * 被觀察者(主題/發佈者)
     */
    public interface Subject {
     void addObserver(Observer observer);
    
     void removeObserver(Observer observer);
    
     void notifyObservers();
    }
  5. 被觀察者實現api

    /**
     * 主題/發佈者實現
     * @author nieweijun
     * @since 2021/6/1 22:12
     */
    public class OneSubject implements Subject{
     Vector<Observer> observers = null;
    
     public OneSubject(){
         this.observers = new Vector<>();
     }
     @Override
     public void addObserver(Observer observer) {
         observers.addElement(observer);
     }
    
     @Override
     public void removeObserver(Observer observer) {
         observers.removeElement(observer);
     }
    
     @Override
     public void notifyObservers() {
         for (Observer observer : observers) {
             observer.update("雷陣雨~");
         }
     }
    }
  6. 測試調用:數據結構

    // 1.普通觀察者
     @Test
     void myObserverTest() {
         // 1.被觀察者(主題)
         OneSubject subject = new OneSubject();
         // 2.觀察者1(訂閱者1)
         OneObserver obs1 = new OneObserver();
         // 3.觀察者2(訂閱者2)
         TwoObserver obs2 = new TwoObserver();
    
         // 註冊觀察者
         subject.addObserver(obs1);
         subject.addObserver(obs2);
    
         // 發佈消息(通知全部觀察者)
         subject.notifyObservers();
     }
    OneObserver收到消息:[雷陣雨~]
    TwoObserver收到消息:[雷陣雨~]

image.png

2.3 JDK中的觀察者

Observable/MySubject: 被觀察對象(發佈者)
Observer: 觀察者(訂閱者)app

  1. jdk中的發佈者實現類(被觀察者)異步

    import java.util.Observable;
    
    /**
     * JDK內置的觀察者模式用例
     * @author nieweijun
     * @since 2021/5/31 18:13
     */
    @Slf4j
    public class WeatherObservable extends Observable {
     @Override
     public String toString() {
         return "布穀天氣";
     }
    
     @Override
     public synchronized void setChanged() {
         super.setChanged();
     }
    }
  2. 測試用例調用

    // 2. jdk觀察者
     @Test
     void weatherReportTest() {
         WeatherObservable obs = new WeatherObservable(); // 主題
         // A
         obs.addObserver((o, arg) -> log.info("訂閱者-A收到[{}]發佈的消息:{}", o, arg));
         // B
         obs.addObserver((o, arg) -> log.info("訂閱者-B收到消息[{}]發佈的消息:{}", o, arg));
         // C
         obs.addObserver((o, arg) -> log.info("訂閱者-C收到[{}]發佈的消息:{}", o, arg));
    
         obs.setChanged();
         obs.notifyObservers("今日北京雷陣雨");
     }

    輸出:

    PatternObserverTest - 訂閱者-C收到[布穀天氣]發佈的消息:今日北京雷陣雨
    PatternObserverTest - 訂閱者-B收到消息[布穀天氣]發佈的消息:今日北京雷陣雨
    PatternObserverTest - 訂閱者-A收到[布穀天氣]發佈的消息:今日北京雷陣雨

    注意: 觀察者的實現用FunctionalInterface實現了, 3個觀察者;

image.png

2.4. jdk中的AWT監聽器

按鈕監聽事件案例:

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * java GUI 觀察者(監聽器)
 * @author nieweijun
 * @since 2021/6/1 22:21
 */
public class GuiObserver {

    public GuiObserver() {
        JFrame jFrame = new JFrame("HelloButton");

        // 1. button 事件源
        JButton btn = new JButton("click me~");

        // 2. 事件監聽器(攜帶監聽事件 ActionEvent)
        ActionListener myListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(null, "按鈕被點擊!");
            }
        };

        // 3. 註冊事件
        btn.addActionListener(myListener);
//        btn.addActionListener((e) -> JOptionPane.showMessageDialog(null, "按鈕被點擊!"));

        jFrame.add(btn);

        jFrame.setLayout(new FlowLayout());
        jFrame.setSize(500,400);
        jFrame.setLocation(400, 400);
        jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        jFrame.setVisible(true);
    }

    public static void main(String[] args) {
        new GuiObserver();
    }
}
  • ActionListener 監聽器(潛藏着事件源)
  • ActionEvent 事件
  • btn.addActionListener 客戶端註冊事件

3. 監聽器

案例: 回家事件; 兩個實現(一個是工做日回家; 一個是節假日回家; 輸出信息不同)

3.1. 事件定義(繼承EventObject)

import java.util.EventObject;

/**
 * 事件:(通常潛藏於Listener)
 * @author nieweijun
 * @since 2021/6/9 9:49
 */

public class HomeEvent extends EventObject {

    @Setter
    @Getter
    private String type;

    public HomeEvent(Object source, String type){
        super(source);
        this.type = type;
    }

    /**
     * @param source 事件源
     * @throws IllegalArgumentException
     */
    public HomeEvent(Object source) {
        super(source);
    }
}

3.2. 監聽器接口定義(繼承EventListener)

定義本身的回調接口, 拉上事件作參數
import java.util.EventListener;

/**
 * 回家事件監聽器
 * @author nieweijun
 * @since 2021/6/9 9:53
 */
public interface HomeListener extends EventListener {
    void onHomeEvent(HomeEvent event);
}

3.3. 監聽器接口實現

  • HolidayHomeListener
import lombok.extern.slf4j.Slf4j;

/**
 * 節假日回家事件
 * @author nieweijun
 * @since 2021/6/9 9:56
 */
@Slf4j
public class HolidayHomeListener implements HomeListener {
    @Override
    public void onHomeEvent(HomeEvent event) {
        if (event.getType().equals("holiday")) {
            log.info("@===============> HolidayHomeListener#onHomeEvent:放假回家, 開開心心的! type={}", event.getType());
        }
    }
}
  • WorkdayHomeListener
import lombok.extern.slf4j.Slf4j;

/**
 * 工做日回家事件
 * @author nieweijun
 * @since 2021/6/9 9:56
 */
@Slf4j
public class WorkdayHomeListener implements HomeListener {
    @Override
    public void onHomeEvent(HomeEvent event) {
        if (event.getType().equals("work")) {
            log.info("@===============> WorkdayHomeListener#onHomeEvent:工做日回家, 吾日三省吾身了沒? type={}", event.getType());
        }
    }
}

3.4. 管理者(Listener容器持有者)

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 事件發佈者
 * @author nieweijun
 * @since 2021/6/9 13:49
 */
public class Manager {
    private List<HomeListener> list;

    public Manager(){
        list = new ArrayList<>();
    }

    public void addListener(HomeListener listener) {
        if(list == null){
            list = new ArrayList<>();
        }
        list.add(listener);
    }

    public void removeListener(HomeListener listener) {
        if(list != null){
            list.remove(listener);
        }
    }

    /**
     * 通知全部的Listener
     */
    private void notifyListeners(HomeEvent event) {
        for (HomeListener listener : list) {
            listener.onHomeEvent(event);
        }
    }


    public void holidayGoHome(){
        HomeEvent event = new HomeEvent(this, "holiday");
        notifyListeners(event);
    }

    public void workdayGoHome(){
        HomeEvent event = new HomeEvent(this, "work");
        notifyListeners(event);
    }
}

注意: 兩個方法: holidayGoHomeworkdayGoHome 兩個方法, 作了發佈事件的調用, 並觸發通知事件(通知全部的監聽者)

3.5. 測試用例調用

// 5. 自定義監聽器 測試用例
    @Test
    void testListener() {
        Manager manager = new Manager();
        manager.addListener(new HolidayHomeListener());
        manager.addListener(new WorkdayHomeListener());
        // 工做日回家
        manager.workdayGoHome();
        // 節假日回家
        manager.holidayGoHome();
    }

輸出:

WorkdayHomeListener - @===============> WorkdayHomeListener#onHomeEvent:工做日回家, 吾日三省吾身了沒? type=work
HolidayHomeListener - @===============> HolidayHomeListener#onHomeEvent:放假回家, 開開心心的! type=holiday

4. 觀察者和監聽器比較

4.1. 事件監聽器三要素:

  • 事件源
  • 事件
  • 事件監聽器

4.2. 觀察者而要素:

  • 被觀察者(主題)
  • 觀察者(訂閱者/接收者/被通知者)

4.3. 比較: 如圖

事件+事件源的做用就是被觀察者;

image.png

5. 模式實踐經典應用

5.1. Spring事件機制

  • ApplicationListener/@EventListener+ApplicationEvent
  • ApplicationEvent
  • ApplicationEventPublisher

    1. 事件:

    import lombok.Getter;
    import org.springframework.context.ApplicationEvent;
    
    /**
     * 觀察者
     * @author nieweijun
     * @since 2021/5/31 14:15
     */
    public class RegisterEvent extends ApplicationEvent {
    
        /**
         * 登陸用戶用戶名
         */
        @Getter
        private String userName;
    
        /**
         * Create a new {@code ApplicationEvent}.
         * @param source the object on which the event initially occurred or with
         *               which the event is associated (never {@code null})
         */
        public RegisterEvent(Object source) {
            super(source);
        }
    
        public RegisterEvent(Object source, String userName) {
            super(source);
            this.userName = userName;
        }
    }

2. 監聽器1:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;

/**
 * 發放優惠券
 * @author nieweijun
 * @since 2021/5/31 14:25
 */
@Service
@Slf4j
public class CouponServiceListener implements ApplicationListener<RegisterEvent> {
    @Override
    public void onApplicationEvent(RegisterEvent event) {
        log.info("#=====>[CouponService.onApplicationEvent] 給用戶{}發放3張滿減新手券! =====#", event.getUserName());
    }
}

2. 監聽器2:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * @author nieweijun
 * @since 2021/5/31 14:23
 */
@Service
@Slf4j
public class EmailServiceListener implements ApplicationListener<RegisterEvent> {
    @Async // 異步發郵件
    @Override
    public void onApplicationEvent(RegisterEvent event) {
        log.info("#=====>[EmailService.onApplicationEvent] 執行發郵件給用戶: {} =====# ", event.getUserName());
    }
}

2. 監聽器3 (註解方式):

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

/**
 * 新用戶告知消息
 * @author nieweijun
 * @since 2021/5/31 14:29
 */
@Service
@Slf4j
public class RegisterNoticeService {

    @EventListener
    public void notice(RegisterEvent event) {
        log.info("#=====>[RegisterNoticeService.notice] 消息告知:{} 你好! 歡迎你加入社團, 您須要遵照如下規定 1, 2, 3, 4, 5 =====#", event.getUserName());
    }
}

3. 事件發佈:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class UserBizService implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void register(String userName){
        // 1. 執行註冊邏輯
        log.info("#=====>用戶 [{}] 註冊邏輯成功!", userName);

        // 2. 發佈事件
        applicationEventPublisher.publishEvent(new RegisterEvent(this, userName));
    }
}

4. 測試用例:

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

/**
 * 設計模式測試用例
 * @author nieweijun
 * @since 2021/6/1 18:20
 */
@Slf4j
@SpringBootTest
public class PatternObserverTest {

    @Resource
    private UserBizService userBizService;
    
    // 3. spring事件監聽測試
    @Test
    void testRegister() { //@SpringBootTest
        userBizService.register("niewj");
    }
    
}

5. 執行結果:

listener.UserBizService   : #=====>用戶 [niewj] 註冊邏輯成功!
listener.RegisterNoticeService   : #=====>[RegisterNoticeService.notice] 消息告知:niewj 你好! 歡迎你加入社團, 您須要遵照如下規定 1, 2, 3, 4, 5 =====#
listener.CouponServiceListener   : #=====>[CouponService.onApplicationEvent] 給用戶niewj發放3張滿減新手券! =====#
listener.EmailServiceListener    : #=====>[EmailService.onApplicationEvent] 執行發郵件給用戶: niewj =====#

5.2. GUI之AWT事件監聽器

  • ActionListener
  • ActionEvent
  • btn.addActionListener

    1. button單擊事件綁定監聽

    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    
    /**
     * java GUI 觀察者(監聽器)
     * @author nieweijun
     * @since 2021/6/1 22:21
     */
    public class GuiObserver {
    
        public GuiObserver() {
            JFrame jFrame = new JFrame("HelloButton");
    
            // 1. button 事件源
            JButton btn = new JButton("click me~");
    
            // 2. 事件監聽器(攜帶監聽事件 ActionEvent)
            ActionListener myListener = new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    JOptionPane.showMessageDialog(null, "按鈕被點擊!");
                }
            };
    
            // 3. 註冊事件
            btn.addActionListener(myListener);
    //        btn.addActionListener((e) -> JOptionPane.showMessageDialog(null, "按鈕被點擊!"));
    
            jFrame.add(btn);
    
            jFrame.setLayout(new FlowLayout());
            jFrame.setSize(500,400);
            jFrame.setLocation(400, 400);
            jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            jFrame.setVisible(true);
        }
    
        public static void main(String[] args) {
            new GuiObserver();
        }
    }

執行結果:

image.png

5.3. 谷歌Guava之EventBus

  • @subscribe
  • EventBus#register#post

    import com.google.common.eventbus.Subscribe;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 事件監聽器
     * @author nieweijun
     * @since 2021/6/9 13:34
     */
    @Slf4j
    public class EventListener {
    
        @Subscribe
        public void listenString(String msg){
          log.info("#======>EventListener.listenString:{}", msg);
        }
    
        @Subscribe
        public void listenInteger(Integer num){
          log.info("@======>EventListener.listenInteger:{}", num);
        }
    }

調用測試:

// 4. Guava EventBus 簡單測試用例
@Test
void testEventBus() {
    EventBus eventBus = new EventBus();
    // register
    eventBus.register(new EventListener());
    // post
    eventBus.post("somename");
    eventBus.post(10);

}

輸出結果:

eventBus.EventListener - #======>EventListener.listenString:somename
eventBus.EventListener - @======>EventListener.listenInteger:10

6. 參考資料

相關文章
相關標籤/搜索