Polymorphic Data Translation/Conversion Design Pattern

803 views Asked by At

The problem is as follows:

consider the following class

class data : public base_data
{
  public:
    int a;
    std::string b;
    double c;
    ... // many other members
};

Assume it makes perfect sense to expose the data members of this class.

Now consider there are many such classes, each with different members, perhaps all deriving from the same base class "base_data".

Now, these classes need to be exported, imported, constructed, 'set' and 'get' from other arbitrary representations of data.

For example:

using any_map = boost::unordered_map < std::string, boost::any > ;

is one such representation.

Additionally, all of these operations need to be done in mass, i.e. polymorphically through a collection of base_data* objects.

One solution to this problem is to provide an interface in base_data as follows

class base_data
{
 public:
   virtual void set(const any_map&) = 0;
   virtual any_map get() const = 0;
};

each derived class knows its members, so it knows how to make the translation. additionally derived classes can provide constructors of the form

data(const any_map&) {...}

To allow easily defining an abstract factory pattern.

Another solution to this problem is to provide static translation functions under some namespace for each derived type, e.g.

static data convert(const any_map&);
static any_map convert(const data&);

So we avoid the pollution of the derived classes at the cost of a "less OO" solution and probably the ability to perform these translation operations in mass.

This also makes a lot more sense if we consider the possibility of needing to support many representations other than any_map, e.g.

using boost::ptree;
using json_class;
using xml_class;

But once again, it is not polymorphic.

Most "translation" design patterns that I have read about deal with interfaces, but I have not found one that formally addresses translation/conversion of data in the context of polymorphism.

I am looking for a reference to a design pattern that formally addresses this problem, advice on how to proceed with the implementation and/or pointing out glaring flaws in my approach.

1

There are 1 answers

2
Tony Delroy On

As requested in comments, there's code below illustrating use of the Visitor Pattern as I'd described. Just add additional Visitors for input, JSON, CSV or whatever formats you require. Note that the visitors don't need to be modified to deal with different record structures - the implementation below just needs to know how to handle the different field types involved via virtual dispatch. All this ends up similar to the boost serialisation library, which I recommend looking at too.

#include <iostream>
#include <string>
#include <vector>
#include <sstream>

struct Visitor
{
    typedef const char* Identifier; // or string...

    Visitor(std::ostream& os) : os_(os) { }

    virtual Visitor& pre(Identifier) { return *this; }
    template <typename T> Visitor& operator()(Identifier id, const T& t)
    {
        std::ostringstream oss;
        oss << t;
        return operator()(id, oss.str());
    }
    virtual Visitor& operator()(Identifier, double) = 0;
    virtual Visitor& operator()(Identifier, const std::string&) = 0;
    virtual Visitor& post() { return *this; }

    std::ostream& os_;
};

struct Visitor__XML_Out : Visitor
{
    using Visitor::Visitor;

    Visitor& pre(Identifier i) override
    { os_ << '<' << i << '>'; i_ = i; return *this; }

    Visitor& operator()(Identifier f, double x) override
    { return out(f, x); }

    Visitor& operator()(Identifier f, const std::string& x) override
    { return out(f, x); }

    Visitor& post() override
    { os_ << "</" << i_ << '>'; return *this; }

  private:
    template <typename T>
    Visitor& out(Identifier f, const T& x)
    {
        os_ << '<' << f << '>' << x << "</" << f << '>';
        return *this;
    }

    Identifier i_;
};

struct Base_Data
{
   virtual void visit(Visitor& v) = 0;
};

struct Data : Base_Data
{
    int a_;
    std::string b_;
    double c_;

    Data(int a, const std::string& b, double c)
      : a_(a), b_(b), c_(c)
    { }

    void visit(Visitor& v) override
    {
        v.pre("Data")("a", a_)("b", b_)("c", c_).post();
    }
};

int main()
{
    Data d { 42, "hawk", 8.8 };
    Visitor__XML_Out xml(std::cout);
    d.visit(xml);
    std::cout << '\n';
}

Output:

<Data><a>42</a><b>hawk</b><c>8.8</c></Data>