Observer之谬何在?
引言
写这篇博客的原因就是在看了陈硕前辈的muduo后对Observer的一些思考.
首先简单说说Observer模式,先来看看它的定义:
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
我觉得这其实说不上是一种模式,而是一种很自然的思考方式,一个subject,多个Observer,需求就是当subject的某个条件完成的时候对注册的observer进行通知,那么我们该如何实现呢,一般给出的解决方案是这样的,
即给出一个subject(Observable)的基类,然后再给出一个observer的基类(接口),我们要做的只是用一个发布者的对象去继承subject,然后再用一个订阅者的类去继承(实现)Observer,这个时候一个Observer模式的基础就已经OK了,暂不考虑其中的线程安全问题,接下来我们要做的其实就是把Observer这个类加到subject中,然后在条件满足时进行执行即可,我们要做的仅仅是用实现几个函数而已,截止目前为止,面向对象表现良好.对象范式的基本概念之一就是对象之间互相发送消息,协作完成任务,消息就我们想成函数调用,那么我们需要知道这个从类实例化出来的对象是否有这么一个成员函数,这个时候我们得知道其地址,然后知道其类型,从而知道如何调用这个函数,这个时候面向对象不就成了面向类了吗?这个问题先放下,我们再回到Observer模式,从上面所说的解决方案反推,我需要实现一个Observer对象,即被通知的对象,我得继承Observer基类才可以,不然根本无法被存储在subject中,更何谈被通知呢,这导致了如果观察者如果想要观察多个消息,那就得去继承,多继承出现了,随着多继承的引入,自然得打上虚继承这个补丁,如果想要一个对象被通知不同的事件,举个简单的例子,subject条件为A的时候通知我,subject条件为B的时候也通知我,那么我们不得不写一系列条件判断,多么丑陋写法啊,不止丑陋,同样低效,当这个数字上升100倍时意味着每个update函数中有着大量的无效代码,且使类变得臃肿,但好像别无方法,着就是这么写的缺陷所在.程序的本质是程序员对于这个世界的抽象,各种模式当然也是对事物行为的抽象,那么Observer模式本身就当然没有问题,那么为什么会出现上面所说的情况呢?以此角度来看好像就是我们对于面向对象的理解出现了偏差.这个偏差就是把面向对象范式理解成了面向类,站在对象的角度来看我只是想和你通信啊,为什么要强加继承这层关系呢?这就出现了上面的问题.但是这并不意味着类这个概念有什么不对的,只是不能粗鲁的把两者混为一谈罢了.
解决这个问题就需要一个对象级别的通信机制,C++中的实现的机制就是function,bind,lambda与closure,利用这个机制我们可以轻松的解决上面提到的问题,具体的代码在陈硕前辈的muduo中有一个很好的实现,就拿其做一个样例:
template<typename Callback>
struct SlotImpl;
template<typename Callback>
struct SignalImpl : boost::noncopyable{
using Slotlist = std::vector<std::weak_ptr<SlotImpl<Callback>>>;
SignalImpl() : slots_(new Slotlist){}
void CopyOnWrite(){ //用于修改时不进行插入
if(!slots_.unique()){
slots_.reset(new Slotlist(*slots_));
}
}
void clean(){
std::lock_guard<std::mutex> guard(mutex_);
CopyOnWrite();
Slotlist& list(*slots_);
auto current = list.begin();
while(current != list.end()){
if(current->expired()){
current = list.erase(current);
}else{
++current;
}
}
}
std::shared_ptr<Slotlist> slots_;
std::mutex mutex_;
};
template<typename Callback>
struct SlotImpl : boost::noncopyable{
using Data = SignalImpl<Callback>;
std::weak_ptr<Data> data_;
Callback cb_;
std::weak_ptr<void> tie_;
bool tied_;
SlotImpl(const std::shared_ptr<Data>& data, Callback&& cb)
:data_(data), cb_(cb), tie_(), tied_(false){}
SlotImpl(const std::shared_ptr<Data>& data, Callback&& cb, const std::shared_ptr<void>& tie)
:data_(data), cb_(cb), tie_(tie), tied_(true){}
~SlotImpl(){
auto data(data_.lock());
if(data){
data->clean();
}
}
};
using Slot = std::shared_ptr<void>;
template<typename Signature> //用一个参数搞出两个类型
class Signal;
template<typename RET, typename... ARGS>
class Signal<RET(ARGS...)> : boost::noncopyable{ //特化版本
public:
using Callback = std::function<void(ARGS...)>;
using SignalComponent = SignalImpl<Callback>;
using SlotComponent = SlotImpl<Callback>;
Signal() : pool(new SignalComponent()){}
Slot connect(Callback&& func){
std::shared_ptr<SlotComponent> slot(
new SlotComponent(pool, std::forward<Callback>(func))
);
add(slot);
return slot;
}
Slot connect(Callback&& func, const std::shared_ptr<void>& tie){ //控制生命周期
std::shared_ptr<SlotComponent> slot(
new SlotComponent(pool, std::forward<Callback>(func), tie)
);
add(slot);
return slot;
}
void call(ARGS&&... args){ //显然也可以是void
SignalComponent& impl(*pool); //不增加引用计数的拷贝
std::shared_ptr<typename SignalComponent::Slotlist> slots;
{
std::lock_guard<std::mutex> guard(pool->mutex_);
//std::lock_guard<std::mutex> guard(impl.mutex_);
slots = impl.slots_;
}
typename SignalComponent::Slotlist& List(*slots); //直接指针使用也可
for(auto item : List){
std::shared_ptr<SlotComponent> slot = item.lock();
if(slot){
std::shared_ptr<void> flag;
if(slot->tied_){
flag = slot->tie_.lock();
if(flag){
slot->cb_(args...);
}
}else {
slot->cb_(args...);
}
}
}
}
private:
void add(const std::shared_ptr<SlotComponent>& slot){
SignalComponent& impl(*pool);
{
std::lock_guard<std::mutex> guard(impl.mutex_);
impl.CopyOnWrite();
impl.slots_->push_back(slot);
}
}
std::shared_ptr<SignalComponent> pool;
};
简单的解释一下上面的代码,我们的发布者成了Signal,在初始化时需要指定一个接收的函数类型,值得一提的是这个类型是一个可被强制转换的类型,即这里把返回值指定为void,已接受更广泛的对象,你可能会发牢骚,那返回值怎么办,我们知道多线程接收返回值future是个不错选择,当然这种情况下promise也可以解决你的问题.我们继续回到这个代码.SlotImpl就是观察者,不过我们并不需要管它而已,相当于一个wrapper,我们只需要传入一个可调用对象而已,然后在SignalImpl中管理SlotImpl,其中用到了weak_ptr和Copy-on-write,这个链接中有提及这个copy_on_write的实现,weak_ptr的使用是为了保证多线程析构时的线程安全,这并不是本篇文章的重点.
其实这段代码本身是比较能说明问题,这是一个线程安全的Observer的实现,且避开了文章上面提到的一般Observer模式的写法会遇到的问题,其实这样看来我们完全可以对Observer模式下一个定义,即一对多回调.
补充一篇文章,写下这篇博客以后遇到的一篇文章,基本和我的这篇博客说了同一个问题,但是主题更加精炼,即接口与回调
参考:
https://blog.csdn.net/u011814346/article/details/71413142
https://blog.csdn.net/fly_wt/article/details/90896170
muduo