观察者模式实战

本文转自“雨夜随笔”公众号,欢迎关注。

上一篇文章 中,我们简单了讲了一下设计模式和观察者模式。那么这次让我们详细了解一下观察者模式和我们如何进行使用。

内容

还记得 上一篇文章 中,我们说过了解或者设计一个设计模式的时候,要按照下面四点进行分析:

image.png

意图

在实际业务场景中,我们经常遇到这样的稳定,也就是在某个对象发生某种事件时,通知其他相关的对象。所以观察者模式就允许你定义一种订阅机制,使得订阅某种事件的对象能够在事件发生时得到通知。

动机

我们来分析一种常见的业务场景,也就是购物APP中经常遇到的货物上架提醒,如果为了实现这种需求,我们可以按照下面的方式来实现:

  • 成本最低 :我们知道在开发中成本最低的就是不做这个需求,当然这个是开玩笑,不过一般在开发初期,我们可能会分析需求的重要性和成本,那么这个需求可能并不是我们的第一优先级的任务,因为用户可以自己来看货物是否上架。所以即使不做,也不会影响到核心的功能。只是会影响用户的体验,而这个在开发初期是可以理解的。
  • 成本最高 :那么现在确定一定要做这个需求,我们在不知道其他的方式下可以选择实现最为方便但是成本最高的方式,那就是货物一上架就通知所有的人,这样虽然可以解决问题,但是那些不感兴趣的人可能会因为经常收到提醒信息而感到烦恼。这个其实是不允许的,因为这个会影响大部分用户的体验。但是目前很多APP都会推送广告信息给所有人,因为在不确定感兴趣的用户是哪些时,这个方法最为方便。

[图片上传失败…(image-e07148-1588475947545)]

这时候如果上面两种方式都被否决后,我们就需要想别的方式,这个时候我们会想到,通过维护一个感兴趣用户的列表,一旦货物上架,就将消息推送给这个列表的用户。那么这个就是观察者模式提供的解决方案。

观察者模式建议为事件发布者提供添加订阅的机制,这样其他对象都可以选择订阅或者取消订阅事件。而事件发生后,发布者可以将事件发送给已经订阅的用户。

结构

通过上面的解决方案,我们很容易得到观察者模式的结构:

  • 发布者 :事件的传播者,可能是事件的创造者或者接收者,但是最主要的是其职责是为了将事件进行传播出去。内部主要包括提供事件的订阅和取消订阅方法,而且一般为了方法,会提供统一的事件接收接口,这样可以在事件发生时去遍历订阅列表,调用事件接收接口去传播事件。

image.png

  • 订阅者 :事件的关注者,主要处理收到事件后进行相应的操作。

image.png

代码实现

观察者模式在很多语言都有内置的实现方式,这里我们主要介绍多个事件订阅的实现方式(代码使用java,你可以用其他语言来进行实现),单个事件的订阅更为简单,可自行实现:

  • 首先检查业务逻辑,然后试着拆成两部分,核心事件封装作为发布者,其他代码作为订阅类
  • 定义订阅者接口
interface SubscriberInterface {
  void onReceive(String event);
}
  • 定义订阅者对象,实现自己的事件处理逻辑
public class Subscriber implements SubscriberInterface {
  @Override
  public void onReceive(String event) {
    System.out.printf("receive event: %s\n", event);
  }
}
  • 定义发布者接口,并提供订阅和取消订阅的方式。并提供事件的触发接口(接受或者创建):
interface PublisherInterface {
  boolean subscribe(String eventType , SubscriberInterface subscriber);
  boolean unSubscribe(String eventType , SubscriberInterface subscriber);
  void onEvent(String event);
}
  • 定义发布者对象,提供订阅列表的数据结构和存放方式(一般来说可以定义在发布者抽象类中,不过如果有很多个发布者对象,每个可以定义自己维护的订阅列表)。
public class Publisher implements PublisherInterface {
  Map> subscriberMap = new HashMap();

