There are some ideas to get rid of these ugly chain dependencies. Think of three specific classes A, B, C where A needs B and B needs C, so implicit A needs C. This is a very poor software design. Testing is more complicated since
- If you need integration testing, you need these three objects.
- if you want to test Isolated, then you need a layout for B and disable ctor, or you also need to mock C
At this point you should focus on your design. To separate A from C, you must create the interface on which BI A depends, and B implements.
From:
A -> B B -> C
now we have:
A -> BI B impl BI B -> C
We can completely separate them from another CI interface:
A -> BI B impl BI B -> CI C impl CI
Now our concrete class is untied, but if we connect them as we separated them (for example, through DI), there is no profit. So the next step is to compress the interfaces, for example, class A depends only on BI, which has only the methods that A needs.
Say this is done, and we need class A2, which needs some methods of B. You find that in fact BI would be good for A2, since BI has some methods of A2 (now we leave C and CI):
A -> BI A2 -> BI B -> BI
In a few days, you will find out that our BI interface is actually too large for A, and you want to reorganize BI so that as little BI as possible. But you cannot reorganize it, because A2 uses some methods that you want to remove.
It could be a bad design. Now we can do this (principle of interface separation):
A -> BI A2 -> BI2 B impl BI B impl BI2
In this case, we can now change the interfaces ourselves. No matter how many classes your project uses, I always recommend some type of creation template for part of your application to get (referring to the first examples) an object from an unknown implementation class that implements the CI interface.
Now this is the real OOP. Today you may have some kind of reworked chain of dependencies for getting CI, but overnight you dream of an innovative idea on how to get CI and change it the way you don’t need to change the code that uses CI! It is important.
I always say: it's all about hiding the implementation (in the right places, of course).
And now don’t start getting every class of the interface, but let some classes interact, and then reorganize them where you see the need.
For your classes, I think Settings is the destination monster / setter. It is not recommended to include classes for which only some settings are required, depending on the settings. This is better than I think:
Settings -> DB Settings impl PageSettings Settings impl CustomerSettings Settings impl ProductSettings Page -> PageSettings Customer -> CustomerSettings Product -> ProductSettings
I do not know how you use your language class, but I hope you now have an idea for software development. Of course there are more.