lundi 27 novembre 2023

Looking for design pattern for flexibile generic serialization

I have a header-only library that provides three class (templates), namely

  • an abstract DAGNode,

    struct DAGNode;
    using NodePtr = std::shared_ptr<DAGNode>;
    
    class DAGNode 
    {
    public:
        virtual ~DAGNode(){}
        virtual std::string serialize() const = 0;
    
        auto const& get_parents() const {
            return m_parents;
        }
     protected:
        std::vector<NodePtr> m_parents;
    };
    
  • non-abstract class template impl::Derived<T>, which is derived from DAGNode and which is not part of the libraries API and

    // this needs to be specialized by consumer
    template <typename T>
    std::string serialize(T const&) {
        throw std::logic_error("Not implemented.");
    }
    
    namespace impl {
    
    // not part of the librarys API
    template <typename T>
    class Derived : public DAGNode 
    {
    public:
        Derived(T const& t) : m_value(t) {}
    
        std::string serialize() const override final
        {
            return ::serialize(m_value);
        }
    
        void add_parent(NodePtr const& ptr) {
            m_parents.push_back(ptr);
        }
    
    private:
        T m_value;
    };
    
    } // namespace impl
    
  • class template DerivedHolder<T>, which stores a std::shared_ptr<impl::Derived<T>>, which is part of the libraries API.

    
    template <typename T>
    class DerivedHolder
    {
    public:
        DerivedHolder(T const& t) : m_node(std::make_shared<impl::Derived<T>>(t)) {}
        NodePtr holder() const 
        {
            return m_node;
        }
    
        void add_parent(NodePtr const& p) {
            m_node->add_parent(p);
        }
    private:
        std::shared_ptr<impl::Derived<T>> m_node;
    };
    

In the current design, DAGNode has a virtual method std::string serialize() const. Derived<T> final overrides this method and delegates to the function template template <typename T> std::string ::serialize.

A consumer of this library will have to specialize the ::serialize function template for whatever types it wants to use. A typical usage scenario is that the consumer will use the visitor pattern to iterate over all DAGNodes and call the virtual serialize method.

Here is a minimal complete code example.

This approach works well, but I have now come across a use-case, where a consumer of this library needs to replace template <typename T> ::serialize with its own "magic implementation" along the lines of:

template <typename T>
std::string magic_serialize(T const& t) 
{
    if constexpr (std::is_same_v<T, MyMagicClass> ) {
        return t.as_string();
    } else {
        MagicClass m(t);
        return m.as_string();
    }
}

Since magic_serialize is in no way more special than ::serialize, specialization won't do the trick.

Therefore, we need to change the design pattern of the serialization framework of the header-only library. We are free to change any part of the library including the API, though I would prefer any minimally invasive solution over a complete rewrite. We are restricted to C++17 for now.

How can I change the library in such a way, that the consumer has complete control over how any type is to be serialized?

Aucun commentaire:

Enregistrer un commentaire