观察者模式(学习笔记)

  1. 意图

  定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并被自动更新

  2. 动机    

  假设这样一种情况,顾客对某个特定品牌的产品非常感兴趣(例如最新型号的 iPhone 手机),而该产品很快将会在商店里出售。顾客可以每天来商店看看产品是否到货。但如果商品尚未到货时,绝大多数来到商店的顾客都会空手而归。另一方面,每次新产品到货时,商店可以向所有顾客发送邮件(可能会被视为垃圾邮件)。这样,部分顾客就无需反复前往商店了,但也可能会惹恼对新产品没有兴趣的其他顾客。

  我们似乎遇到了一个矛盾:要么让顾客浪费时间检查产品是否到货,要么让商店浪费资源去通知没有需求的顾客。观察者模式可以解决这一问题。  

  观察者模式为发布者(将自身的状态改变通知给其他对象)类添加订阅机制,让每个对象都能订阅或取消订阅发布者事件流。该机制包括:

  1)一个用于存储订阅者(所有希望关注发布者状态变化的其他对象)对象引用的列表成员变量;

  2)几个用于添加或删除该列表中订阅者的公有方法。

        

  这样,无论何时发生了重要的发布者事件,它都要遍历订阅者并调用其对象的特定通知方法。在实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 我们不希望发布者与所有这些类相耦合的。因此,所有订阅者都必须实现同样的接口,发布者仅通过该接口与订阅者交互。接口中必须声明通知方法及其参数,这样发布者在发出通知时还能传递一些上下文数据。如果在应用中存在不同类型的发布者,且希望一个订阅者可以同时订阅多个发布者。需要让所有订阅者遵循相同的接口,并在该接口中描述几个订阅方法(需要将发布者作为参数传入方法中)即可。这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态

          

  3. 适用性

  • 一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这两者封装在独立的对象中,以使它们可以各自独立的改变和复用
  • 对一个对象地改变需要同时改变其它对象,而不知道具体有多少对象有待改变
  • 一个对象必须通知其他对象,而它又不能假定其他对象是谁。换言之,你不希望这些对象是紧密耦合的

  4. 结构

         

  5. 效果

  Observer模式允许你独立地改变目标和观察者

  1. 目标和观察者间地抽象耦合   一个目标所知道的仅仅是它有一系列观察者,每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪个具体的类。这样目标和观察者之间地耦合是抽象和最小的。

  2. 支持广播通信    不像通常的请求,目标发送的通知不需要指定它的接收者。通知被自动广播给所有已向该目标对象登记的对象。另外,处理还是忽略一个通知取决于观察者

  3. 意外的更新      由于一个观察者并不知道其他观察者地存在,它可能对改变目标的最终代价一无所知

  6. 代码实现    

  本例中,观察者模式在文本编辑器的对象之间建立了间接的合作关系。每当编辑器 (Editor)对象改变时,它都会通知其订阅者。 ​邮件通知监听器 (Email­Notification­Listener)和日志开启监听器 (Log­Open­Listener)都将通过执行其基本行为来对这些通知做出反应。
订阅者类不与编辑器类相耦合,且能在需要时在其他应用中复用。 ​编辑器类仅依赖于抽象订阅者接口。这样就能允许在不改变编辑器代码的情况下添加新的订阅者类型。

  publisher/EventManager.java: 基础发布者

package observer.publisher;

import observer.listeners.EventListener;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:01
 */
public class EventManager {
    Map<String, List<EventListener>> listeners = new HashMap<>();

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

    public void subscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.add(listener);
    }

    public void unsubscribe(String eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.remove(listener);
    }

    public void notify(String eventType, File file) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.update(eventType, file);
        }
    }
}

  editor/Editor.java: 具体发布者,由其他对象追踪

package observer.editor;

import observer.publisher.EventManager;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:01
 */
public class Editor {
    public EventManager events;
    private File file;

    public Editor() {
        this.events = new EventManager("open", "save");
    }

    public void openFile(String filePath) {
        this.file = new File(filePath);
        events.notify("open", file);
    }

    public void saveFile() throws Exception {
        if (this.file != null) {
            events.notify("save", file);
        } else {
            throw new Exception("Please open a file first.");
        }
    }
}

  listeners/EventListener.java: 通用观察者接口

package observer.listeners;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:02
 */
public interface EventListener {
    void update(String eventType, File file);
}

  listeners/EmailNotificationListener.java: 收到通知后发送邮件

package observer.listeners;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:02
 */
public class EmailNotificationListener implements EventListener{
    private String email;

    public EmailNotificationListener(String email) {
        this.email = email;
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }
}

  listeners/LogOpenListener.java: 收到通知后在日志中记录一条消息

package observer.listeners;

import java.io.File;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:03
 */
public class LogOpenListener implements EventListener{
    private File log;

    public LogOpenListener(String fileName) {
        this.log = new File(fileName);
    }

    @Override
    public void update(String eventType, File file) {
        System.out.println("Save to log " + log + ": Someone has performed " + eventType + " operation with the following file: " + file.getName());
    }

}

  Demo.java: 客户端代码

