Problems Solved By Dependency Injection
If we didn't use dependency injection, the Greeter
class might look more like this:
public class ControlFreakGreeter
{
public void Greet()
{
var greetingProvider = new SqlGreetingProvider(
ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
var greeting = greetingProvider.GetGreeting();
Console.WriteLine(greeting);
}
}
It's a "control freak" because it controls creating the class that provides the greeting, it controls where the SQL connection string comes from, and it controls the output.
Using dependency injection, the Greeter
class relinquishes those responsibilities in favor of a single responsibility, writing a greeting provided to it.
The Dependency Inversion Principle suggests that classes should depend on abstractions (like interfaces) rather than on other concrete classes. Direct dependencies (coupling) between classes can make maintenance progressively difficult. Depending on abstractions can reduce that coupling.
Dependency injection helps us to achieve that dependency inversion because it leads to writing classes that depend on abstractions. The Greeter
class "knows" nothing at all of the implementation details of IGreetingProvider
and IGreetingWriter
. It only knows that the injected dependencies implement those interfaces. That means that changes to the concrete classes that implement IGreetingProvider
and IGreetingWriter
will not affect Greeter
. Neither will replacing them with entirely different implementations. Only changes to the interfaces will. Greeter
is decoupled.
ControlFreakGreeter
is impossible to properly unit test. We want to test one small unit of code, but instead our test would include connecting to SQL and executing a stored procedure. It would also include testing the console output. Because ControlFreakGreeter does so much it's impossible to test in isolation from other classes.
Greeter
is easy to unit test because we can inject mocked implementations of its dependencies that are easier to execute and verify than calling a stored procedure or reading the output of the console. It doesn't require a connection string in app.config.
The concrete implementations of IGreetingProvider
and IGreetingWriter
might become more complex. They, in turn might have their own dependencies which are injected into them. (For example, we'd inject the SQL connection string into SqlGreetingProvider
.) But that complexity is "hidden" from other classes which only depend on the interfaces. That makes it easier to modify one class without a "ripple effect" that requires us to make corresponding changes to other classes.