Object-oriented – Parameter management in OOP application

cdesign-patternsobject-oriented

I'm writing a medium size OOP application in C++ as a way to practice OOP principles.

I have several classes in my project, and some of them need to access run-time configuration parameters. These parameters are read from several sources during application start-up. Some are read from a config file in the users home-dir, some are command line arguments (argv).

So I created a class ConfigBlock. This class reads all the parameter sources and stores it in an appropriate data structure. Examples are path- and filenames which can be changed by user in the config file, or the –verbose CLI flag. Then, one can call ConfigBlock.GetVerboseLevel() in order to read this specific parameter.

My question: Is it good practice to collect all such runtime config data in one class?

Then, my classes need access to all these parameters. I can think of several ways to achieve this, but I'm not sure which one to take. A class' constructor can be a given a reference to my ConfigBlock, like

public:
    MyGreatClass(ConfigBlock &config);

Or they just include a header "CodingBlock.h" which contains a definition of my CodingBlock:

extern CodingBlock MyCodingBlock;

Then, only the classes .cpp file needs to include and use the ConfigBlock stuff.
The .h file does not introduce this interface to the user of the class.
However, the interface to ConfigBlock is still there, however, it's hidden from the .h file.

Is it good to hide it this way?

I want the interface to be as small as possible, but in the end, I guess every class which needs config parameters has to have a connection to my ConfigBlock. But, what should this connection look like?

Best Answer

I'm quite the pragmatist, but my main concern here is that you might be allowing this ConfigBlock to dominate your interface designs in a possibly bad way. When you have something like this:

explicit MyGreatClass(const ConfigBlock& config);

... a more appropriate interface might be like this:

MyGreatClass(int foo, float bar, const string& baz);

... as opposed to just cherry-picking these foo/bar/baz fields out of a massive ConfigBlock.

Lazy Interface Design

On the plus side, this kind of design makes it easy to design a stable interface for your constructor, e.g., since if you end up needing something new, you can just load that into a ConfigBlock (possibly without any code changes) and then cherry-pick whatever new stuff you need without any kind of interface change, only a change to the implementation of MyGreatClass.

So it's both kind of a pro and con that this frees you of designing a more carefully thought-out interface that only accepts inputs it actually needs. It applies the mindset of, "Just give me this massive blob of data, I'll pick out what I need from it" as opposed to something more like, "These precise parameters are what this interface needs to work."

So there are definitely some pros here, but they might be heavily outweighed by the cons.

Coupling

In this scenario, all such classes being constructed from a ConfigBlock instance end up having their dependencies look like this:

enter image description here

This can become a PITA, for example, if you want to unit test Class2 in this diagram in isolation. You might have to superficially simulate various ConfigBlock inputs containing the relevant fields Class2 is interested in to be able to test it under a variety of conditions.

In any kind of new context (whether unit test or whole new project), any such classes can end up becoming more of a burden to (re)use, as we end up having to always bring ConfigBlock along for the ride, and setting it up accordingly.

Reusability/Deployability/Testability

Instead if you design these interfaces appropriately, we can decouple them from ConfigBlock and end up with something like this:

enter image description here

If you notice in this above diagram, all the classes become independent (their afferent/outgoing couplings reduce by 1).

This leads to a lot more independent classes (at least independent of ConfigBlock) which can be a lot easier to (re)use/test in new scenarios/projects.

Now this Client code ends up being the one that has to depend on everything and assemble it all together. The burden ends up being transferred to this client code to read the appropriate fields from a ConfigBlock and pass them into the appropriate classes as parameters. Yet such client code is generally narrowly-designed for a specific context, and its potential for reuse is typically going to be zilch or close anyway (it might be your application's main entry point function or something like that).

So from a reusability and testing standpoint, it can help to make these classes more independent. From an interface standpoint for those using your classes, it can also help to explicitly state what parameters they need instead of just one massive ConfigBlock which models the whole universe of data fields required for everything.

Conclusion

In general, this kind of class-oriented design which depends on a monolith which has everything needed tends to have these kinds of characteristics. Their applicability, deployability, reusability, testability, etc. can get significantly degraded as a result. Yet they can kind of simplify the interface design if we attempt a positive spin on it. It's up to you to measure those pros and cons and decide whether the trade-offs are worth it. Typically it's much safer to err against this kind of design where you're cherry-picking from a monolith in classes that are generally intended to model a more general and widely-applicable design.

Last but not least:

extern CodingBlock MyCodingBlock;

... this is potentially even worse (more skewed?) in terms of the characteristics described above than the dependency injection approach, as it ends up coupling your classes not only to ConfigBlocks, but directly to a specific instance of it. That further degrades applicability/deployability/testability.

My general advice would to err on the side of designing interfaces which don't depend on these kinds of monoliths to provide their parameters, at least for the most generally-applicable classes you design. And avoid the global approach without dependency injection if you can unless you really have a very strong and confident reason not to avoid it.