【问题标题】:Implementing the observer pattern using CRTP and 'anonymous types/template'使用 CRTP 和“匿名类型/模板”实现观察者模式
【发布时间】:2015-10-24 17:11:26
【问题描述】:

我正在开发一个解析某个 XML 文件(使用 RapidXML)并返回包含该文件数据的我的对象的库。该 XML 文件是由其他人的应用程序创建的。我需要使用观察者模式,因为速度至关重要,例如:

假设一个文件有 10.000 个标签及其子节点。在一个简单的解析器中,它们会按照它们被发现的顺序被添加到 std::vector 中。然后,在文件被解析后,我们需要再次遍历这 10.000 个值,然后对它们做任何我们想做的事情。

通过使用观察者模式,我允许外部观察者(任何想要观察并被通知每个获取的节点的类都必须从我的库附带的 AbstractObserver 继承,并实现他的功能)成为解析的一部分无需在解析的节点上再次迭代 X 次。但是......有多种节点,例如:tileset,layer,imagelayer等(根据Observer/Subject模式,其对应节点需要多个onObserved和notify函数,可能有很多'重复' 代码 - 注意:不使用继承。请参阅下面的“坏”示例)。我可以简单地让节点从某种 BaseNode 类继承,但我不想在这里使用继承,因为我不想处理指针。相反,我使用枚举来键入节点,这就是我的问题所在。

/* ################## ABSTRACT OBSERVER #################### */
// Implements the observer pattern, using the CRTP pattern
template<class ConcreteObserver>
class AbstractObserver
{
public:
    virtual ~AbstractObserver() { }

    template<class Attribute>
    inline void onObserved(Attribute attribute) {
        // This requires ConcreteObserver to have a method onObservedImpl<Attribute>
        static_cast<const ConcreteObserver*>(this)->onObservedImpl(attribute);
    }
};

/* ################## ABSTRACT SUBJECT #################### */
class AbstractSubject
{
public:
    virtual ~AbstractSubject() { }

    // ???????
    inline void attach(AbstractObserver<??????>* observer) {
        m_observers.push_back(observer);
    }

    // ???????
    inline void detatch(AbstractObserver<??????>* observer) { 
        auto& it = std::find(m_observers.begin(), m_observers.end(), observer);

        // Remove the observer from the list, if it was observing
        if (it != m_observers.end())
            m_observers.erase(it);
    }

protected:
    template<typename Attribute>
    void notify(Attribute& attribute) {
        for (auto* observer : m_observers)
            observer->onObserved(attribute)
    }

private:
    // ???????
    std::vector<AbstractObserver<??????>*> m_observers;
};

/* ################## CONCRETE OBSERVER #################### */
class SomeConcreteObserver : public AbstractObserver<SomeConcreteObserver>
{
public:
    // The CRTP 'virtual' function implementation
    template<class Attribute>
    void onObservedImpl(Attribute attribute)
    {
        // Filter the attribute and use it accordingly
        switch (attribute.type)
        {
            // ....
        }
    }
};

/* ################## CONCRETE SUBJECT #################### */
class Parser : public AbstractSubject
{
public:
    void parse(/* params */)
    {
        Foo f;

        notify<Foo>(f);

        // Later on....

        Bar b;

        notify<Bar>(b);
    }
};

正如我们所见,我也在使用 CRTP,因为我需要“模板化虚函数”,否则无法实现。由于 AbstractObserver 需要一个类型(因为 CRTP),我不能在 AbstractSubject 类中正确使用它们(见上文)。甚至可以使用像 Java 或类似的匿名模板吗?我相信这会完成这项工作。

这是我想到的一个“坏”示例的实现,但这是针对这种情况我能想到的最好的例子:

// Remove the CRTP
class AbstractObserver
{
public:
    virtual ~AbstractObserver() { }

    virtual void onNodeA(NodeA n) = 0;
    virtual void onNodeB(NodeB n) = 0;
    virtual void onNodeC(NodeC n) = 0;
    virtual void onNodeD(NodeD n) = 0;
    virtual void onNodeE(NodeE n) = 0;
    // .....
};

class AbstractSubject
{
public:
    // ....

protected:
    void notifyNodeA(NodeA n) { 
        for (auto* observer : m_observers)
                observer->onNodeA(n);
    }
    void notifyNodeB(NodeB n) { 
        for (auto* observer : m_observers)
                observer->NodeB(n);
    }
    void notifyNodeC(NodeC n) { }
    void notifyNodeD(NodeD n) { }
    void notifyNodeE(NodeE n) { }
    // ....

private:
    std::vector<Observer*> m_observers;
};

解决方案必须使用 C++11 或以下版本,并且没有任何提升。

