观察者模式与不同通知的实现

3
我正在尝试创建一个观察者模式,其中主题使用不同的通知通知观察者。
通常在观察者模式实现中,您只能看到一个名为“notify”的方法,它通知观察者发生了某些事情,并且具有一种反转,其中观察者持有主题的指针,并在被通知时请求主题的内容。
我正在以稍微不同的方式实现此功能,其中主题附加观察者并通知所有观察者,而无需在观察者内部持有主题的指针。例如:
#include <iostream>
#include <vector>

class ObserverEvents
{
public:
    virtual addSomethingOne(int) = 0;
    virtual addSomethingTwo(float) = 0;
};

class Observer : public ObserverEvents
{
public:
    Observer();
    ~Observer();

    virtual addSomethingOne(int) {}
    virtual addSomethingTwo(float) {}
};

class Subject : public ObserverEvents
{
public:
    Subject() {}

    ~Subject()
    {
        for (int i = 0; i < observers.size(); ++i)
        {
            delete observers[i];
        }
    }

    void attach(Observer * observer)
    {
        observers.push_back(observer);
    }

    virtual addSomethingOne(int something)
    {
        for (int i = 0; i < observers.size(); ++i)
        {
            observers[i].addSomethingOne(something);
        }
    }

    virtual addSomethingTwo(float something)
    {
        for (int i = 0; i < observers.size(); ++i)
        {
            observers[i].addSomethingTwo(something);
        }
    }
private:
    std::vector<Observer *> observers;
};

class FooObserver : public Observer
{
public:
    BarObserver() {}
    ~BarObserver() {}

    addSomethingOne(int something)
    {
        // do something with something
    }

    addSomethingTwo(float something)
    {
        // do something with something
    }
};

class BarObserver : public Observer
{
public:
    BizObserver() {}
    ~BizObserver() {}

    addSomethingOne(int something)
    {
        // do something with something
    }

    addSomethingTwo(float something)
    {
        // do something with something
    }
};

int main(int argc, char const * argv[])
{
    Subject subject;
    subject.attach(new FooObserver());
    subject.attach(new BarObserver());

    return 0;
}

我唯一关心的是是否违反了设计原则,例如开放封闭原则或类似原则,而且如果我需要添加新通知,则需要在所有其他类中实现此通知(这很痛苦-想象一下10个或更多观察者)。

我考虑过不同的方法,只创建一个接口,然后继承它创建其他通知,但有一个问题是观察者如何确定每种不同类型的通知是什么?

示例:

#include <iostream>
#include <vector>

class Notifier
{
public:
    Notifier() {}
    ~Notifier() {}

    virtual int getInt() const = 0;
};

class FooNotifier
{
public:
    FooNotifier() {}
    ~FooNotifier() {}

    int getInt() const
    {
        return 10;
    }
};

class BarNotifier
{
public:
    BarNotifier() {}
    ~BarNotifier() {}

    int getInt() const
    {
        return 50;
    }
};

class Observer : public ObserverEvents
{
public:
    Observer();
    ~Observer();

    virtual receive(Notifier *) = 0;
};

class Subject : public ObserverEvents
{
public:
    Subject() {}

    ~Subject()
    {
        for (int i = 0; i < observers.size(); ++i)
        {
            delete observers[i];
        }
    }

    void attach(Observer * observer)
    {
        observers.push_back(observer);
    }

    virtual notify(Notifier * notification)
    {
        for (int i = 0; i < observers.size(); ++i)
        {
            observers[i].receive(notification);
        }
    }
private:
    std::vector<Observer *> observers;
};

class FooObserver : public Observer
{
public:
    BarObserver() {}
    ~BarObserver() {}

    receive(Notifier * notification)
    {
        // ...
    }
};

class BarObserver : public Observer
{
public:
    BizObserver() {}
    ~BizObserver() {}

    receive(Notifier * notification)
    {
        // ...
    }
};

int main(int argc, char const * argv[])
{
    Subject subject;
    subject.attach(new FooObserver());
    subject.attach(new BarObserver());

    subject.notify(new FooNotifier());
    subject.notify(new BarNotifier());

    return 0;
}

这个实现只是一个例子,我知道我可以使用智能指针、删除原始指针并做得更好,但它只是一种实现示例。

这种新方法的问题在于,我需要知道Notifier的API才能在Observer内部使用它,调用getInt

我该怎么做,什么是最好的方式?如何向观察者发送不同的通知?


看起来你选择了Java。Java之所以这样做,是因为语言受到限制,而不是因为它很好。我们有指针、lambda和functor。 - Deduplicator
其实我不知道,我以前从未使用过Java。 - yayuj
3个回答

3
template<class C> class event {
    C list;
public:
    template<class... ARGS> inline void add(ARGS&&... args)
    {list.emplace_back(std::forward<ARGS>(args)...);}
    template<class... ARGS> inline void notifyAll(ARGS&&... args)
    {for(auto&& x : list) x(args...);}
};

