2

I have been trying to come up with a method to "serialize" various objects into various different formats. For example:

class Shape {
public:
    virtual std::string_view name() const = 0;
    virtual double area() const = 0;
};

class Square : public Shape {
...
};

class Triangle: public Shape {
...
};

Suppose I have the two types of shapes above. Now, I want to be able to serialize (and eventually deserialize) these different class into different formats (e.g., string, JSON, bytes, ...)

The first solution is to perform the serialization and deserialization within the class itself (this is what overriding the insertion operator does). However, if I start adding different types of serialization, I have to modify every single Shape class.

class Shape {
public:
    ...
    virtual std::string serializeToString() const = 0;
    virtual json_object serializeToJSON() const = 0;
    //Repeat for every type of serialized output...
};

The second solution I found was to use the visitor pattern. Using that pattern, I can create a different visitor for each type of serialized format. And, since I have much less serialized formats than visitors, I suppose it is acceptable that you have to modify every visitor class when a new Shape class is added.

class Shape {
public:
    virtual void accept(ShapeVisitor& v) = 0;
};

class Square : public Shape;
class Triangle : public Square;

class ShapeVisitor {
public:
    virtual void visit(const Square& s) = 0;
    virtual void visit(const Triangle& s) = 0;
};

class StringShapeVisitor : public ShapeVisitor {
public:
    void visit(const Square& s) const;
    void visit(const Triangle& s) const;
};

But of course, the problem with the visitor pattern is the visitors have no way to access the private data of each class. And since this is serialization I am talking about, I have to access every single private data member which I cannot see how to do without breaking encapsulation of the shapes completely.

A third option I thought of is just using some form of templates and template specialization to choose the correct function for serialization based upon the format and class. The problem is, this doesn't work at runtime on a generic Shape instance...

So my questions are:

  • Is there a modification to the visitor pattern which overcomes the private data problem?
  • Is there an alternative to the visitor pattern that would ideally not require updating a the same classes over and over?
  • Whatever method I use, is there ay way to make the process as "reversible" as possible (e.g, for deserialization)?
4
  • Did you check whether adding the visiting class as a friend gets you access to the private methods? Sep 24, 2021 at 20:37
  • You could have a look at how boost does it (boost.org/doc/libs/1_76_0/libs/serialization/doc/index.html), but in the end, they also use friend to get access to private members.
    – Bert
    Sep 24, 2021 at 21:01
  • @BobDalgleish If you add the visitor as a friend, you end up with the same problem of having to make every visitor a friend of every class; thus you have to modify every class. In C++, you cannot just make a parent class friend and have all the inherited classes inherit the friendship as well. Sep 24, 2021 at 21:02
  • If you are willing to have one virtual serialization method in Shape (for example the JSON one), you could use that to serialize into different formats through a converter class. This would remove the need to modify your Shape classes whenever you add a new serialization format. Instead, you'd add a new converter. This would work for serialization and deserialization the same.
    – Bert
    Sep 24, 2021 at 21:10

2 Answers 2

2

What you need is an intermediary unified representation. The problem now is that your serialization procedures need to understand the details/semantics of the various shape types. Instead, what you could do is provide the shapes with the ability to return a self-describing unified representation of some sort, that the serialization code can just treat as generic structured data, without needing to understand what the data means in the context of a specific shape.

Depending on what you're doing and on what exactly the data that's associated with the shapes is, you might come up with different schemes for this intermediary representation. E.g., it could just be some metadata followed by a list of key-value pairs ({ "width": 1.0, "height": 1.0 }, or perhaps you'd treat shapes as polygons and use a list of vertices and edges. Understand your goals and constraints and try to come up with some scheme that's suitable for what you're doing. Note that, for the shape polymorphism to be useful, there should be parts of your application that are able to work entirely through the abstract Shape interface, never requiring to know any details of the concrete shapes. If there are aspects of the application (other than serialization) for which this doesn't quite work, perhaps you can make use of this unified representation there too - if you design it well.

You'd then create various shape serializers (which may or may not form a hierarchy) that take the unified representation and output different formats. For deserialization, reconstitute the unified representation and pass it along to the shape, or a factory associated with the shape, or a shape prototype. A factory would have to be aware of different Shape subtypes, but this knowledge would be confined there (in a single place).

So, something like

class Shape {
public:
    virtual UnifiedRepr toUnifiedRepr() const = 0;
    ...
};

class Square : public Shape {
...
};

class Triangle: public Shape {
...
};

// ----------------
// Elsewhere:  
void JSONSerializer::serialize(const Shape& shape) {
    UnifiedRepr inputData = shape.toUnifiedRepr();
    // encode as JSON
    // ...
}

1

A good start is to avoid reinventing the wheel and look at Boost serialization library. Even if you can't use the library, its design is worth studying.

Your first solution has a drawback: you foresee as many functions as there are formats. Your class therefore suffers from a lack of separation of concerns, has more than one reason to change (SRP), and moreover is not really open to extension since each new format requires a modification.

A better variant of this approach solves this with a serialization object (archive/json file/stream/xml string/....): the serialisation functions in the serialised objects performs the primitive serialization operations with this object. You could use a template approach but you could also design a serialisation classes with format specializations.

In the second solution, the visitor is another kind of intermediary. But instead of the serialized object telling serialization primitives, it's up to the visitor to ask for the data and do the saving. This is due to the fact that the visitor is meant for a large spectrum of algorithms. Moreover, you have the privacy issue. You could of course make the visitor friend, but the serialisation visitor would completely rely on the internals of another object, which is contrary to the law of demeter.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.