【问题讨论】:

  • 请问您为什么反对“指针”?
  • 恐怕这不是泛型的正确用法——它在 java 中工作只是因为 java 将它全部向上转换为对象然后进行类型检查。如果您坚持的话,您也可以在 C++ 中执行此操作(void* + dynamic_cast)...
  • @user3159253 你好。很复杂,我试着解释一下。有这个xml文件。我的文件类基本上是一堆不同类型的向量(内容是静态的)。会有一些我不知道的外部类会消耗我的文件对象。文件类可以有指针向量,是的,然后在 dtor 上“删除”它们。如果文件有指针并且它们都将在 dtor 上被销毁,那么消费者类也会使指针失效,除非他们在他们的类中保留指向文件的引用/指针,我不“推荐”(这就是为什么文件中没有指针)
  • @Alegnem 我知道。这只是一个简单的例子,说明它是如何工作的。我为此找到了 2 个解决方案,即从基类继承节点并将指针存储在文件类中,或者为每个 xml 节点使用多个 onNotified 和 notifyX 函数(这听起来很错误),尽管我在问是否有这样做的好方法

标签: c++ templates c++11 observer-pattern crtp


【解决方案1】:

解决方案 #1:很潮湿但很简单

#include <vector>
#include <algorithm>
#include <iostream>

template<typename TAttribute>
class Observer
{
public:
    virtual void Observe(TAttribute& attribute) = 0;

    virtual ~Observer() = default;
};

template<typename TAttribute>
class OutputtingObserver : public Observer<TAttribute>
{
public:
    void Observe(TAttribute& attribute)
    {
        std::cout << attribute << std::endl;
    }

    ~OutputtingObserver() = default;
};

template<typename TAttribute>
class Subject
{
private:
    std::vector<Observer<TAttribute>*> mutable m_observers;

public:
    void Attach(Observer<TAttribute>& observer) const
    {
        m_observers.push_back(&observer);
    }

    void Detach(Observer<TAttribute>& observer) const
    {
        m_observers.erase(std::remove(m_observers.begin(), m_observers.end(), &observer), m_observers.end());
    }

    void Notify(TAttribute& attribute)
    {
        for (auto observer : m_observers)
            observer->Observe(attribute);
    }
};

class NodeA
{
public:
    friend std::ostream& operator<<(std::ostream& o, const NodeA& node)
    {
        return o << "a";
    }
};

class NodeB
{
public:
    friend std::ostream& operator<<(std::ostream& o, const NodeB& node)
    {
        return o << "b";
    }
};

class Parser
{
private:
    Subject<NodeA> m_subjectA;
    Subject<NodeB> m_subjectB;

public:
    void Parse()
    {
        auto a = NodeA();
        auto b = NodeB();

        m_subjectA.Notify(a);
        m_subjectB.Notify(b);
    }

    void Attach(Observer<NodeA>& observer)
    {
        m_subjectA.Attach(observer);
    }

    void Attach(Observer<NodeB>& observer)
    {
        m_subjectB.Attach(observer);
    }
};

int main()
{
    auto observerA = OutputtingObserver<NodeA>();
    auto observerB = OutputtingObserver<NodeB>();

    auto parser = Parser();

    parser.Attach(observerA);
    parser.Attach(observerB);
    parser.Attach(observerA);

    parser.Parse();

    return 1;
}

您需要在此处使用组合,并为每种类型的节点设置一个主题。但是,这是经过编译时验证的,所以我更喜欢它而不是第二个版本。

解决方案 #2:动态且更接近您的需求

#include <unordered_map>
#include <vector>
#include <algorithm>
#include <iostream>

class ObserverBase
{
public:
    virtual ~ObserverBase() = default;
};

template<typename TAttribute>
class Observer : public ObserverBase
{
public:
    virtual void Observe(TAttribute& attribute) = 0;
};

template<typename TAttribute>
class OutputtingObserver : public Observer<TAttribute>
{
public:
    void Observe(TAttribute& attribute)
    {
        std::cout << attribute << std::endl;
    }

    ~OutputtingObserver() = default;
};

template<typename TKey>
class Subject
{
private:
    using ObserverList = std::vector<ObserverBase*>;
    using ObserverMap = std::unordered_map<TKey, ObserverList>;

    ObserverMap mutable m_observers;

public:
    void Attach(TKey key, ObserverBase& observer) const
    {
        auto itr = m_observers.find(key);
        if (itr == m_observers.end())
        {
            m_observers.emplace(std::make_pair(key, ObserverList { &observer }));
            return;
        }

        itr->second.push_back(&observer);
    }

    void Detach(ObserverBase& observer) const
    {
        m_observers.erase(std::remove(m_observers.begin(), m_observers.end(), &observer), m_observers.end());
    }

    template<TKey key, typename TAttribute>
    void Notify(TAttribute& attribute)
    {
        auto itr = m_observers.find(key);
        if (itr == m_observers.end())
            return;

        for (auto observer : itr->second)
            dynamic_cast<Observer<TAttribute>*>(observer)->Observe(attribute);
    }
};

