Home > Back-end >  How can I minimize both boilerplate and coupling in object construction?
How can I minimize both boilerplate and coupling in object construction?

Time:03-17

I have a C 20 program where the configuration is passed externally via JSON. According to the “Clean Architecture” I would like to transfer the information into a self-defined structure as soon as possible. The usage of JSON is only to be apparent in the “outer ring” and not spread through my whole program. So I want my own Config struct. But I am not sure how to write the constructor in a way that is safe against missing initializations, avoids redundancy and also separates the external library from my core entities.

One way of separation would be to define the structure without a constructor:

struct Config {
  bool flag;
  int number;
};

And then in a different file I can write a factory function that depends on the JSON library.

Config make_config(json const &json_config) {
  return {.flag = json_config["flag"], .number = json_config["number"]};
}

This is somewhat safe to write, because one can directly see how the struct field names correspond to the JSON field. Also I don't have so much redundancy. But I don't really notice if fields are not initialized.

Another way would be to have a an explicit constructor. Clang-tidy would warn me if I forget to initialize a field:

struct Config {
  Config(bool const flag, int const number) : flag(flag), number(number) {}

  bool flag;
  int number;
};

And then the factory would use the constructor:

Config make_config(json const &json_config) {
  return Config(json_config["flag"], json_config["number"]);
}

I just have to specify the name of the field five times now. And in the factory function the correspondence is not clearly visible. Surely the IDE will show the parameter hints, but it feel brittle.

A really compact way of writing it would be to have a constructor that takes JSON, like this:

struct Config {
  Config(json const &json_config)
      : flag(json_config["flag"]), number(json_config["number"]) {}

  bool flag;
  int number;
};

That is really short, would warn me about uninitialized fields, the correspondence between fields and JSON is directly visible. But I need to import the JSON header in my Config.h file, which I really dislike. It also means that I need to recompile everything that uses the Config class if I should change the way that the configuration is loaded.

Surely C is a language where a lot of boilerplate code is needed. And in theory I like the second variant the best. It is the most encapsulated, the most separated one. But it is the worst to write and maintain. Given that in the realistic code the number of fields is significantly larger, I would sacrifice compilation time for less redundancy and more maintainability.

Is there some alternative way to organize this, or is the most separated variant also the one with the most boilerplate code?

CodePudding user response:

I'd go with the constructor approach, however:

// header, possibly config.h

// only pre-declare!
class json;

struct Config
{
    Config(json const& json_config); // only declare!

    bool flag;
    int number;
};

// now have a separate source file config.cpp:

#include "config.h"
#include <json.h>

Config::Config(json const& json_config)
    : flag(json_config["flag"]), number(json_config["number"])
{ }

Clean approach and you avoid indirect inclusions of the json header. Sure, the constructor is duplicated as declaration and definition, but that's the usual C way.

  • Related