  @Override
  public boolean subscribe(String eventType, SubscriberInterface subscriber) {
    // 第一次订阅事件,创建订阅列表并加入订阅者
    if(!subscriberMap.containsKey(eventType)){
      List subscribers = new ArrayList<>();
      subscribers.add(subscriber);
      subscriberMap.put(eventType, subscribers);
    }
    // 已经订阅的,不做任何反应
    if(subscriberMap.get(eventType).contains(subscriber)){
      return true;
    }
    // 将订阅者加入到订阅列表
    subscriberMap.get(eventType).add(subscriber);
    return true;
  }

  @Override
  public boolean unSubscribe(String eventType, SubscriberInterface subscriber) {
    // 如果不存在订阅类型,返回失败
    if (!subscriberMap.containsKey(eventType)){
      return false;
    }
    // 如果之前没有订阅,不做任何反应
    if(!subscriberMap.get(eventType).contains(subscriber)){
      return true;
    }
    subscriberMap.get(eventType).remove(subscriber);
    return true;
  }

  @Override
  public void onEvent(String event){
    // 遍历订阅者列表,并调用接受接口
    for(SubscriberInterface subscriber : subscriberMap.get(event)){
      subscriber.onReceive(event);
    }
  }
}
  • 客户端,这里是单独做一个客户端,也可以直接在发布者或者订阅者里面定义,见 上一篇文章 的推模式和拉模式:
public class Application {
  private static final String EVENT_1 = "event1";
  private static final String EVENT_2 = "event2";

  public static void main(String[] args){
    // 定义对象
    PublisherInterface publisher = new Publisher();
    SubscriberInterface subscriber1 = new Subscriber();
    SubscriberInterface subscriber2 = new Subscriber();

    // subscriber1订阅事件1,subscriber2订阅事件2
    publisher.subscribe(EVENT_1, subscriber1);
    publisher.subscribe(EVENT_2, subscriber2);

    // 触发事件1, subscriber1响应
    publisher.onEvent(EVENT_1);

    // subscriber1取消订阅事件1
    publisher.unSubscribe(EVENT_1, subscriber1);

    // 触发事件1, 因为没有订阅者,不响应
    publisher.onEvent(EVENT_1);

    // 触发事件2,subscriber2响应
    publisher.onEvent(EVENT_2);
  }
}

// 输出如下
//  receive event: event1
//  receive event: event2

上面的代码已经放到Github中了: https://github.com/soolaugust/observer-pattern-demo

使用

上面我们了解了观察者模式应对的问题和解决方案细节,那么在实际业务场景中我们如何使用观察者模式,这个是最主要的,在 上一篇文章 中,我们说过设计模式不能滥用,要根据具体业务情况来进行使用,这里我们来了解观察者模式使用上的注意事项。

应用场景

通常来说,当遇到下面的场景时,观察者模式都比较适合:

  • 当一个对象状态的改变需要改变其他对象时。
  • 实际对象是无法预知或者动态变化时。
  • 需要在具体的对象的某种动作中注入代码逻辑,这种在界面类中最为常见,比如定义在按钮按下时执行逻辑。
  • 当一个对象必须“观察”其他对象时。

而在下面的场景时,观察者模式就没必要使用,或者说需要换成或者配合更为适合的设计模式:

  • 如果事件需要顺序经过一系列接收者,那么观察者模式这种不不太适合了,因为原生的观察者模式的通知顺序是随机的,这时就需要责任链模式,这个我们以后也会进行介绍。
  • 如果应用场景非常简单的话,那么就没有必要拆分业务逻辑。这样使用观察者模式就显得没有必要了。
  • 如果需要和其他不可控的对象进行交流,或者以后的组件多样或者不可知的话,那么就不能简单使用观察者模式,而需要配合中介模式,定义一个中介者,并提供统一的事件推送接口和事件接收接口。可以参考 上一篇文章 说到的中介模式。

最佳实践

学习设计模式最好的例子永远是语言或者平台的内部实现,这里我们来研究一下他们是如何应用观察者模式的。

Android

Android中新特性中加入了LiveData,是一种可观察的数据存储类。官方介绍如下:

image.png

如果是按照我们上面介绍的代码实现,我们应该会这么做:

  • 定义订阅者,并提供事件接收方法。这里的事件应该是数据改动。
  • 定义发布者,这里应该就是LiveData,并提供订阅和取消订阅的方式,以及事件触发方法。
  • 定义客户端,来声明订阅者和发布者,并注册订阅者。

