Question

I have several configuration structures that can be JSON serialized and deserialized as follows:

namespace config
{

struct Foo
{
    std::string   hostname  {};
    std::string   ip_address{};
    std::uint32_t port      {};
};

JSON::Object serialize( config::Foo const& foo )
{
    // ...
}

config::Foo deserialize( JSON::Object const& object )
{
    // ...
}

} // namespace config

and in the serialization::JSON namespace I have main serialize and deserialize functions that call specific config serialize and deserialize functions:

template <typename T>
Object serialize( T const& obj, Allocator& allocator )
{
    return serialize( obj, allocator );
}

// ...

Questions:

  1. Is this a good design that follows SOLID principles (specifically Single Responsibility Principle)? Would making serialize/deserialize member functions of configuration structures break SRP or any of other SOLID principles? What in case adding XML serialization/deserialization?

  2. One thing that drives me towards making serialize/deserialize member functions is that I would need to use template specializations to make deserialize function free function for each configuration structure.

  3. If I have config::App configuration structure, but I also have main App class, would naming configuration structure config::AppConfig make more sense, even though config part repeats?

Thank you very much

Was it helpful?

Solution

In short

You are right, there is a problem in this design. But it's about dependency inversion and not single responsibility.

More details

SRP is about having a one and only one reason to change. Changes in the hidden implementation details of another module do not matter, as long as its public interface stays stable:

  • config::Foo only needs to change if the configuration information changes.
  • In the config namespace, serialize() and serialize() only need to change if Foo changes, so if configuration information chances, assuming they use JSON::Object's public interface and do not rely on its internals.
  • In the serialization::JSON namespace, serialize() and serialize() do not need to change due to change in config thanks to templates.

Therefore, there seem to be no issue in view of a single reason to change.

The real problem in your design is that you have a concrete implementation of serialization based on JSON::Object, and you build the rest on this fundament. This is why introducing a second way of serialization (e.g. XML in addition of JSON) causes a trouble.

This dependency to a concrete implementation is both undesirable and avoidable. You therefore should consider the dependency inversion principle:

  • Define an abstract interface for a serialization object/service.
  • Then rewrite your config to depend on this abstraction.
  • And then find some way to inject the serialization service you want when you want it.

This could be at runtime using a strategy pattern (which would include a factory to instantiate the serialization objects). In C++, this could be at compile-time, using the metaprogramming equivalent to the strategy patter, the policy based design. By the way, the boost:serialization could be an interesting source of inspiration if you're looking for an example of good serialization design.

Licensed under: CC-BY-SA with attribution
scroll top