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:
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:
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.