Java设计模式—观察者模式

前言:在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,某种商品的物价上涨时会导致部分商家高兴,而消费者伤心;还有,当我们开车到交叉路口时,遇到红灯会停,遇到绿灯会行。在软件世界也是这样,例如,Excel 中的数据与折线图、饼状图、柱状图之间的关系;MVC 模式中的模型与视图的关系;事件模型中的事件源与事件处理者。所有这些,如果用观察者模式来实现就非常方便。


一、观察者模式简介

1.1、观察者模式概述

观察者模式(Observer),又叫发布-订阅模式(Publish/Subscribe),定义对象间一种一对多的依赖关系(注册),使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新(通知)。说白了就是个注册,通知的过程。

UML结构图如下:

观察者模式的结构图

实现观察者模式时要注意具体目标对象和具体观察者对象之间不能直接调用,否则将使两者之间紧密耦合起来,这违反了面向对象的设计原则。

观察者模式的主要角色如下:

  • 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。

  • 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。继承Subject类,在这里实现具体业务,在具体项目中,该类会有很多变种。

  • 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。

  • 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。

1.2、观察者模式的优缺点

观察者模式是一种对象行为型模式,其主要优点如下:

  1. 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。

  2. 目标与观察者之间建立了一套触发机制。

它的主要缺点如下:

  1. 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。

  2. 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

1.3、观察者模式注意事项

1、JAVA 中已经有了对观察者模式的支持类。

2、避免循环引用。

3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。

1.4、异步处理线程安全问题

被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步呗,异步处理就要考虑线程安全和队列的问题,这个大家有时间看看 Message Queue,就会有更深的了解。


二、观察者模式实现

2.1、主题Subject

首先定义一个观察者数组,并实现增、删及通知操作。它的职责很简单,就是定义谁能观察,谁不能观察,用Vector是线程同步的,比较安全,也可以使用ArrayList,是线程异步的,但不安全。

public class Subject {

    //观察者数组
    private Vector oVector = new Vector<>();

    //增加一个观察者,相当于观察者注册
    public void addObserver(Observer observer) {
        this.oVector.add(observer);
    }

    //删除一个观察者
    public void deleteObserver(Observer observer) {
        this.oVector.remove(observer);
    }

    //通知所有观察者,主题有变化时通知观察者
    public void notifyObserver() {
        for(Observer observer : this.oVector) {
            observer.response();
        }
    }

}

 2.2、具体主题

继承Subject类,在这里实现具体业务,在具体项目中,该类会有很多变种。

public class ConcreteSubject extends Subject {

    //具体业务
    public void doSomething() {
        //...

        System.out.println("具体目标发生改变...");
        System.out.println("--------------");
        super.notifyObserver();
    }

}

2.3、抽象观察者Observer

观察者一般是一个接口,每一个实现该接口的实现类都是具体观察者。

public interface Observer {
   //响应
    public void response();
}

2.4、具体观察者

实现Observer接口。

//具体观察者1
class ConcreteObserver1 implements Observer {
    public void response() {
        System.out.println("具体观察者1作出反应!");
    }
}

//具体观察者1
class ConcreteObserver2 implements Observer {
    public void response() {
        System.out.println("具体观察者2作出反应!");
    }
}
​

2.5、ObserverPattern的main方法

首先创建一个被观察者,然后定义一个观察者,将该被观察者添加到该观察者的观察者数组中,进行测试。

public class ObserverPattern {

    public static void main(String[] args) {
        //创建一个主题
        ConcreteSubject subject = new ConcreteSubject();
        //定义一个观察者
        Observer observer = new ConcreteObserver();
        //注册观察者
        subject.addObserver(observer);
        //开始活动
        subject.doSomething();
    }

}

输出结果:

具体目标发生改变... --------------

具体观察者1作出反应!

具体观察者2作出反应!


三、Java中的观察者模式

在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。

3.1、Observable类

Observable 类是抽象目标类,它有一个 Vector 向量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。

  • void addObserver(Observer o) 方法:用于将新的观察者对象添加到向量中。

  • void notifyObservers(Object arg) 方法:调用向量中的所有观察者对象的 update() 方法,通知它们数据发生改变。通常越晚加入向量的观察者越先得到通知。

  • void setChange() 方法:用来设置一个 boolean 类型的内部标志位,注明目标对象发生了变化。当它为真时,notifyObservers() 才会通知观察者。

3.2、Observer 接口

Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 void update(Observable o,Object arg) 方法,进行相应的工作。

3.3、利用 Observable 类和 Observer 接口实现原油期货的观察者模式实例

分析:当原油价格上涨时,空方伤心,多方局兴;当油价下跌时,空方局兴,多方伤心。本实例中的抽象目标(Observable)类在 Java 中已经定义,可以直接定义其子类,即原油期货(OilFutures)类,它是具体目标类,该类中定义一个 SetPriCe(float price) 方法,当原油数据发生变化时调用其父类的 notifyObservers(Object arg) 方法来通知所有观察者;另外,本实例中的抽象观察者接口(Observer)在 Java 中已经定义,只要定义其子类,即具体观察者类(包括多方类 Bull 和空方类 Bear),并实现 update(Observable o,Object arg) 方法即可。图 5 所示是其结构图
 

