1. 引子

1.1 不完美的实现方案

  • 公司业务发展壮大,集群监控也逐渐走向自动化:上报集群重要指标,实时监控集群状态,异常时进行自动告警

  • 老大说:你去写一个告警程序,集群状态异常时,以短信和电话的形式通知运维人员

  • 新来的可能会这样写(程序简化了,能表述出编程思路就行):

    public class AlertApplication {private final MessageAlarm messageAlarm;private final TelephoneAlarm telephoneAlarm;public AlertApplication(MessageAlarm messageAlarm, TelephoneAlarm telephoneAlarm) {this.messageAlarm = messageAlarm;this.telephoneAlarm = telephoneAlarm;}// 收到来自实时监控的指标数据,根据阈值确定是否需要进行告警public void metricData(double memory, double cpu) {// 打印日志System.out.printf("集群内存使用: %.2fGB, cpu使用率: %.2f%%\n", memory, cpu * 100);if (memory >= Threshold.MAX_MEMORY.getThreshold()) {String msg = String.format("集群内存使用量: %.2fGB, 超过阈值: %.2fGB", memory, Threshold.MAX_MEMORY.getThreshold());messageAlarm.sendMessage(msg);telephoneAlarm.ringUp(msg);}if (cpu >= Threshold.MAX_CPU.getThreshold()) {String msg = String.format("集群cpu使用率: %.2f%%, 超过阈值: %.2f%%", cpu * 100, Threshold.MAX_CPU.getThreshold() * 100);messageAlarm.alert(msg);telephoneAlarm.alert(msg);}}}enum Threshold {MAX_MEMORY(100),MAX_CPU(0.8);private double threshold;Threshold(double threshold) {this.threshold = threshold;}public double getThreshold() {return this.threshold;}
    }class MessageAlarm {public void alert(String msg) {System.out.println("短信告警: " + msg);}
    }class TelephoneAlarm {public void alert(String msg) {System.out.println("电话告警: " + msg);}
    }
    
  • 编写主程序,启动AlertApplication

    public class Main {public static void main(String[] args) {AlertApplication application = new AlertApplication(new MessageAlarm(), new TelephoneAlarm());application.metricData(64.8, 0.45);application.metricData(120.26, 0.97);}
    }
    
  • 执行结果如下:

1.2 存在的问题

  • 有经验的同事对这段代码做了如下评价(实际来自博客:Observer Pattern | Set 1 (Introduction)):

    • AlertApplication持有具体Alarm对象的引用,可以访问到超出其需要的更多额外信息,即使它只需要调用这些Alarm对象的alert()方法。(违反了迪米特原则?菜鸟不是很懂)
    • 调用Alarm对象的alert(String msg)方法,是在使用具体对象共享数据,而非使用接口共享数据。这违背了一个重要的设计原则:

      Program to interfaces, not implementations

    • AlertApplication与Alarm对象紧耦合,如果要添加或移除Alarm对象,需要修改AlertApplication,这明显违背了开闭原则
  • 针对以上问题,自己体会最深的就是违反了开闭原则,代码不易维护

2. 使用observer模式

  • observer模式属于行为设计模式,其定义如下:

    observer模式定义了对象之间的一对多依赖,当一个对象的状态发生变化,会自动通知并更新其他依赖对象

  • 根据上面的场景,我们可以分析出:
    • AlertApplication与Alarm对象之间存在一对多关系(one-to-many relationship),AlertApplication是one,Alarm对象是many
    • 当集群处于异常状态时,AlertApplication需要自动调用(通知)Alarm对象
    • 换句话说,Alarm对象是否执行告警动作,依赖于AlertApplication对象的状态是否发生改变(这里是指是否达到告警阈值)
  • 不难发现,上述场景可以使用observer模式

2.1 概念解读

  • observer模式中,将一对多关系中的one叫做Subject(主题),many叫做Observer
  • 但是,这里的Observer不能主动获取消息,而是等待Subject向他推送消息
    • 就像医院排号看病一样,病人如果频繁询问医生或者护士现在多少号了,那治疗工作就没法进行下去了
    • 需要通过叫号器,显示当前进度、通知下一个病人进入诊室就诊
    • 这时,叫号器就是Subject,病人就是Observer
  • 其实,observer模式还有很多其他的称呼:如发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式
  • 我们熟悉的Java GUI中的各种Listener,就是源-监听器模式(简称事件监听模式)
  • 微信公众号的订阅、银行活动推送等,使用发布-订阅模式来描述更加简洁易懂

