观察者模式可以说是众多设计模式中,最容易理解的设计模式之一了,观察者模式在Spring中也随处可见,面试的时候,面试官可能会问,嘿,你既然读过Spring源码,那你说说Spring中运用的设计模式吧,你可以自信的告诉他,Spring中的ApplicationListener就运用了观察者模式。

让我们一步一步来,首先我们要知道到底什么是观察者模式,用Java是如何实现的,在这里,我将会用三种方式来实现观察者模式。

什么是观察者模式

在现实生活中,观察者模式处处可见,比如

  • 看新闻,只要新闻开始播放了,就会把新闻推送给订阅了新闻的用户,在这里,新闻就是【被观察者】,而用户就是【观察者】。

  • 微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是【被观察者】,而用户就是【观察者】。

  • 热水器,假设热水器由三部分组成,热水器,警报器,显示器,热水器仅仅负责烧水,当水温到达设定的温度后,通知警报器,警报器发出警报,显示器也需要订阅热水器的烧水事件,从而获得水温,并显示。热水器就是【被观察者】,警报器,显示器就是【观察者】。

在这里,可以看到,【观察者】已经失去自主的权利,只能被动的接收来自【被观察者】的事件,无法主动观察。【观察者】成为了“受”,而【被观察者】成为了“攻”。【被观察者】只是通知【观察者】,不关心【观察者】收到通知后,会执行怎样的动作。

而在设计模式中,又把【被观察者】称为【主题】。

在观察者设计模式中,一般有四个角色:

  • 抽象主题角色(Subject)
  • 具体主题角色(ConcreteSubject)
  • 抽象观察者角色(Observer)
  • 具体观察者角色(ConcreteObserver)

其中,【主题】需要有一个列表字段,用来保存【观察者】的引用,提供两个方法(虚方法),即【删除观察者】【增加观察者】,还需要提供一个给客户端调用的方法,通知各个【观察者】:你们关心(订阅)的事件已经推送给你们了。

下面,我就用三种方式来实现观察者模式。

经典

public class News {
private String title;
private String content; public String getTitle() {
return title;
} public void setTitle(String title) {
this.title = title;
} public String getContent() {
return content;
} public void setContent(String content) {
this.content = content;
}
}

此类不属于观察者模式必须的类,用来存放事件的信息。

public interface Subject {
List<People> peopleList = new ArrayList<>(); default void add(People people) {
peopleList.add(people);
} default void remove(People people) {
peopleList.remove(people);
} void update();
}

抽象主题角色,在这个角色中,有一个字段peopleList,用来保存【观察者】的引用,同时定义了两个接口,这是Java8默认接口实现的写法。这两个接口是给客户端调用的,用来【删除观察者】【增加观察者】,还提供一个方法,此方法需要被【具体主题角色】重写,用来通知各个【观察者】。

public class NewsSubject implements Subject{
public void update() {
for (People people : peopleList) {
News news = new News();
news.setContent("今日在大街上,有人躲在草丛中袭击路人,还大喊“德玛西亚万岁”");
news.setTitle("德玛西亚出现了");
people.update(news);
}
}
}

具体主题角色,重写了【抽象主题角色】的方法,循环列表,通知各个【观察者】。

public interface People {
void update(News news);
}

抽象观察者角色,定义了一个接口,【具体观察者角色】需要重写这个方法。

下面就是【具体观察者角色】了:

public class PeopleA implements People {
@Override
public void update(News news) {
System.out.println("这个新闻真好看");
}
}
public class PeopleB implements People {
@Override
public void update(News news) {
System.out.println("这个新闻真无语");
}
}
public class PeopleC implements People {
@Override
public void update(News news) {
System.out.println("这个新闻真逗");
}
}

客户端:

public class Main {
public static void main(String[] args) {
Subject subject = new NewsSubject();
subject.add(new PeopleA());
subject.add(new PeopleB());
subject.add(new PeopleC());
subject.update();
}
}