原油期货的观察者模式实例的结构图 原油期货的观察者模式实例的结构图

 程序代码如下:

package net.biancheng.c.observer;

import java.util.Observer;
import java.util.Observable;

public class CrudeOilFutures {
    public static void main(String[] args) {
        OilFutures oil = new OilFutures();
        Observer bull = new Bull(); //多方
        Observer bear = new Bear(); //空方
        oil.addObserver(bull);
        oil.addObserver(bear);
        oil.setPrice(10);
        oil.setPrice(-8);
    }
}

//具体目标类:原油期货
class OilFutures extends Observable {
    private float price;

    public float getPrice() {
        return this.price;
    }

    public void setPrice(float price) {
        super.setChanged();  //设置内部标志位,注明数据发生变化
        super.notifyObservers(price);    //通知观察者价格改变了
        this.price = price;
    }
}

//具体观察者类:多方
class Bull implements Observer {
    public void update(Observable o, Object arg) {
        Float price = ((Float) arg).floatValue();
        if (price > 0) {
            System.out.println("油价上涨" + price + "元,多方高兴了!");
        } else {
            System.out.println("油价下跌" + (-price) + "元,多方伤心了!");
        }
    }
}

//具体观察者类:空方
class Bear implements Observer {
    public void update(Observable o, Object arg) {
        Float price = ((Float) arg).floatValue();
        if (price > 0) {
            System.out.println("油价上涨" + price + "元,空方伤心了!");
        } else {
            System.out.println("油价下跌" + (-price) + "元,空方高兴了!");
        }
    }
}

程序运行结果如下:

油价上涨10.0元,空方伤心了!

油价上涨10.0元,多方高兴了!

油价下跌8.0元,空方高兴了!

油价下跌8.0元,多方伤心了!

3.4、Java中手写线程安全的观察者模式

先来定下一种场景,我每个月都会在微信上收到包租婆的房租单,假设包租婆是被观察者,我是观察者,租客我把联系方式留给包租婆这就是一个注册过程,每个月月初,包租婆都会通知我们交租(被观察者变化通知观察者)。

package com.hs.demo.design;

import java.util.Observable;
import java.util.Observer;

public class JavaObserverPattern
{
    public static void main(String[] args)
    {   
        Exploiter exploiter = new Exploiter();

        TheOppressed theOppressed1 = new TheOppressed("打工仔1");
        TheOppressed theOppressed2 = new TheOppressed("打工仔2");
        TheOppressed theOppressed3 = new TheOppressed("打工仔3");

        //房东给打工仔(观察者)注册
        exploiter.addObserver(theOppressed1);
        exploiter.addObserver(theOppressed2);
        exploiter.addObserver(theOppressed3);

        //(被观察者)房东发信息了,通知所有观察者交房租
        exploiter.postChange("打工仔们快来交房租啦~~");

    }
}

//Observer抽象观察者
class TheOppressed implements Observer
{
    public String name;
    public TheOppressed(String name){
        this.name = name;
    }

    @Override
    public void update(Observable o, Object arg) {
        System.out.println(name +" 收到要交租信息");
    }
}

//Observable抽象目标类
class Exploiter extends Observable
{

    public void postChange(String content)
    {
        setChanged();
        notifyObservers(content);
    }

}

输出结果:

打工仔3 收到要交租信息
打工仔2 收到要交租信息
打工仔1 收到要交租信息

其实在JDK9之后,Observer 这些都已经被废弃了,主要因为它

  • 不可序列化,Observable是个类,而不是一个接口,没有实现Serializable,所以,不能序列化和它的子类

  • 没有线程安全,方法可以被其子类覆盖,并且事件通知可以以不同的顺序并且可能在不同的线程上发生。

  • 可以使用PropertyChangeEvent和PropertyChangeListener,它是java.beans包下的类

手写线程安全的观察者模式

其实我们从上面很容易看出来,多观察者需要串行调用,被观察者发生动作,观察者要作出回应,如果观察霆太多,而且处理时间长怎么办?用异步,也许你会脱口而出,那么,异步的处理就要考虑到线程安全和队列的问题。

就是刚刚同样的场景,如果房东先发了一条涨租200,后来又发了一条,收房租(当然要多准备200),假设延迟性是非常的大的情况下,我们不可能单线程串行一直等,太费性能了,开了多线程的情况下,那么就会出现问题,可能某人会先收到交房租,这样就乱了。

现实中有好多这种并发场景,一个或者多个线程,要等待另一组线程执行完成后,才能继续执行的问题,jdk已的com.util.concurrent下为我们提供了很多多线程同步的类,我们可以使用 CountDownLatch来保证被观察者的多个消息之间是有先后顺序的。

总结:

观察者模式主要是对象的解耦,将观察者与被观察者之间完全隔离。jdk提供的默认观察者Observer/Observable在多线程下有安全性问题,需要自己手写,JDK9之后已经废弃了。


参考链接:

深入理解设计模式(八):观察者模式

重学设计模式——线程安全的观察者模式 

你可能感兴趣的:(Java基础,观察者模式,发布-订阅模式,Observable抽象目标类,Observer抽象观察者接口)