package observer;

import observer.editor.Editor;
import observer.listeners.EmailNotificationListener;
import observer.listeners.LogOpenListener;

/**
 * @author GaoMing
 * @date 2021/7/26 - 10:00
 */
public class Demo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        editor.events.subscribe("open", new LogOpenListener("/path/to/log/file.txt"));
        editor.events.subscribe("save", new EmailNotificationListener("admin@example.com"));

        try {
            editor.openFile("test.txt");
            editor.saveFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  运行结果

Save to log \path\to\log\file.txt: Someone has performed open operation with the following file: test.txt
Email to admin@example.com: Someone has performed save operation with the following file: test.txt

    

  7. 实现

  1)创建目标到其观察者之间的映射     一个目标对象跟踪它应通知的观察者的最简单的方法是显式地在目标中保存对它们地引用。然而,当目标很多而观察者较少时,这样存储可能代价太高。一个解决办法是用时间换空间,用一个关联查找机制(例如一个hash表)来维护目标到观察者地映射。这样一个没有观察者的目标就不产生存储开销。但另一方面,这一方法增加了访问观察者的开销

  2)观察多个目标    有时候,一个观察者依赖于多个目标。例如,一个表格对象可能依赖于多个数据源。在这种情况下,必须扩展update接口,目标对象可以简单的将自己作为Update操作地一个参数,让观察者知道应该去检查哪个目标

  3)谁触发更新     目标和它的观察者依赖于通知机制来保持一致。但到底哪个对象调用Notify来触发更新? 这里有两个选择:

  • 由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低
  • 让客户负责在合适的时候调用notify。这样做的优点是客户在一系列状态改变完成后一次性的触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用Notify,这种方式交易出错

  4)在发出通知前确保目标的状态自身是一致的       在发出通知前确保状态自身一致这一点很重要,因为观察者在更新其状态的过程中需要查询目标的当前状态。可以使用模板方法发送通知来避免这种错误。定义那些子类可以重定义的原语操作,并将Notify作为模板方法中的最后一个操作,这样当子类重定义Subject的操作时,还可以保证该对象的状态是自身一致的。另外,最好在文档中注明哪个Subject操作触发通知

  5)避免特定于观察者的更新协议——推/拉模型     观察者模式的实现经常需要让目标广播关于其改变的其他一些信息。目标将这些信息作为Update操作的一个参数传递出去。一个极端情况是,目标向观察者发送关于改变的详细信息,而不管它们需要与否,即推模型。另一个极端是拉模型,目标除最小通知外什么也不送出,而在此之后由观察者显式的向目标询问细节。拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者需要的信息。推模型使得观察者相对难以复用。另一方面,拉模型效率会较差,因为观察者对象需要在没有目标对象的帮助下,确定什么改变了

  6)显式地指定感兴趣的改变       可以通过扩展目标的注册接口,让观察者注册为仅对特定事件感兴趣的观察者。如上面例子中,将EventType作为参数传递给Notify 和Update方法

  7)封装复杂的更新语义        当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象,即ChangeManager(更改管理器)。其目的是尽量减少观察者反映其目标状态变化所需的工作量。例如,如果一个操作涉及几个相互依赖的目标进行改动,就必须保证在所有的目标更新完毕后,才一次性的通知它们的观察者,而不是每个目标都通知观察者。ChangeManager是一个Mediator模式的实例。相比于,SimpleChangeManager,当一个观察者观察多个目标时,DAGChangeManager保证观察者仅接收一个更新

                   

  8. 与其他模式的关系

  • 责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
    责任链按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理
    命令在发送者和请求者之间建立单向连接
    中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通
    观察者允许接收者动态地订阅或取消接收请求

  • 中介者和观察者有的时候会非常相似
    中介者的主要目标是消除一系列系统组件之间的相互依赖。这些组件将依赖于同一个中介者对象。观察者的目标是在对象之间建立动态的单向连接,使得部分对象可作为其他对象的附属发挥作用
    有一种流行的中介者模式实现方式依赖于观察者。中介者对象担当发布者的角色,其他组件则作为订阅者,可以订阅中介者的事件或取消订阅。当中介者以这种方式实现时,它可能看上去与观察者非常相似

  9. 已知应用  

  观察者模式在 Java 代码中很常见,特别是在 GUI 组件中。它提供了在不与其他对象所属类耦合的情况下对其事件做出反应的方式
  这里是核心 Java 程序库中该模式的一些示例:
  java.util.Observer/ java.util.Observable (极少在真实世界中使用)
  java.util.EventListener的所有实现 (几乎广泛存在于 Swing 组件中)
  javax.servlet.http.HttpSessionBindingListener
  javax.servlet.http.HttpSessionAttributeListener
  javax.faces.event.PhaseListener
  识别方法: 该模式可以通过将对象存储在列表中的订阅方法, 和对于面向该列表中对象的更新方法的调用来识别

posted @ 2021-07-28 18:51  慕仙白  阅读(155)  评论(0编辑  收藏  举报