2021年7月29日星期四

观察者模式(学习笔记)

  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.txtEmail to admin@example.com: Someone has performed save operation with the following file: test.txt

    

  7. 实现

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

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

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

没有评论:

发表评论