• 对观察者模式的理解


    一、场景

    • 观察者模式是行为型模式之一。
      • 类与类之间如何协作才算观察者模式呢?
    • 试想一下:
      • 学生们坐在教室上课,到了下课时刻,下课铃声响起,学生们听到铃声,进入课间休息时段。
        • 学生是观察者,观察对象是铃声通知器(一般挂在教室门口的墙上)。
    • 这种协作便是观察者模式。

    1、题目描述 【案例来源

    小明所在的学校有一个时钟(主题),每到整点时,它就会通知所有的学生(观察者)当前的时间,请你使用观察者模式实现这个时钟通知系统。
    注意点:时间从 1 开始,并每隔一个小时更新一次。

    2、输入描述

    输入的第一行是一个整数 N(1 ≤ N ≤ 20),表示学生的数量。
    接下来的 N 行,每行包含一个字符串,表示学生的姓名。
    最后一行是一个整数,表示时钟更新的次数。

    3、输出描述

    对于每一次时钟更新,输出每个学生的姓名和当前的时间。

    4、输入示例

    2
    Alice
    Bob
    3

    5、输出示例

    Alice 1
    Bob 1
    Alice 2
    Bob 2
    Alice 3
    Bob 3

    二、实现

    • 主题(Subject): 铃声通知器

    主题状态(数据)变化后,通知订阅了该主题的观察者。

    亦称:发布者(Publisher)
    发布者状态(数据)变化后,通知订阅者(Subscriber)/观察者。

    public interface Subject {
        void registerObserver(Observer observer);
        void removeObserver(Observer observer);
        void notifyObservers();
    }
    
    public class TimeSubject implements Subject {
        private List<Observer> observers;
    
        @Setter
        private Integer startTime;
    
        public TimeSubject(Integer startTime) {
            this.startTime = startTime;
            this.observers = new ArrayList<>();
        }
    
        @Override
        public void registerObserver(Observer observer) {
            observers.add(observer);
        }
    
        @Override
        public void removeObserver(Observer observer) {
            observers.remove(observer);
        }
    
        @Override
        public void notifyObservers() {
            for (Observer observer : observers) {
                observer.update(startTime);
            }
            startTime = (startTime + 1) % 24;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 观察者/订阅者
    public interface Observer {
        void update(Integer currentTime);
    }
    
    public class StudentObserver implements Observer {
        private String name;
    
        public StudentObserver(String name) {
            this.name = name;
        }
    
        @Override
        public void update(Integer currentTime) {
            System.out.println(name + " " + currentTime);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 客户端
    public class Application {
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            TimeSubject timeSubject = new TimeSubject(1);
    
            int n = scanner.nextInt();
            for (int i = 0; i < n; i++) {
                String name = scanner.next();
                Observer studentObserver = new StudentObserver(name);
                timeSubject.registerObserver(studentObserver);
            }
    
            int frequency = scanner.nextInt();
            for (int i = 0; i < frequency; i++) {
                timeSubject.notifyObservers();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    三、更复杂的场景 【案例来源

    • 编辑器提供两种功能:打开文件 和 保存文件。
      • 当打开文件时,发邮件通知监听该行为的观察者。
      • 当保存文件时,会触发打印日志。

    分析:

    • 客户端使用编辑器,当出现“打开文件”或者“保存文件”时,触发发送邮件或打印日志。
      • 很显然,编辑器是“铃声通知器”,是发布者。邮件监听、日志监听是观察者。

    1、简单实现

    • 发布者
    public interface Publisher {
        void registerObserver(String observeType, Observer observer);
        void removeObserver(String observeType, Observer observer);
        void notifyObservers(String observeType);
    }
    
    public class Editor implements Publisher {
        private List<Observer> emailObservers;
        private List<Observer> logObservers;
    
        private File file;
    
        public Editor() {
            emailObservers = new ArrayList<>();
            logObservers = new ArrayList<>();
        }
    
        @Override
        public void registerObserver(String observeType, Observer observer) {
            if ("open file".equals(observeType)) {
                emailObservers.add(observer);
            } else if ("save file".equals(observeType)) {
                logObservers.add(observer);
            } else {
                throw new RuntimeException("invalid observeType");
            }
        }
    
        @Override
        public void removeObserver(String observeType, Observer observer) {
            if ("open file".equals(observeType)) {
                emailObservers.remove(observer);
            } else if ("save file".equals(observeType)) {
                logObservers.remove(observer);
            } else {
                throw new RuntimeException("invalid observeType");
            }
        }
    
        @Override
        public void notifyObservers(String observeType) {
            ObserveContext observeContext = new ObserveContext()
                    .setObserveType(observeType)
                    .setFile(file);
    
            if ("open file".equals(observeType)) {
                emailObservers.stream().forEach(observer -> observer.update(observeContext));
    
            } else if ("save file".equals(observeType)) {
                logObservers.stream().forEach(observer -> observer.update(observeContext));
    
            } else {
                throw new RuntimeException("invalid observeType");
            }
        }
    
        public void openFile(String filePath) {
            if (StringUtils.isEmpty(filePath)) {
                throw new RuntimeException("filePath is empty");
            }
    
            this.file = new File(filePath);
    
            notifyObservers("open file");
        }
    
        public void saveFile() {
            if (null == this.file) {
                throw new RuntimeException("file is null");
            }
    
            notifyObservers("save file");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 订阅者
    public interface Observer {
        void update(ObserveContext context);
    }
    
    public class EmailObserver implements Observer {
        private String email;
    
        public EmailObserver(String email) {
            this.email = email;
        }
    
        @Override
        public void update(ObserveContext context) {
            String observeType = context.getObserveType();
            File file = context.getFile();
            Objects.requireNonNull(observeType, "observeType must not be null");
            Objects.requireNonNull(file, "file must not be null");
            System.out.println("send email to " + email + ", content: " + observeType + " " + file.getName());
        }
    }
    
    public class LogObserver implements Observer {
        private String logFilePath;
    
        public LogObserver(String logFilePath) {
            this.logFilePath = logFilePath;
        }
    
        @Override
        public void update(ObserveContext context) {
            String observeType = context.getObserveType();
            File file = context.getFile();
            Objects.requireNonNull(observeType, "observeType must not be null");
            Objects.requireNonNull(file, "file must not be null");
            System.out.println("save log to " + logFilePath + ", content: " + observeType + " " + file.getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 发布者-订阅者,交互的数据
    @Data
    @Accessors(chain = true)
    public class ObserveContext {
        private String observeType;
        private File file;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    打开文件 or 保存文件

    • 客户端
    public class Application {
        public static void main(String[] args) {
            Editor editor = new Editor();
            editor.registerObserver("open file", new EmailObserver("forrest@qq.com"));
            editor.registerObserver("save file", new LogObserver("/user/forrest/log/editor_log.txt"));
    
            editor.openFile("/user/forrest/file/test.txt");
            editor.saveFile();
        }
    }
    
    /*
    send email to forrest@qq.com, content: open file test.txt
    save log to /user/forrest/log/editor_log.txt, content: save file test.txt
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    1.1 可以改进的地方

    • Editor不符合单一原则。
      • 既有与编辑器相关的打开文件/关闭文件的API,又有与编辑器不相关的发布者逻辑。
    • 解决办法:对发布者逻辑进行封装。

    2、更优雅的实现

    • 发布者
    public interface Publisher {
        void registerObserver(String observeType, Observer observer);
        void removeObserver(String observeType, Observer observer);
        void notifyObservers(ObserveContext context); // 这里从`String observeType`变成了`ObserveContext context`(更灵活)
    }
    
    // 对发布者逻辑进行封装。
    public class PublisherManager implements Publisher {
        private static final Map<String, List<Observer>> OBSERVER_MAP = new HashMap<>();
    
        @Override
        public void registerObserver(String observeType, Observer observer) {
            List<Observer> observerList = OBSERVER_MAP.get(observeType);
    
            if (observerList == null) {
                observerList = new ArrayList<>();
                OBSERVER_MAP.put(observeType, observerList);
            }
            observerList.add(observer);
        }
    
        @Override
        public void removeObserver(String observeType, Observer observer) {
            List<Observer> observerList = OBSERVER_MAP.get(observeType);
            if (observerList != null) {
                observerList.remove(observer);
            }
        }
    
        @Override
        public void notifyObservers(ObserveContext context) {
            List<Observer> observerList = OBSERVER_MAP.get(context.getObserveType());
            if (observerList != null) {
                for (Observer observer : observerList) {
                    observer.update(context);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 订阅者 + ObserveContext(和之前没变化)
      • 发布者和订阅者通过接口基于context进行交互,两者的耦合度极低。当发布者重构了,订阅者是无感知的。
    • Editor(组合了Publisher接口,逻辑很纯粹)
    public class Editor {
        private static final Publisher publisherManager = new PublisherManager();
    
        private File file;
    
        public static Publisher getPublisherManager() {
            return publisherManager;
        }
    
        public void openFile(String filePath) {
            if (StringUtils.isEmpty(filePath)) {
                throw new RuntimeException("filePath is empty");
            }
    
            this.file = new File(filePath);
    
            ObserveContext observeContext = new ObserveContext()
                    .setObserveType("open file")
                    .setFile(file);
    
            publisherManager.notifyObservers(observeContext);
        }
    
        public void saveFile() {
            if (null == this.file) {
                throw new RuntimeException("file is null");
            }
    
            ObserveContext observeContext = new ObserveContext()
                    .setObserveType("save file")
                    .setFile(file);
    
            publisherManager.notifyObservers(observeContext);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 客户端:
    public class Application {
        public static void main(String[] args) {
            Editor editor = new Editor();
            Editor.getPublisherManager().registerObserver("open file", new EmailObserver("forrest@qq.com"));
            Editor.getPublisherManager().registerObserver("save file", new LogObserver("/user/forrest/log/editor_log.txt"));
    
            editor.openFile("/user/forrest/file/test.txt");
            editor.saveFile();
        }
    }
    
    /*
    send email to forrest@qq.com, content: open file test.txt
    save log to /user/forrest/log/editor_log.txt, content: save file test.txt
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    四、个人思考

    • 像“一、场景”中纯粹的发布者应该不多见,更常见的应该是“三、更复杂的场景”:一个应用,当其某些状态改变时(文件的打开或者关闭)会触发其他行为。这是观察者模式的用武之地。
  • 相关阅读:
    第二证券|比特币重拾升势 新高背后风险涌动
    SpringMVC(二)响应
    二叉树的四种遍历方式以及中序后序、前序中序、前序后序、层序创建二叉树【专为力扣刷题而打造】
    kubernetes event 的内幕
    【408篇】C语言笔记-第十一章(单链表)
    Mysql查询优化 -Explain 详解(上)
    【微服务 Spring Cloud Alibaba】- Nacos 服务注册中心
    互联网是如何运作的?以前端角度出发(b站objtube的卢克儿听课笔记)
    React(8)-组件ref
    HTML文件的书写规范、HTML标签的介绍、HTML标签的语法
  • 原文地址:https://blog.csdn.net/weixin_37477009/article/details/138034490