对观察者模式的理解

发布于:2024-04-26 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、场景

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

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;
    }
}
  • 观察者/订阅者
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);
    }
}
  • 客户端
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、简单实现

  • 发布者
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");
    }
}
  • 订阅者
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());
    }
}
  • 发布者-订阅者,交互的数据
@Data
@Accessors(chain = true)
public class ObserveContext {
    private String observeType;
    private File file;
}

打开文件 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.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);
            }
        }
    }
}
  • 订阅者 + 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);
    }
}
  • 客户端:
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
*/

四、个人思考

  • 像“一、场景”中纯粹的发布者应该不多见,更常见的应该是“三、更复杂的场景”:一个应用,当其某些状态改变时(文件的打开或者关闭)会触发其他行为。这是观察者模式的用武之地。