enum class NodeType
{
    TypeA,
    TypeB
};

class NodeA
{
public:
    friend std::ostream& operator<<(std::ostream& o, const NodeA& node)
    {
        return o << "a";
    }
};

class NodeB
{
public:
    friend std::ostream& operator<<(std::ostream& o, const NodeB& node)
    {
        return o << "b";
    }
};

class Parser
{
private:
    Subject<NodeType> m_subject;

public:
    void Parse()
    {
        auto a = NodeA();
        auto b = NodeB();

        m_subject.Notify<NodeType::TypeA, NodeA>(a);
        m_subject.Notify<NodeType::TypeB, NodeB>(b);
    }

    void Attach(Observer<NodeA>& observer)
    {
        m_subject.Attach(NodeType::TypeA, observer);
    }

    void Attach(Observer<NodeB>& observer)
    {
        m_subject.Attach(NodeType::TypeB, observer);
    }
};

int main()
{
    auto observerA = OutputtingObserver<NodeA>();
    auto observerB = OutputtingObserver<NodeB>();

    auto parser = Parser();

    parser.Attach(observerA);
    parser.Attach(observerB);
    parser.Attach(observerA);

    parser.Parse();

    return 1;
}

这更接近您的版本。相当不安全且速度较慢,但​​打字略少。

总结

两个输出 a\na\nb 和它们都是缝合在一起的,只是作为一个最小的概念证明,而不是你应该遵循的东西(尤其是使用 unordered_map 感觉很讨厌)。

这不是你想要的,但我想你可以从那里得到它......

我强烈认为有更好的解决方案,所以请随意尝试。

编辑:

解决方案 #3:完全动态

#include <unordered_map>
#include <vector>
#include <algorithm>
#include <iostream>
#include <typeinfo>
#include <typeindex>

template<typename TAttribute>
class Observer
{
public:
    virtual void Observe(TAttribute& attribute) = 0;

    virtual ~Observer() = default;
};

class Subject
{
private:
    using ObserverList = std::vector<void*>;
    using ObserverMap = std::unordered_map<std::type_index, ObserverList>;

    ObserverMap mutable m_observers;

public:
    template<typename TAttribute>
    void Attach(Observer<TAttribute>& observer) const
    {
        auto index = std::type_index(typeid(Observer<TAttribute>));
        auto itr = m_observers.find(index);
        if (itr == m_observers.end())
        {
            m_observers.emplace(std::make_pair(index, ObserverList { &observer }));
            return;
        }

        itr->second.push_back(&observer);
    }

    template<typename TAttribute>
    void Detach(Observer<TAttribute>& observer) const
    {
        m_observers.erase(std::remove(m_observers.begin(), m_observers.end(), &observer), m_observers.end());
    }

    template<typename TAttribute>
    void Notify(TAttribute& attribute)
    {
        auto itr = m_observers.find(std::type_index(typeid(Observer<TAttribute>)));
        if (itr == m_observers.end())
            return;

        for (auto observer : itr->second)
            static_cast<Observer<TAttribute>*>(observer)->Observe(attribute);
    }
};

这基本上是 Dictionary&lt;Type, Object&gt; 的移植 C# 版本,它使用 rtti 所以你可能会被 C++ 强硬派吐槽...

【讨论】:

  • 我很感激。你确实明白我的意思。我真正想要的是让观察者调度 A-N-Y 类型的对象:onNotify(T& t)。有一个非常相似的东西:codeproject.com/Articles/3267/…。下面的例子展示了通知观察者一个主题,但不是不是主题的东西(如果节点类不继承主题,这是我的情况)。如果它有多种类型的主题,我可以简单地拥有一个并使用我上面的“坏”示例。我真的被困住了。还是我迷路了?
  • Continuing... 你认为不是让解析器成为主题,因为另一个对象将等待它(观察)并添加组合吗?!我在上面的 cmets 中提到的指针问题有什么替代方案吗?
  • @YvesHenri 我认为 OOP 中的解析器设计应该是 Parser = factory,Node = 具有工厂返回的继承的核心元素,而您现在拥有的观察者应该是节点访问者......这需要你使用指针(std::unique_ptr 和 std::make_unique 是你的朋友)。除非你想在这里使用 C 风格,否则没有指针就无法逃脱。另一件事是完全动态的观察者,自 C++11 以来就有可能,并且有多种方法可以做到这一点。如果我有时间,我会编辑我的答案......
  • 即使您的回答没有完全回答我的问题,但由于您的努力,我接受了它,并且它确实回答了我的问题。无论如何,我最终使用了继承、引用和老式类型检查来确保安全向下转换。它似乎更直接、干净、美丽和安全(我相信 - 并希望)。 :)
猜你喜欢
  • 2011-12-21
  • 1970-01-01
  • 2015-02-12
  • 2017-01-23
  • 1970-01-01
  • 1970-01-01
  • 2013-10-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多