Serialization and SOLID principles
https://softwareengineering.stackexchange.com/questions/421682
-
21-03-2021 - |
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:
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?One thing that drives me towards making
serialize
/deserialize
member functions is that I would need to use template specializations to makedeserialize
function free function for each configuration structure.If I have
config::App
configuration structure, but I also have mainApp
class, would naming configuration structureconfig::AppConfig
make more sense, even thoughconfig
part repeats?
Thank you very much
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()
andserialize()
only need to change ifFoo
changes, so if configuration information chances, assuming they useJSON::Object
's public interface and do not rely on its internals. - In the
serialization::JSON
namespace,serialize()
andserialize()
do not need to change due to change inconfig
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.