运行:

我们学习设计模式,必须知道设计模式的优缺点,那么观察者设计模式的优缺点是什么呢?

优点:

  • 【主题】和【观察者】通过抽象,建立了一个松耦合的关系,【主题】只知道当前有哪些【观察者】,并且发送通知,但是不知道【观察者】具体会执行怎样的动作。这也很好理解,比如 微信公众号推送了一个消息过来,它不知道你会采取如何的动作,是 微笑的打开,还是愤怒的打开,或者是直接把消息删了,又或者把手机扔到洗衣机洗刷刷。

  • 符合开闭原则,如果需要新增一个【观察者】,只需要写一个类去实现【抽象观察者角色】即可,不需要改动原来的代码。

缺点:

  • 客户端必须知道所有的【观察者】,并且进行【增加观察者】和【删除观察者】的操作。

  • 如果有很多【观察者】,那么所有的【观察者】收到通知,可能需要花费很久时间。

当然以上优缺点,是最直观的,可以很容易理解,并且体会到的。其他优缺点,可以自行百度。

Lambda

在介绍这种写法之前,有必要介绍下函数式接口函数式接口的概念由来已久,一般来说只定义了一个虚方法的接口就叫函数式接口,在Java8中,由于Lambda表达式的出现,让函数式接口大放异彩。

我们仅仅需要修改客户端的代码就可以:

    public static void main(String[] args) {
Subject subject = new NewsSubject();
subject.add(a -> System.out.println("已阅这新闻"));
subject.add(a -> System.out.println("假的吧"));
subject.add(a -> System.out.println("昨天就看过了"));
subject.update();
}

运行结果:

利用Lambda表达式和函数式接口,可以省去【具体观察者角色】的定义,但是个人认为,这并非属于严格意义上的观察者模式,而且弊端很明显:

  • 客户端需要知道观察者的具体实现。
  • 如果观察者的具体实现比较复杂,可能代码并没有那么清晰。

所以这种写法,具有一定的局限性。

借用大神的一句话

设计模式的出现,是为了弥补语言的缺陷。

正是由于语言的升级,让某些设计模式发生了一定的变化,除了观察者模式,还有模板方法模式、责任链模式等,都由于 Lambda表达式的出现,而出现了一些变化。

JDK

在Java中,本身就提供了一个接口:Observer,一个子类:Observable,其中Observer表示【观察者】,Observable表示【主题】,可以利用这两个子类和接口来实现观察者模式:

public class NewsObservable extends Observable {
public void update() {
setChanged();
notifyObservers();
}
}
public class People1 implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("小编真无聊");
}
}
public class People2 implements Observer {
@Override
public void update(Observable o, Object arg) {
System.out.println("开局一张图,内容全靠编");
}
}

客户端:

  public static void main(String[] args) {
NewsObservable newsObservable = new NewsObservable();
newsObservable.addObserver(new People1());
newsObservable.addObserver(new People2());
newsObservable.update();
}

运行结果:

在这里,我不打算详细介绍这种实现方式,因为从Java9开始,Java已经不推荐这种写法了,而推荐用消息队列来实现。是不是很开心,找到一个借口不去研究Observable,Observer 这两个东西了。

Spring中的事件编程模型

Spring中的事件编程模型就是观察者模式的实现,SpringBoot就利用了Spring的事件编程模型来完成一些操作,这里暂时不表。

在Spring中定义了一个ApplicationListener接口,从名字就知道它是一个监听器,是监听Application的事件的,那么Application又是什么,就是ApplicationContext,ApplicationContext内置了几个事件,其中比较容易理解的是:

  • ContextRefreshedEvent
  • ContextStartedEvent
  • ContextStoppedEvent
  • ContextClosedEvent

    从名称上来看,就知道这几个事件是什么时候被触发的了。

下面我演示下具体的用法,比如我想监听ContextRefreshedEvent事件,如果事件发生了,就打印一句话。

