Search code examples
c++templatesc++11observer-patterncrtp

Implementing the observer pattern using CRTP and 'anonymous types/template'


I'm developing a library that parses a certain XML file (using RapidXML) and returns an object of mine, containing that file data. That XML file is created by someone else's application. I needed to use the observer pattern because speed is extremely crucial, for example:

Suppose a file has 10.000 tag and its child nodes. In a simple parser, they would be added to a std::vector in the order they were found. Then, after the file was parsed, we would need to iterate over the 10.000 values AGAIN and do whatever we want with them.

By using the observer pattern, I allow external observers (whichever class that wants to observe and be notified about each fetched node has to inherit from AbstractObserver that comes with my library, and implement his functions) to be a part of the parsing process without the need to iterate X times again over the parsed nodes. HOWEVER... There are multiple kinds of nodes, for example: tileset, layer, imagelayer and so on (being necessary multiple onObserved and notify functions for its corresponding node, according to the Observer/Subject pattern, probably having a lot of 'duplicated' code - NOTE: inheritance is not used. See bellow a 'bad' example). I could simply make the nodes inherit from some sort of BaseNode class, but I dont want to use inheritance here since I dont want to deal with pointers. Instead, I'm using enums to type the nodes and thats where my problem lies.

/* ################## 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);
    }
};

As we can see, I'm using the CRTP as well, since I need 'templated virtual functions', which is impossible to achieve otherwise. Since the AbstractObserver needs a type (because of the CRTP), I can't properly use them in the AbstractSubject class (see above). Is it even possible to use annonymous templates just like Java, or something like that? I believe this WOULD do the job.

Here is the implementation of a 'bad' example I thought of, but this is the best I could come up with for this situation:

// 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;
};

The solution has to use C++11 or bellow and no boost.


Solution

  • Solution #1: Quite wet but simple

    #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;
    }
    

    You would need to use composition here and have a subject for each type of node. However, this is compile-time validated so I'd prefer this to the second version.

    Solution #2: Dynamic and closer to what you want

    #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;
    }
    

    This is closer to your version. Quite unsafe and slower but slightly less typing.

    Summary

    Both output a\na\nb and both are something sewn together just as a minimal proof of concept, not something you should follow (especially the working with unordered_map feels quite nasty).

    It's not directly what you want but I guess that you can take it from there...

    I have strong feeling that there are better solutions to this so feel free to experiment.

    EDIT:

    Solution #3: Completely dynamic

    #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);
        }
    };
    

    This is basically a ported C#'s version of Dictionary<Type, Object>, it uses rtti so you might get spat on by C++ hardliners...