2.2 真实应用场景

  1. observer模式,在GUI工具包和事件监听器中大量使用。例如,java AWT中的button(Subject)和 ActionListener(observer) 是用观察者模式构建的。
  2. 社交媒体、RSS 提要、电子邮件订阅、公众号等,使用observer模式向关注或订阅的用户推送最新消息
  3. 手机应用商店,应用如果有更新,将使用observer模式通知所有用户

2.3 UML图

  • observer模式的UML图如下:

  • Subject(抽象主题):Subject一般为接口或抽象类,提供添加、删除、通知observer对象的三个抽象方法

  • ConcreteSubject(具体主题):内部使用集合存储注册的observer,实现Subject中的抽象方法,以便在内部状态发生变化时,通知所有注册过的observer对象。

  • Observer(抽象观察者):Observer一般为借口或抽象类,为Subject提供通知自己的notify()方法

    • 还可以定义为update()方法,二者都是subject向observer传递信息的接口)
  • ConcreteObserver(具体观察者): 实现notify()方法,在Subject状态边变化时,做出相应的反应

2.4 使用observer模式实现需求

  • 定义Subject接口:

    public interface Subject {void addObserver(Observer observer);void deleteObserver(Observer observer);void notifyObservers(double cpu, double memory);
    }
    
  • 定义Observer接口:

    public interface Observer {void notify(double cpu, double memory);
    }
    
  • 实现AlertApplication对应的Subject:

    public class AlertApplicationSubject implements Subject {private final List<Observer> observers;public AlertApplicationSubject() {this.observers = new ArrayList<>();}@Overridepublic void addObserver(Observer observer) {if (observer == null && observers.contains(observer)) {return;}observers.add(observer);}@Overridepublic void deleteObserver(Observer observer) {if (observer == null) {return;}observers.remove(observer);}@Overridepublic void notifyObservers(double cpu, double memory) {System.out.printf("集群当前cpu使用率: %.2f%%, 内存使用量: %.2fGB\n", cpu * 100, memory);// 当cpu或memory超过阈值,通知observer// observer接收到信息后,自动告警if (cpu > 0.8 || memory > 100) {for (Observer observer : observers) {observer.notify(cpu, memory);}}}
    }
    
  • 实现短信告警、电话告警两种observer:

    public class MessageAlarmObserver implements Observer{@Overridepublic void notify(double cpu, double memory) {if (cpu > 0.8 ) {System.out.printf("短信告警: 集群cpu使用率%.2f%%, 超过阈值80%%\n", cpu*100);}if (memory > 100) {System.out.printf("短信告警: 集群内存使用量%.2fGB, 超过阈值100GB\n", memory);}}
    }public class PhoneAlarmObserver implements Observer{@Overridepublic void notify(double cpu, double memory) {if (cpu > 0.8 ) {System.out.printf("电话告警: 集群cpu使用率%.2f%%, 超过阈值80%%\n", cpu * 100);}if (memory > 100) {System.out.printf("电话告警: 集群内存使用量%.2fGB, 超过阈值100GB\n", memory);}}
    }
    
  • 使用observer模式实现的集群监控告警程序

    public class Main {public static void main(String[] args) {Subject subject = new AlertApplicationSubject();Observer messageAlarm = new MessageAlarmObserver();Observer phoneAlarm = new PhoneAlarmObserver();subject.addObserver(messageAlarm);subject.addObserver(phoneAlarm);// 采集集群监控数据subject.notifyObservers(0.6, 45);subject.notifyObservers(0.5, 127);}
    }
    
  • 执行结果如下:

2.5 observer模式的优缺点

2.5.1 优点

  • 自己的理解: 相对第一个版本的代码实现,使用observer模式符合迪米特原则、接口编程原则、开闭原则
  • 专业的评价: observer模式实现了交互对象之间的松耦合
  • 松耦合的对象可以灵活应对不断变化的需求,且交互对象无需拥有其他对象的额外信息
  • 松耦合具体指:
    • Subject只需要知道observer对象实现了Observer接口
    • 添加或删除observer无需修改Subject
    • 可以相互独立地重用subject和observer对象(例如,可以直接调用observer的相关方法)

2.5.2 缺点

  • 由于需要显式地注册和注销observer,Lapsed listener problem 将导致内存泄漏

关于Lapsed listener problem

  1. 问题一:内存泄漏

    • 在observer模式中,subject持有对已经注册的observer的强引用,使得observer不会被垃圾回收
    • 如果observer不再需要接收subject的通知,但却没有正确地从subject中注销,则将发生内存泄漏
    • 此时,subject持有对observer的强引用,observer及其引用其他对象都将无法被垃圾回收
  2. 问题二:性能下降

    • 不感兴趣的observer没有从subject中注册自己,将增加subject发送消息的工作量,导致性能下降
  3. 解决办法:

    • subject持有observer的弱引用,而非强引用,使得observer不再工作后(只被弱引用关联),无需注销就能被垃圾回收
    • 自己的疑问:这不靠谱啊,为了不让observer被垃圾回收,不得另外找个地方给它创建一个强引用?不然,不知啥时候就被垃圾回收了

3. Java内置的observer模式

3.1 两种模式(推模式 vs 拉模式)

推模式

  • 上面的代码实现中,subject知道observer需要哪些数据,并通过notify()方法主动向observer传递数据,属于push模式(推模式)
  • 这样的设计使得observer难以复用,因为observer的notify()方法需要根据实际需求定义参数,很可能无法兼顾其他需求场景
  • 例如,上面的示例代码,observer只适合用于监控cpu和memory的场景。如果切换成发送广告邮件的场景,则无法适用

拉模式

  • 既然subject无法准确判断observer需要什么数据,那干脆就把自身作为入参,让observer按需按需获取
  • 这样的模式,被叫做pull模式(拉模式)
  • Java内置的observer模式,在我个人看来是拉模式 + 推模式的完美结合

3.2 java内置的observer模式

  • Java提供了Observable类,对应Subject;Observer接口,对应观察者
  • 其中,Observable类非常简单
    • 包含一个存储observer的Vector对象obs,一个标识状态是否变化的布尔值changed
    • 提供了用于添加、删除、计数observer的同步方法,用于更新、重置、获取状态的同步方法
    • 但是,其notifyObservers()却只是局部同步,并非整体同步 —— 这样的设计存在问题?欢迎讨论
      public void notifyObservers(Object arg) {Object[] arrLocal;synchronized (this) {if (!changed)return;arrLocal = obs.toArray();clearChanged();}// 如注释说的一样,没有对这部分代码进行同步,容易出现:// 新添加的observer无法收到正在进行的通知,最近移除的observer会错误地收到通知for (int i = arrLocal.length-1; i>=0; i--)((Observer)arrLocal[i]).update(this, arg);
      }
      
  • Observer接口只有一个update(Observable o, Object arg)方法
    public interface Observer {void update(Observable o, Object arg);
    }
    
  • 这样的设计既可以使用拉模式,让observer主动从Subject获取数据;又可以基于Object arg使用推模式,主动向observer传递数据

3.3 使用实战

  • 使用Java自带的observer模式,实现看病叫号的需求

  • 继承Observable类实现叫号器

    public class Caller extends Observable {private final int room; // 诊室private int number; // 记录当前的就诊序号public Caller(int room) {super(); // 初始化存储observer的Vectorthis.room = room;}public void call(int number) {// 就诊序号发生变化,开始叫号if (number != this.number) {this.number = number; // 记录最新的就诊序号setChanged(); // 将状态更新为true,表示状态发生变化,以触发notifyObservers()方法notifyObservers(); // 调用notifyObservers()通知就诊的病人}}public int getNumber() {return number;}public int getRoom() {return room;}
    }
    
  • 实现Observer接口,创建Patient类

    public class Patient implements Observer {private final int number;private final String name;public Patient(int number, String name) {this.number = number;this.name = name;}@Overridepublic void update(Observable o, Object arg) {// 获取诊室号和就诊序号,如果是自己则做出回应int room = ((Caller) o).getRoom();int number = ((Caller) o).getNumber();if (number == this.number) {System.out.printf("我是%d号病人: %s,轮到我去%d诊室就诊\n", number, name, room);}}
    }
    
  • 测试程序

    public class Main {public static void main(String[] args) {Caller caller = new Caller(7);// 添加已经到场的病人Patient patient1 = new Patient(3, "张三");caller.addObserver(patient1);Patient patient2 = new Patient(1, "王二");caller.addObserver(patient2);Patient patient3 = new Patient(4, "李四");caller.addObserver(patient3);// 开始叫号caller.call(1);caller.call(2); // 没有对应的病人,无任何响应caller.call(3);}
    }
    
  • 执行结果如下:

  • 通过这个示例程序的顿悟: Subject和observer之间的一对多关系,并非是对应多个不同类型的observer,同一类型的多个observer也行

3.5 其他

  • 博客The Observer Pattern in Java还提出,Observer接口不完美,且Observable类容易开发者重写方法而破坏线程安全
  • 所以,在JDK 9中,Observer接口被弃用,推荐基于ProperyChangeListener接口实现
  • 由于笔者使用的是JDK 8,所以无法验证,后续有机会可以体验一下ProperyChangeListener接口

4. 参考链接

  • Observer Pattern | Set 1 (Introduction) & Observer Pattern | Set 2 (Implementation)
  • 推模式、拉模式:《JAVA与模式》之观察者模式
  • PropertyChangeSupport:The Observer Pattern in Java

观察者(observer)模式(一)相关推荐

  1. 设计模式学习笔记——观察者(Observer)模式

    设计模式学习笔记--观察者(Observer)模式 @(设计模式)[设计模式, 观察者模式, Observer] 设计模式学习笔记观察者Observer模式 基本介绍 观察者案例 类图 实现代码 Ob ...

  2. 移动项目开发笔记(.Net下的观察者(Observer)模式)

    下面是一些关于这个Observer的基本一些概念,在很多地方都能看到,这里我归纳这这里便于以后查阅: 一.发布订阅模型(pub-sub) 二.动机(Motivation) 在软件构建过程中,我们需要为 ...

  3. 设计模式--观察者(Observer)模式

    模式定义 定义了对象之间的一对多依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者都会收到通知并更新 类图 应用场景 当更改一个对象的状态可能需要更改其他对象,并且实际 ...

  4. Java设计模式之观察者Observer模式代码示例

  5. Observer(观察者)模式

    1.概述 一些面向对象的编程方式,提供了一种构建对象间复杂网络互连的能力.当对象们连接在一起时,它们就可以相互提供服务和信息. 通常来说,当某个对象的状态发生改变时,你仍然需要对象之间能互相通信.但是 ...

  6. java获取jsp 组件,利用Observer模式解决组件间通信问题-JSP教程,Java技巧及代码

    1. 问题的提出 以前做一个界面的时候常常会遇到这样的尴尬情况:希望保留各个独立的组件(类),但又希望它们之间能够相互通信.譬如windows中的explorer,我们希望鼠标点击左边是树型目录的一个 ...

  7. 设计模式-Observer模式

    目录 角色组成 抽象主题(Subject) 具体主题(Concrete Subject) 抽象观察者(Observer) 具体观察者(Concrete Observer) 具体实现 总结 观察者模式( ...

  8. Observer 模式在eHR中的应用

    接触模式应该是在大三的时候,那时候感觉是模式让我真正的认识到了OO,现在毕业了,也工作有半年了,终于体会到了"商业特色的软件开发"了,根本就没有多余的时间去考虑运用什么模式.怎样使 ...

  9. VTK修炼之道27:图像基本操作_三维图像切片交互提取(回调函数、观察者-命令模式)

    1.鼠标滑动提取三维图像切片 学习三维图像切面的提取后,我们可以实现一个稍微复杂的程序--通过滑动鼠标来切换三维图像切片,这也是医学图像处理软件中一个很基本的功能.实现该功能难点是怎样在VTK中控制鼠 ...

  10. 重温Observer模式--热水器·改(转载)

    引言 在 C#中的委托和事件 一文的后半部分,我向大家讲述了Observer(观察者)模式,并使用委托和事件实现了这个模式.实际上,不使用委托和事件,一样可以实现Observer模式.在本文中,我将使 ...

最新文章

  1. Docker系列 三. Docker安装mysql
  2. <Module>的类型初始值设定项引发异常
  3. Maven实战:pom.xml与settings.xml
  4. 中国台湾芯片设计商 Realtek 的WiFi SDK漏洞影响数百万IOT设备
  5. [转]百万数据查询优化技巧三四则
  6. 深搜——数字划分问题
  7. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十七)地图自适应区域加载...
  8. 我想创业,但不懂技术怎么办
  9. 云安全到来 手动更新病毒码将成为历史
  10. Subsonic使用
  11. linux 下令chmod 755的意思
  12. web前端经典面试题
  13. Android websocket闪退,退出手机浏览器,websocket会自动关闭,不是长持续吗
  14. Aqara网关、yeelight智能灯、智能窗帘电机如何实现场景化互联?
  15. Praat脚本-017 |拆分已经标注好的音素为两个音素
  16. 苹果CMS个人收款扫码收款插件 闪电收款
  17. SAP中报表清单导出的常用方法
  18. val和var和Java
  19. Others2_谈谈个人常用的软件
  20. CoreData单表

热门文章

  1. html表单中下拉列表,HTML select下拉列表标签
  2. java微博源码_基于jsp的微博-JavaEE实现微博 - java项目源码
  3. 人工智能和新能源行业有哪些
  4. Audit login 与 Audit logout
  5. 通达信公式-接近均线
  6. 深富策略核心资产崩了
  7. 视频教程-计算机网络技术-网络技术
  8. 病毒不断变异,我们如何防护?
  9. 自签名多级证书亲测可用
  10. 商业汇票的背书、贴现与质押