@Component
public class MyListener implements ApplicationListener{
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
if(applicationEvent instanceof ContextRefreshedEvent){
System.out.println("刷新了");
}
}
}
@Configuration
@ComponentScan
public class AppConfig {
}
    public static void main(String[] args) {
AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
}

运行结果:

当时学习Spring,看到Spring提供了各式各样的接口来让程序员们对Spring进行扩展,并且没有任何侵入性,我不得不佩服Spring的开发者们。这里也是,我们可以看到在客户端找不到任何关于“订阅事件”的影子。

这种实现方式不是太好,可以看到我们在方法内部做了一个判断:接收到的事件是否为ContextRefreshedEvent。

伟大的Spring还提供了泛型的ApplicationListener,我们可以通过泛型的ApplicationListener来完善上面的代码:

@Component
public class MyListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("刷新了");
}
}

我们还可以利用Spring中的事件编程模型来自定义事件,并且发布事件:

首先,我们需要定义一个事件,来实现ApplicationEvent接口,代表这是一个Application事件,其实上面所说的四个内置的事件也实现了ApplicationEvent接口:

public class MyEvent extends ApplicationEvent {
public MyEvent(Object source) {
super(source);
}
}

还需要定义一个监听器,当然,在这里需要监听MyEvent事件:

@Component
public class MyListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
System.out.println("我订阅的事件已经到达");
}
}

现在有了事件,也有了监听器,是不是还少了发布者,不然谁去发布事件呢?

@Component
public class MyEventPublish implements ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; @Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
} public void publish(Object obj) {
this.publisher.publishEvent(obj);
}
}

发布者,需要实现ApplicationEventPublisherAware 接口,重写publish方法,顾名思义,这就是发布方法,那么方法的参数obj是干嘛的呢,作为发布者,应该需要知道我要发布什么事件,以及事件来源(是谁触发的)把,这个obj就是用来存放这样的数据的,当然,这个参数需要我们手动传入进去。setApplicationEventPublisher是Spring内部主动调用的,可以简单的理解为初始化发布者。

现在就剩最后一个角色了,监听器有了,发布者有了,事件也有了,对,没错,还少一个触发者,毕竟要有触发者去触发事件啊:

@Component
public class Service { @Autowired
private MyEventPublish publish; public void publish() {
publish.publish(new MyEvent(this));
}
}

其中publish方法就是给客户端调用的,用来触发事件,可以很清楚的看到传入了new MyEvent(this),这样发布者就可以知道我要触发什么事件和是谁触发了事件。

当然,还需要把一切交给Spring管理:

@Configuration
@ComponentScan
public class AppConfig {
}

客户端:

    public static void main(String[] args) {
AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(Service.class).publish();;
}

运行结果:

这一篇博客比较简单,只是简单的应用,但是只有会了应用,才能谈源码。

这篇博客到这里就结束了,谢谢大家。

