Dependency injection is a powerful pattern when used well, but all too often its practitioners become dependent on some external framework. The resulting API is quite painful for those of us who don't want to loosely tie our app together with XML duct tape. Don't forget about Plain Old Objects (POO).
First consider how you would expect someone to use your API--without the framework involved.
- How do you instantiate the Server?
- How do you extend the Server?
- What do you want to expose in the external API?
- What do you want to hide in the external API?
I like the idea of providing default implementations for your components, and the fact that you have those components. The following approach is a perfectly valid, and decent OO practice:
public Server()
: this(new HttpListener(80), new HttpResponder())
{}
public Server(Listener listener, Responder responder)
{
// ...
}
As long as you document what the default implementations for the components are in the API docs, and provide one constructor that performs all the setup, you should be fine. The cascading constructors are not fragile as long as the set up code happens in one master constructor.
Essentially, all the publicly facing constructors would have the different combinations of components you want to expose. Internally, they would populate the defaults and defer to a private constructor that completes the setup. In essence, this provides you some DRY and is pretty easy to test.
If you need to provide friend
access to your tests to set up the Server object to isolate it for testing, go for it. It won't be part of the public API, which is what you want to be careful with.
Just be kind to your API consumers and don't require a IoC/DI framework to use your library. To get a feel for the pain you are inflicting make sure your unit tests don't rely on the IoC/DI framework either. (It's a personal pet peeve, but as soon as you introduce the framework it's no longer unit testing--it becomes integration testing).
This is a good approach to get you started. Note that the most important is to cover your existing code with unit tests; once you have the tests, you can refactor more freely to improve your design further.
So the initial point is not to make the design elegant and shiny - just to make the code unit testable with the least risky changes. Without unit tests, you have to be extra conservative and cautious to avoid breaking your code. These initial changes may even make the code look more clumsy or ugly in some cases - but if they enable you to write the first unit tests, eventually you will be able to refactor towards the ideal design you have in mind.
The fundamental work to read on this subject is Working Effectively with Legacy Code. It describes the trick you show above and many, many more, using several languages.
Best Answer
I'd like to suggest a clarification of some of the terminology you are using here, specifically "dependency" and "dependency injection".
Dependency:
A "dependency" is typically a complex object that performs some functionality that another class may need to depend on. Some classic examples would be a logger or a database accessor or some component that processes a particular piece of business logic.
A data only object like a DTO or a value object are not typically referred to as a "dependency", since they don't perform some needed function.
Once you look at it this way, what you are doing in your example (composing the
DO
object with a list ofD02
objects through the constructor) should not be considered "dependecy injection" at all. It's just setting a property. It's up to you whether you provide it in the constructor or some other way, but simply passing it in through the constructor does not make it dependency injection.Dependency Injection:
If your
DO2
class were actually providing some additional functionality that theDO
class needs, then it would truly be a dependency. In that case, the dependent class,DO
, should depend on an interface (like ILogger or IDataAccessor), and in turn rely on the calling code to provide that interface (in other words, to 'inject' it into theDO
instance).Injecting the dependency in such a way makes the
DO
object more flexible, since each different context can provide its own implementation of the interface to theDO
object. (Think unit testing.)