设计模式之观察者模式

40 阅读4分钟

1.简介

观察者模式属于行为型模式(类和对象如何交互,及划分责任和算法)的一种。

定义:

定义了对象之间的一对多的依赖,这样一来,当一个对象改变时,它的所有的依赖者都会收到通知并自动更新。

模式:

1、服务号就是主题,业务就是推送消息

2、观察者只需要订阅主题,只要有新的消息就会送来

3、当不想要此主题消息时,取消订阅

4、只要服务号还在,就会一直有人订阅

当你的业务场景符合 订阅-发布 这套流程时,可以尝试使用这个模式,当然也可以根据具体场景灵活变通。

2.UML图

观察者模式.png

说明:

  • Subject(主题接口) 作为一个主题接口,提供了三个方法。

registerObserver()注册为观察者

removeObeserver() 将自己从观察者中删除

notifyObservers() 更新所有的观察

  • Observer(观察者) 作为一个接口,提供一个方法

update()供外部调用

在观察者模式中,作为主题,每个主题可能会有许多观察者。同时,作为观察者必须注册到主题当中,这样才可以接受更新。(其实这就是订阅和取消订阅公众号的概念)

3.代码实现

我们模拟一个彩票发布的微信公众号,每天发布当天中奖的相关彩票号,订阅此公众号的人可以接受到相关信息,其实也就是 订阅-发布 的一个过程。

可以参考 uml图中构建相关类和接口。首先我们构建主题和观察者这两个接口。

此时主题就相当于微信公众号,观察者就相当于使用的人。

主题接口

package com.gs.designmodel.observermodel.object;
/**
 * @author: Gaos
 * @Date: 2022-10-31 16:10
 *  主题接口,所有的主题必须实现此接口
 **/
public interface Subject {
​
    /**
     * 注册一个观察者
     * @param observer
     */
    public void registerObserver(Observer observer);
​
    /**
     * 移除一个观察者
     * @param observer
     */
    public void removeObserver(Observer observer);
​
​
    /**
     * 通知所有的观察者
     */
    public void notifyObservers();
}
​

观察者接口:

package com.gs.designmodel.observermodel.object;
​
/**
 * @author: Gaos
 * @Date: 2022-10-31 16:19
 *
 * 所有的观察者,都需要实现此接口, java.util 中提供的有此接口
 **/
public interface Observer {
​
    /**
     * todo:
     * @param message
     */
    public void update(String message);
}
​

此时有一个具体的公众号,3D彩票服务号,所以我们需要实现主题接口。

package com.gs.designmodel.observermodel.object;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * @author: Gaos
 * @Date: 2022-10-31 16:23
 **/
public class ObjectFor3D implements Subject{
​
    /**
     * 订阅的观察者集合
     */
    private List<Observer> observers = new ArrayList<>();
​
    /**
     * 3D彩票的号码
     */
    private String message;
​
    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }
​
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
​
    @Override
    public void notifyObservers() {
        for (Observer item : observers) {
            item.update(message);
        }
    }
​
    /**
     * 主题更新消息
     * @param message
     */
    public void setMessage(String message) {
        this.message = message;
        notifyObservers();
    }
}

作为具体的实现,首先它需要有一个 message 来存储到彩票的开奖号码,然后有一个observers来存储订阅的观察者对象。这个是我觉得比较精髓的点,在于它需要维护所有向它订阅的使用者的集合,才可以在需要时(在目前的场景下是每日开奖),通知到各个使用者。

模拟两个使用者:

主题观察者1:

package com.gs.designmodel.observermodel.object;
​
import lombok.extern.slf4j.Slf4j;
​
/**
 * @author: Gaos
 * @Date: 2022-10-31 16:30
 *
 * 主题观察者1
 **/
@Slf4j
public class ObserverOne implements Observer{
​
    private Subject subject;
​
    public ObserverOne(Subject subject) {
        this.subject = subject;
        subject.registerObserver(this);
    }
​
    @Override
    public void update(String message) {
      log.info("ObserverOne得到号码 {}, 已记录", message);
    }
}

主题观察者2:

package com.gs.designmodel.observermodel.object;
​
import lombok.extern.slf4j.Slf4j;
​
/**
 * @author: Gaos
 * @Date: 2022-10-31 16:35
 *
 * 主题观察者2
 **/
@Slf4j
public class ObserverTwo implements Observer{
    private Subject subject;
​
    public ObserverTwo(Subject subject) {
        this.subject = subject;
        subject.registerObserver(this);
    }
​
    @Override
    public void update(String message) {
        log.info("ObserverTwo得到号码 {}, 已记录", message);
    }
}

因为在这种场景下,观察者必须要注册到相应的主题当中去。这里的实现方式是存储相应的主题对象,在向外提供的构造方法中,来进行对相应属性的赋值,并同时注册到观察者中去。

测试代码

