阅读 257

把书读薄 | 《设计模式之美》设计模式与范式(行为型-观察者模式)

0x0、引言

为了肝 《Python爬虫从入门到入狱》学习札记 | Python 主题月 + 一些杂事,好久没啃《设计模式之美》了,之前学的都有点忘记了,哈哈,今天继续往下学,本文对应设计模式与范式:行为型(56-57),观察者模式 (Observer Pattern) 。创建型对象创建问题结构型类与对象的组合或封装行为性类或对象间的交互

Tips:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。


0x1、定义

又称 订阅-发布模式,在对象间定义一个一对多的依赖,当一个对象状态改变时,所有依赖对象都会自动收到通知。被依赖对象称作 被观察者,依赖对象称作 观察者

听着有点抽象,写个简单例子帮助理解 (蜜蜂、蝴蝶授粉):

// 观察者:昆虫接口
public interface Insect {
    void startWork();
    void stopWork();
}

// 具体观察者:蜜蜂
public class Bee implements Insect {
    private String name;
    public Bee(String name) { this.name = name; }
    @Override public void startWork() { System.out.println("蜜蜂【" + name + "】开始传粉"); }
    @Override public void stopWork() { System.out.println("蜜蜂【" + name + "】停止传粉"); }
}

// 具体观察者:蜜蜂
public class Butterfly implements Insect {
    private String name;
    public Butterfly(String name) { this.name = name; }
    @Override public void startWork() { System.out.println("蝴蝶【" + name + "】开始传粉"); }
    @Override public void stopWork() { System.out.println("蝴蝶【" + name + "】停止传粉"); }
}

// 被观察者:植物接口
public interface Plant {
    void registerInsect(Insect insect);
    void unregisterInsect(Insect insect);
    void notifyInsect(boolean isOpen);
}

// 具体被观察者:花类
public class Flower implements Plant {
    private final List<Insect> insects = new ArrayList<>();

    @Override public void registerInsect(Insect insect) { insects.add(insect); }

    @Override public void unregisterInsect(Insect insect) { insects.remove(insect); }

    @Override public void notifyInsect(boolean isOpen) {
        if(isOpen) {
            System.out.println("花开咯~");
            for(Insect insect: insects) insect.startWork();
        } else {
            System.out.println("花谢了~");
            for(Insect insect: insects) insect.stopWork();
        }
    }

    // 定义了一个批量解绑的方法
    public void unregisterAllInsect() {
        for(int i = 0; i < insects.size(); i++) unregisterInsect(insects.get(i));
    }
}

// 测试用例
public class ObserverTest {
    public static void main(String[] args) {
        Flower flower = new Flower();
        // 创建并注册观察者
        for (int i = 1; i < 4; i++) {
            flower.registerInsect(new Bee(i + ""));
            flower.registerInsect(new Butterfly(i + ""));
        }
        // 通知观察者
        flower.notifyInsect(true);
        System.out.println("=== 开花期已过 ===");
        // 通知观察者
        flower.notifyInsect(false);
        // 解绑所有观察者
        flower.unregisterAllInsect();
    }
}
复制代码

代码运行输出结果如下:

代码非常简单,老规矩带出UML类图、角色解读、适用场景和优缺点:

角色解读

  • Subject (被观察者) → 又称发布者、主题、目标、被订阅者等,通常指观察者关心的相关对象集合;
  • ConcreteSubject (具体被观察者) → 实现了被观察者定义方法的具体实现类,类中有存放观察者的容器;
  • Observer (观察者) → 又称订阅者,提供被观察者变化对应的反应方法;
  • ConcreteObserver (具体被观察者) → 观察者的具体实现;

适用场景

  • 对象状态改变需要修改其他对象时;
  • 对象发生改变时只想发送通知,而不需要知道接收者是谁;
  • 链式触发机制:在系统中构建一个触发链,A影响B、B影响C;
  • 创建基于事件触发的场景;

优点

观察者与目标间的抽象解耦,提高扩展性,动态联动(一个操作引起其他相关操作)

缺点

互为观察者和被观察者间有相互依赖时要避免相互通知引起的死循环!增加代码的理解难度,观察者对象多时,被观察者通知观察者花费的时间会变成,某正程度上影响程序的效率。


0x2、观察者模式的推与拉

推方式

被观察者 → 观察者推送主题的 详细信息 (通常是被观察者的全部或部分数据),不管观察者是否需要。

拉方式

被观察者 → 观察者,只传递 少量信息,如果观察者需要更详细的信息,可主动到被观察者中获取,一般的实现方式是被观察者自身通过update()方法传递给观察者,观察者再通过这个实例按需获取。

推方式是假定被观察者知道观察者所需的数据,拉方式是被观察者不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传递给观察者,让其按照自己所需取值。


0x3、Java中对观察者模式的支持

Java的java.util包中,提供了一个 Observable 类和 Observer 接口,让我们可以更便捷地实现观察者模式。

核心用法:被观察者实现继承Observable,观察者实现Observer接口,通知变化时,调用setChange方法

简单的代码示例如下:

// 被观察者
import java.util.Observable;
import java.util.Observer;

public class CodingBoy extends Observable {
    private String title;
    private String contentUrl;
    
