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:... a more appropriate interface might be like this:
... as opposed to just cherry-picking these
foo/bar/baz
fields out of a massiveConfigBlock
.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 ofMyGreatClass
.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: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 variousConfigBlock
inputs containing the relevant fieldsClass2
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: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 aConfigBlock
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'smain
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:
... 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.