一个仅支持添加监听器并调用所有监听器的事件小模板。
实例化方式为:
event<std::vector<void(*)()>> a;
event<std::vector<std::function<void()>>> a;

或者使用其他的可调用容器。
一个有用的改变是使用一个包含std::tuple<tag, callable>的容器,添加时必须给出标签,并且可以用于移除监听器。
template<class F, class T=void*> class event {
    std::vector<std::pair<T, F>> v;
public:
    template<class... ARGS> inline void add(T tag, ARGS&&... args)
    {v.emplace_back(std::piecewise_construct, std::make_tuple(tag),
         std::forward_as_tuple(std::forward<ARGS>(args)...));}
    template<class... ARGS> inline void notifyAll(ARGS&&... args)
    {for(auto&& x : v) x(args...);}
    void remove(T tag) {v.erase(std::remove_if(v.begin(), v.end(),
        [=](const std::pair<T, F>& x){return x.first()==tag;}), v.end());}
};

后者是使用函数对象类型实例化的,例如std :: function<...>或函数指针。

你的代码在某些情况下可以使用,但我认为它还没有用处。如果你理解我上面说的话,我希望主题能够接收不同类型的通知。想象一下这种情况:一个名为“动物”的通知,我可以继承并创建“猫”和“动物”。在动物中,有三种可能的情况:死亡、存活和生病(只是一个例子)。我想做的是:当观察者/监听器接收到其中一种通知时,知道动物是死亡、存活还是生病。- 正如我上面所说,有两种方法,第一种是为每个状态创建一个方法... - yayuj
你可以决定每个事件传递哪些参数,当然你也可以有多个事件。哪种方式适合你的情况,这取决于你自己。此外,在通知的上下文中,“继承”并没有真正意义上的含义。它会意味着什么? - Deduplicator
我知道,但是为每种情况创建一个方法,比如notifyCatDeadnotifyCatSick,以及为每个新状态创建一个方法,这样做会有些痛苦,因为我需要在每个主题和观察者中实现。使用一个保存该状态的对象,并且监听器/观察者尝试了解该对象的状态似乎不那么痛苦,但也似乎违反了规则,因为要了解对象的状态,我需要使用switch或if。 - yayuj
更进一步,考虑使用 notifyState(enum state) 这样的命名方式如何? - Deduplicator
但正如我所说,通知具有状态,并且可以具有方法,观察者/侦听器也可以使用它们。我认为我找到了一些有用的东西,但我仍然不知道是否是最好的方法:使用dynamic_cast并比较通知的类型是否是观察者/侦听器所需的类型。例如:我有一个名为CatDiedListener的侦听器,在receive(AnimalEvent * notification)中,我可以使用if(notification = dynamic_cast <CatDiedEvent *>(notification))并知道基类是否是我想要的那个。- 你对这种方法有什么看法? - yayuj
显示剩余4条评论

2

1

听众需要基本了解他们将被调用的签名。打包消息集合很有用,但只有在某些情况下才值得这样做。考虑这个接口:

template<class...Args>
struct broadcaster {
  template<class F> // F:Args...->vpid
  std::shared_ptr<void> attach( F&& f );
  size_t operator()(Args... args)const;
};

现在,听众只需要将一个可调用对象传递给广播器,然后在他们想要收听的时候存储一个共享指针。当共享指针过期时,消息停止发送。
发送方使用参数调用广播器。
template<class...Args>
struct broadcaster {
  template<class F> // F:Args...->vpid
  std::shared_ptr<void> attach( F&& f ){
    auto x = std::make_shared<std::function<void(Args...)>>(std::forward<F>(f)));
    funcs.push_back(x);
    return x;
  }
  size_t operator()(Args... args)const{
    funcs.erase(
      std::remove_if(begin(funcs),end(funcs),
        [](auto&&f){return f.expired();}
      )
      ,end(funcs)
    );
    aito fs=funcs;
    for(auto&&f:fs){
      if(auto f_=f.lock())
        f_(args...);
    }
  }
public:
  mutable std::vector<std::weak_ptr<std::function<void(Args...)>>> funcs;
};

这个术语有点拗口。

它意味着你的两种方法是解耦的。但它们可以通过一个简单的lambda连接起来。在监听类中存储一个std::vector<std::shared_ptr<void>>以处理生命周期管理问题。


两点:1. 为什么要坚持创建一个新的shared_ptr?最好允许传递一个已经具有所需生命周期的指针。2. 在调用时没有必要对监听器进行两次迭代,一次就足够了。这也允许您询问监听器是否应再次被调用,如果需要的话。 - Deduplicator
@Yakk - 请阅读此内容:https://dev59.com/h4Xca4cB1Zd3GeqPCgcY#3UAPoYgBc1ULPQZFznWE - yayuj
@dedup 上述内容允许在广播时添加监听器而不会出现顺序问题(或对列表进行其他更改)。在多线程代码中,您需要在迭代监听器的副本之前释放锁定。无论如何,该列表可能很短(10个或100个而不是1000个或百万个)。至于预先存在的智能指针,稍作修改即可实现。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接