    public String getTitle() { return title; }
    public String getContentUrl() { return contentUrl; }
    
    public void update(String title, String url) {
        this.title = title;
        this.contentUrl = url;
        System.out.println("抠腚男孩公众号更新了文章:" + title);
        this.setChanged();  // 必不可少,通知改变
        this.notifyObservers(this); // 这里用拉方式
    }
}

// 观察者
public class Fan implements Observer {
    private String name;

    public Fan(String name) { this.name = name; }

    @Override
    public void update(Observable o, Object arg) {
        // 拉方式,通过实例按需获取所需信息
        CodingBoy codingBoy = (CodingBoy) arg;
        System.out.println("粉丝【" + name + "】收到公号文章更新推送[" + codingBoy.getTitle() + "](" + codingBoy.getContentUrl() + ")");
    }
}

// 测试用例
public class ClientTest {
    public static void main(String[] args) {
        CodingBoy codingBoy = new CodingBoy();
        // 注册观察者
        for (int i = 1; i < 4; i++) codingBoy.addObserver(new Fan(i + ""));
        codingBoy.update("《Python爬虫从入门到入狱》学习札记", "https://juejin.cn/post/6985093530473463816");
        // 取消注册观察者
        codingBoy.deleteObservers();
    }
}
复制代码

代码运行输出结果如下:

非常简单~


0x4、加餐:模式应用实例 → EventBus源码解读

观察者模式在不同的场景与需求下,有不同的实现形式,我们上面的实现都属于 进程内同步堵塞,在一些需要快速响应的场景(如注册)就需要把实现方式改为 异步非堵塞,还有在跨进程的场景又得换成其他实现方式,如MQ。

此处剖析下Android中EventBus事务总线的源码(版本:3.1.1),了解下异步非堵塞实现方式的具体玩法~

EventBus的用法很简单:

// 注册订阅者
EventBus.getDefault().register(this);

// 编写响应事件订阅方法,必须添加@Subscribe注解!!!
@Subscribe(threadMode = ThreadMode.BACKGROUND, sticky = true, priority = 100)
public void onMessageEvent(MessageEvent event) { }

// 发送事件
EventBus.getDefault().post(new MessageEvent("Hello EventBus!"));  
EventBus.getDefault().postSticky(new MessageEvent("Hello EventBus!"));    // 粘性事件

// 粘性事件所处理的问题:发布者先发送了事件,但此时订阅者还未产生,
// 一段时间后订阅者才订阅该时间,就是使得发送事件后订阅者再订阅此时间也能收到该事件;

// 解除注册
EventBus.getDefault().unregister(this);
复制代码

① 初始化

先从 EventBus.getDefault() 入手,跟下:

线程安全带懒加载的 DCL单例,实例不存在,调用构造方法初始化了Eventbus的一些配置,

② 订阅者订阅

getDefault()就是获得EventBus单例,往下走,跟 register(类实例)

subscriberMethodFinder.findSubscriberMethods() 返回了一个**SubscriberMethod** 列表,跟下这个类:

了解完订阅方法,往下走,看下 findSubscriberMethods() 是怎么找的:

Tips:EventBus 3.0中提供了EventBusAnnotationProcessor注解处理器在编译期通过读取@Subscribe()注解并解析,处理其中所包含的信息,然后生成java类来保存所有订阅者关于订阅的信息,这样比在运行时使用反射动态获取的速度快,所以ignoreGeneratedIndex默认为false~

findUsingInfo() 就是获取 MyEventBusIndex类 中的信息,遍历生成订阅者中订阅的方法数据,而 findUsingReflection()

跟下prepareFindState():

静态数组保存 FindState 避免重复创建:

上面循环递归订阅类及其父类(直到父类java、javax、android.开头为止),递归同时调用 findUsingReflectionInSingleClass() 方法:

findSubscriberMethods()通过读取编译时生成文件或反射获得订阅类中所有的订阅方法数据,接着看下subscribe() 订阅方法又做了啥:

所以这一步的核心逻辑是:

获取所有订阅了此事件类型的订阅者信息 → 根据优先级将订阅者信息插入订阅者队列 → 获得当前订阅者所有事件队列,将当前事件添加其中方便后续取消订阅 → 如果是粘性事件,直接post此事件给当前订阅者。


③ 订阅者取消订阅

直接跟 unregister() 方法:

跟下 unsubscribeByEventType(),取消订阅还是很简单的~


④ 发布普通事件

跟下 post() 方法:

这里的currentPostingThreadState是一个 ThreadLocal<PostingThreadState>,线程内部存储类,可指定线程存取数据,跟下 postSingleEvent()

是否考虑事件继承,最后都是走的 postSingleEventForEventType(),跟下:

跟下 postToSubscription()

以上就是EventBus对于普通事件的分发,根据订阅事件的线程模式,执行回调的过程。

⑤ 发布粘性(Sticky)事件

相比普通事件就多了一步把事件放到粘性集合里的操作,粘性事件分发的话在上面的subscribe()中已经提到了:

以上就是EventBus主要原理,不是标准的观察者模式实现,但整体就是一个发布/订阅的框架,也拥有观察者模式的优点,如:发布者和订阅者解耦。


参考文献

文章分类
Android