三种方式实现观察者模式 及 Spring中的事件编程模型的更多相关文章

  1. spring配置datasource三种方式

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp34 spring配置datasource三种方式 1.使用org.spri ...

  2. spring配置datasource三种方式及具体信息

    1.使用org.springframework.jdbc.datasource.DriverManagerDataSource说明:DriverManagerDataSource建立连接是只要有连接就 ...

  3. Spring配置dataSource的三种方式 数据库连接池

    1.使用org.springframework.jdbc.dataSource.DriverManagerDataSource 说明:DriverManagerDataSource建立连接是只要有连接 ...

  4. spring配置datasource三种方式 数据库连接池

    尊重原创(原文链接):http://blog.csdn.net/kunkun378263/article/details/8506355 1.使用org.springframework.jdbc.da ...

  5. JavaScript 基础——使用js的三种方式,js中的变量,js中的输出语句,js中的运算符;js中的分支结构

    JavaScript 1.是什么:基于浏览器 基于(面向)对象 事件驱动 脚本语言 2.作用:表单验证,减轻服务器压力 添加野面动画效果 动态更改页面内容 Ajax网络请求 () 3.组成部分:ECM ...

  6. NGUI注册事件的三种方式

    1.第一种方式 当一个元素要执行某个方法,而这个方法在此元素赋予的脚本上有,那么直接会调用此方法,但此方法的名称必须是内置的固定名称,例如OnClick,OnMouseOver,OnMouseOut等 ...

  7. Entity Framework加载数据的三种方式。

    MSDN文章Loading Related Entities 有 Eagerly Loading Lazy Loading Explicitly Loading 三种方式. 而看到查询中包含Inclu ...

  8. 格式化输出的三种方式,运算符及流程控制之if判断

    ''' 格式化输出的三种方式,运算符及流程控制之if判断 ''' # 格式化输出的三种方式 # 一.占位符 程序中经常会有这样场景:要求用户输入信息,然后打印成固定的格式 比如要求用户输入用户名和年龄 ...

  9. python核心高级学习总结3-------python实现进程的三种方式及其区别

    python实现进程的三种方式及其区别 在python中有三种方式用于实现进程 多进程中, 每个进程中所有数据( 包括全局变量) 都各有拥有⼀份, 互不影响 1.fork()方法 ret = os.f ...

随机推荐

  1. fiddler抓包软件的使用--请求头--ajax

    User-Agent: FiddlerHost: localhost:49828Content-Length: 0Accept: application/xmlContent-Type: applic ...

  2. procotol.go 源码阅读

    )         return     }     bufferOver = buffer[i:]     return } //整形转换成字节 // func IntToBytes(n int) ...

  3. req_params.go

    , fmt.Sprintf("%s: closing %s", proto, listener.Addr())) }

  4. cad二次开发--添加对象到模型空间中

    通过实体名来将实体加入到模型空间 AcDbObjectId PostToModelSpace(AcDbEntity *pEnt){ //打开块表 AcDbBlockTable *pBlockTable ...

  5. BZOJ4944 泳池 解题报告

    题目描述 有一个 \(n\) 行无穷列的海域,每个格子有 \(q\) 的概率安全, \(1-q\) 的概率不安全.从中框出一个面积最大的矩形,满足以下两个条件: (1)矩形内的格子均安全: (2)矩形 ...

  6. awk的递归

    想来惭愧,之前写的一篇文章<用awk写递归>里多少是传递里错误的信息.虽然那篇文章目的上是为了给出一种思路,但实际上awk是可以支持函数局部变量的. awk对于局部变量的支持比起大多数过程 ...

  7. shell与export命令

    围绕以下几个问题来学习export命令: 1.什么是export命令? 2.为什么要用export命令? 3.怎么使用export命令? 1.什么是export命令? ♦ 用户登录到Linux系统后, ...

  8. Word页眉、页码的使用:利用分隔符设置指定页显示页眉,解决页码显示{PAGE \* MERGEFORMAT}问题

    不常编辑对文档有格式要求的朋友来说,偶尔需要编辑指定格式页眉页码的word文档时,会一时不记得如何使用,在网上搜索半天,异常烦躁. 特整理一下,记录下来,备不时只需. 以下操作环境为word2016. ...

  9. 史上最最靠谱,又双叒叒简单的基于MSXML的XML解析指南-C++

    目录 史上最最靠谱,又双叒叒简单的基于MSXML的XML解析指南 流程设计 xml信息有哪几种读取形式(xml文件或wchar) 如何选取节点,and取节点属性有哪些方法? IXMLDOMNode与I ...

  10. Vue.js组件间通信方式总结

    平时在使用Vue框架的业务开发中,组件不仅仅要把模板的内容进行复用,更重要的是组件之间要进行通信.组件之间通信分为三种:父-子:子-父:跨级组件通信.下面,就组件间如何通信做一些总结. 1.父组件到子 ...