客户端

class Activity extends AppCompatActivity {
    // 定义发布者和事件类型
    LiveData data = new MutableLiveData();
    
    public MutableLiveData getData() {
        return data;
    }
    
    // 响应事件
    public Observer dataObserver = new Observer() {
        @Override
        public void onChanged(@Nullable final String newData) {
            dataUI.setText(newData);
        }
    };
    // 订阅事件
    getData().oberve(this, dataObserver);
    
    // 取消订阅
    getData().removeObserver(this);
    
    // 触发事件
    button.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            String newData = "new data";
            getData().setValue(newData);
        }
    });
}

源码

那么我们看一下内部实现是如何做的(这里主要列出相关代码,并为了方便,更改了代码顺序),首先内部相关的代码有三部分,分别是:

  • Observer.java : 订阅者接口,这里同时增加了事件,也就是数据。
public interface Observer {
    // 事件接收方法
    void onChanged(T t);
}
  • MutableLiveData.java : LiveData的子类,主要是开放了setValue和postValue,也就是事件触发方法
  • LiveData.java :主类,我们可以在里面看到订阅者的创建,订阅列表的存储结构,订阅和取消订阅方式以及事件触发方法:
...;
// 这里首先定义的是一个抽象类,和接口一样,不过可以有默认实现,
// 这个也是Java中发布者接口常用的实现方式,因为可以将订阅者列表定义在这里面
public abstract class LiveData {
    // 装饰器模式基类,封装了激活状态的处理
    private abstract class ObserverWrapper {
        final Observer mObserver;
        ...;

        ObserverWrapper(Observer observer) {
            mObserver = observer;
        }

        ...;
        void activeStateChanged(boolean newActive) {
            if (newActive == mActive) {
                return;
            }
            ...;
            // 这里是为了保证订阅者被激活后立刻受到最新的事件状态
            // 并且保证订阅者没有被激活的情况下不会收到任何事件
            if (mActive) {
                dispatchingValue(this);
            }
        }
    }
    