package com.gs.designmodel.observermodel.object;
​
/**
 * @author: Gaos
 * @Date: 2022-10-31 16:40
 *
 * 观察者模式 测试类
 **/public class Test {
    public static void main(String[] args) {
        // 模拟3d服务号
        ObjectFor3D objectFor3D = new ObjectFor3D();
        // 客户1
        ObserverOne observerOne = new ObserverOne(objectFor3D);
        // 客户2
        ObserverTwo observerTwo = new ObserverTwo(objectFor3D);
​
        objectFor3D.setMessage("20221031的3d号码是:12345");
        objectFor3D.setMessage("20221030的3d号码是:54321");
    }
}

测试结果

23:52:08.188 [main] INFO com.gs.designmodel.observermodel.object.ObserverOne - ObserverOne得到号码 202210313d号码是:12345, 已记录
23:52:08.191 [main] INFO com.gs.designmodel.observermodel.object.ObserverTwo - ObserverTwo得到号码 202210313d号码是:12345, 已记录
23:52:08.191 [main] INFO com.gs.designmodel.observermodel.object.ObserverOne - ObserverOne得到号码 202210303d号码是:54321, 已记录
23:52:08.191 [main] INFO com.gs.designmodel.observermodel.object.ObserverTwo - ObserverTwo得到号码 202210303d号码是:54321, 已记录

可以感觉到,主题的实现不依赖于使用者,当增加新的使用者时,主题的代码逻辑不需要做改变,使用者拿到数据如何进行处理也与主题无关,在此基础上也可以更好的进行扩展。

4.扩展

JDK中有一些地方使用到了观察者模式,所以在其中其实已经帮助我们实现了观察者模式。

主要的并随便的看一些主题接口和观察者接口。

主题接口:

package java.util;
​
public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs;
​
    /** Construct an Observable with zero Observers. */
​
    public Observable() {
        obs = new Vector<>();
    }
​
    public synchronized void addObserver(Observer o) {
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }
  
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }
​
    public void notifyObservers() {
        notifyObservers(null);
    }
​
    public void notifyObservers(Object arg) {
        /*
         * a temporary array buffer, used as a snapshot of the state of
         * current Observers.
         */
        Object[] arrLocal;
​
        synchronized (this) {
            /* We don't want the Observer doing callbacks into
             * arbitrary code while holding its own Monitor.
             * The code where we extract each Observable from
             * the Vector and store the state of the Observer
             * needs synchronization, but notifying observers
             * does not (should not).  The worst result of any
             * potential race-condition here is that:
             * 1) a newly-added Observer will miss a
             *   notification in progress
             * 2) a recently unregistered Observer will be
             *   wrongly notified when it doesn't care
             */
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }
​
        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }
​
    /**
     * Clears the observer list so that this object no longer has any observers.
     */
    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }
​
    /**
     * Marks this <tt>Observable</tt> object as having been changed; the
     * <tt>hasChanged</tt> method will now return <tt>true</tt>.
     */
    protected synchronized void setChanged() {
        changed = true;
    }
​
    protected synchronized void clearChanged() {
        changed = false;
    }
​
    public synchronized boolean hasChanged() {
        return changed;
    }
​
    public synchronized int countObservers() {
        return obs.size();
    }
}
​

可以看到,其实思路的实现和具体提供的方法,和我们上面 uml和代码中体现的差不多。

多了锁的控制,可能会出现并发的情况。所以存储观察者的集合使用了 Vector 线程安全的list。

观察者1.png

从另一面也体现了这个 JDK提供的观察者相关Api应该很久没有维护了,因为 Vector是相当老的,效率比较低的容器对象了。 同时多了一些方法的实现

观察者2.png 观察者3.png 观察者接口:

package java.util;
​
public interface Observer {
    void update(Observable o, Object arg);
}
​

总体来说,并不推荐使用 jdk 自带的观察者模式,但可以借鉴其中的思想。

一方面是因为里面的相关容器比较老,所以可能会有一些性能方面的问题。

另一方面它所提供的主题,是以类的方式提供的,而并非接口,在 java 的单继承多实现的机制中就不太容易进行扩展和维护。

再一方面,它的实现并不困难,在业务比较复杂的场景中,它的API过于简单往往还需要自己扩展。

再来说一说它那两个方法的目的,我们可以思考换一种场景。

在订单创建后,系统会发送邮件短信,并保存日志记录。在这里订单就是主题,邮件短信和日志其实就是观察者了。

此时后续的观察者的状态的改变(邮件、短信、日志),其实还依赖于主题(订单)本身的一个状态,所以需要这么一个设置来进行更好的扩展。如果没有这个的话那么主题的每一次更改都需要通知到相关的观察者,这并不合理,所以我们如果自定义实现的话,也可以借鉴其中的思想,制定好相关的业务规则。

相关参考文章:

blog.csdn.net/lmj62356579… www.awaimai.com/patterns/ob…