Consider the following relation between classes:
int main(int, char**) { | class Window { | class Layout { | class Box {
/* Use argc/argv */ | Layout layout; | Box box; | int height,
Window window; | | | max_width;
} | bool print_fps; | public: |
| | Layout(); | public:
| public: | }; | Box (int,int);
| Window (); | | };
| }; | |
I made up this structure just for simplicity, in reality there are many more classes.
In main()
I fetch some application parameters (via configuration files, database, CLI arguments). Now I want to deliver those values to the desired objects.
My question: Which is the best/most elegant way to "break the wall" between the classes so that I can "throw" the configuration and whoever needs it to "grab" it?
Initialy I "opened some doors" and gave the Window
constructor everything that was needed by Window
, Layout
and Box
. Then, Window
gave to Layout
everything needed by Layout
and Box
. And so on.
I quickly realized this was very similar to what Dependency Injection it about but as it turns out, it does not apply directly to my case.
Here I work with primitives like bool
and int
and in fact if I accept them as constructor parameters, I get the result described just above - a very long chain of similar calls: Window(box_height, box_max_width, window_print_fps)
.
What if I'd like to change the type of Box::height
to long
? I would need to walk through every pair of header/source of every class in the chain to change it.
If I want my classes to be isolated (and I do) then Window shouldn't worry about Box and main shouldn't worry about Layout.
Then, my second idea came up: create some JSON-like structure which acts as a config object. Everybody gets a (shared) pointer to it and whenever they want, they say this->config["box"]["height"]
- everyone's happy.
This would kinda work, but there are two problems here: no type safety
and tight coupling between a class (Config
) and the entire code base.
Basically, I see two ways around the problem:
- "Downwards": Out -> In
Objects on the top (outer) care about object in the deep (inner). They push down explicitly what inners want. - "Upwards": In <- Out (the "diagram" is the same, but please wait)
Objects on the bottom (inner) care about themselves on their own. They satisfy their needs by reaching out to some container on the top(outer) and pull what they want.
It's either up or down - I'm trying to think out of the box (In fact, it is a line - just ↑ or ↓) but I ended up only here.
Another issue arising from my two ideas earlier is about the way configuration is parsed:
- If main.cpp (or the config parser) needs to give
int height
toBox
, then it needs to know about box in order to parse the value properly, right? (tight-coupling) If, on the other hand, main.cpp doesn't know about Box (ideally), how should it store the value in a friendly for the box way?
Optional parameters shouldn't be needed in constructors => shouldn't break the application. That is, main should accept the absence of some parameter, but it also needs to know that a setter has to be called for the desired object after it's been constructed with the required parameters.
The whole idea is to strive to these three principles:
- Type safety. Provided by solution 1. but not 2.
- Loose coupling. Provided neither by 1. (main cares about Box) nor by 2. (everybody needs Config)
- Avoid duplication. Provided by 2 but not 1. (many identical parameters forwarded until they reach their target)
I implemented a sub-optimal solution I'll post as a self-answer, which works well for now and it's better than nothing but I'm looking forward for something better!