    // 订阅者装饰器具体实现类,这里的LifecycleOwner是生命周期接口
    // Activity, Fragment或Service都实现了这个接口
    class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer observer) {
            super(observer);
            mOwner = owner;
        }
        
        ...;
    }
    
    // 这里可以看出订阅列表的存储数据结构是SafeIterableMap
    // 特点是可以在遍历中删除元素,这个我们在之后会进行相关内容讲解
    // 和我们的方式不同的是,这里第一个参数就是相应的订阅者
    // 第二个参数就是对应的订阅者封装类
    private SafeIterableMap, ObserverWrapper> mObservers =
            new SafeIterableMap<>();
    ...;
    
    // 构造方法,这里value就是要订阅的数据
    // 如果看过之前的分布式锁,可以看出这里使用了乐观锁中的版本号机制
    public LiveData(T value) {
        mData = value;
        mVersion = START_VERSION + 1;
    }
    
    // 触发事件方法,注意这里是protected,也就是只有本包和子类可以访问
    // 这里也是很好的设计哲学,也就是访问控制
    protected void setValue(T value) {
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }
    
    void dispatchingValue(@Nullable ObserverWrapper initiator) {
        if (mDispatchingValue) {
            mDispatchInvalidated = true;
            return;
        }
        mDispatchingValue = true;
        do {
            mDispatchInvalidated = false;
            if (initiator != null) {
                // 如果指定接收者,直接通知订阅者
                considerNotify(initiator);
                initiator = null;
            } else {
                // 否则通知全部注册的订阅者
                for (Iterator, ObserverWrapper>> iterator =
                     mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

    
    private void considerNotify(ObserverWrapper observer) {
        ...;
        // 检查版本
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        // 调用订阅者的订阅事件的onChanged方法
        observer.mObserver.onChanged((T) mData);
    }
    
    ...;
    // 订阅方法
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
        ...;
        // 将订阅者添加到订阅列表中
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        ...
    }
    ...;
    // 取消订阅方法
    public void removeObserver(@NonNull final Observer observer) {
        ...;
        ObserverWrapper removed = mObservers.remove(observer);
        if (removed == null) {
            return;
        }
        ...
    }
}

我们在这里看到了源码中看到不仅仅使用了观察者模式,还配合使用了装饰器模式。这个我目前还没研究,后续有研究了再来讨论,不过基本上符合我们上面的流程。同时结合 上一篇文章 所说的设计模式的使用可以根据具体的业务场景进行搭配和替换。

Go语言

上一篇文章 中说的Go语言的Channel设计时观察者模式,在分析之后发现是错误的,channel因为主要是进行一对一消息的传递,所以不需要强行应用观察者模式来增加复杂度。在Go语言中,更适合观察者模式的场景主要是对于系统信号的处理,也就是Signal包。在Go程序中,这种经常会遇到需要监听系统信号的场景,和我们上面说的观察者模式应用场景很符合。

按照我们上面介绍的代码实现,我们应该会这么做:

  • 定义订阅者,并提供事件接收方法。这里的事件应该是系统事件。
  • 定义发布者,这里就是系统,并提供订阅和取消订阅的方式,以及事件触发方法。
  • 定义客户端,来声明订阅者和发布者,并注册订阅者。

客户端

ch := make(chan os.Signal, 1)
// 订阅事件
signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGSTOP, syscall.SIGUSR1)

// 取消订阅事件
signal.Ignore(syscall.SIGHUP, syscall.SIGQUIT);

// 响应事件
for {
    s := <-ch
    switch s {
        case syscall.SIGQUIT:
        log.Infof("SIGSTOP")
        return
        case syscall.SIGSTOP:
        log.Infof("SIGSTOP")
        return
        case syscall.SIGHUP:
        log.Infof("SIGHUP")
        return
        case syscall.SIGKILL:
        log.Infof("SIGKILL")
        return
        case syscall.SIGUSR1:
        log.Infof("SIGUSR1")
        return
        default:
        log.Infof("default")
        return
    }
}

源码

我们可以看到事件定义和触发事件都是由系统做了(同样对代码进行了省略和调整)。那么我们看一下Go语言是如何实现订阅和取消订阅接口:

// 订阅者的数据结构
var handlers struct {
    sync.Mutex
    // 订阅者列表,这里是一个map结构
    m map[chan<- os.Signal]*handler
    ...
}

// 取消订阅
func Ignore(sig ...os.Signal) {
    cancel(sig, ignoreSignal)
}

func cancel(sigs []os.Signal, action func(int)) {
    handlers.Lock()
    defer handlers.Unlock()

    remove := func(n int) {
        var zerohandler handler

        for c, h := range handlers.m {
            if h.want(n) {
                ...
                if h.mask == zerohandler.mask {
                    delete(handlers.m, c) // 将订阅者从列表中删除
                }
            }
        }

        ...
    }

    // 不填时去除全部订阅者
    if len(sigs) == 0 {
        for n := 0; n < numSig; n++ {
            remove(n)
        }
    } else {
        // 去除指定的订阅者
        for _, s := range sigs {
            remove(signum(s))
        }
    }
}

// 订阅方法
func Notify(c chan<- os.Signal, sig ...os.Signal) {
    if c == nil {
        panic("os/signal: Notify using nil channel")
    }

    handlers.Lock()
    defer handlers.Unlock()

    h := handlers.m[c]
    if h == nil {
        if handlers.m == nil {
            handlers.m = make(map[chan<- os.Signal]*handler)
        }
        h = new(handler)
        handlers.m[c] = h
    }

    ...
}

这里我们可以看到和LiveData相比简单了许多,第一是事件的定义和触发都是由系统做的,第二是这里是使用悲观锁机制,并没有和LiveData一样使用乐观锁。第三是这里也没有使用其他的设计模式。而这些是因为场景较为简单,所以设计上并没有过于复杂,这个和我们之前说的根据具体场景来选择设计模式如出一辙。

总结

从上面看实现一个观察者模式并不难,但是重要的是结合业务场景去进行相应的取舍。如何拆分好发布者和订阅者,并理清两者的边界才是应用观察者模式需要关心的问题。

同样,并不是所有涉及到状态订阅的都需要使用观察者模式,业务简单的没有必要使用观察者模式来增加复杂度,业务复杂的更需要其他模式来进行涉及。