The primary benefit of DI is not unit testing (although that is certainly a benefit). The primary benefit is loose-coupling. An application that is "testable" is not necessarily loosely-coupled.
However, loose-coupling brings a lot more to the table than just testability.
Loose Coupling Benefits
- Late Binding (services can be swapped with other services, often without changing existing code)
- Extensibility (code can be extended, often without changing existing code)
- Parallel development (abstract contracts are defined that multiple developers can adhere to)
- Maintainability (classes with clearly defined responsibilities are easier to maintain)
- Testability (classes are easier to test).
IMHO, when combining DI with software patterns, extensibility is definitely the main benefit. Consider the following types:
public interface IWriter
{
void WriteSomething();
}
public interface ISomeService
{
void Write();
}
You could extend a service by using a Decorator Pattern:
public class NullWriter : IWriter
{
public void WriteSomething()
{
// Do nothing - this is a "null object pattern".
}
}
public class HelloWriter : IWriter
{
public readonly IWriter innerWriter;
public HelloWriter(IWriter innerWriter)
{
if (innerWriter == null)
throw new ArgumentNullException("innerWriter");
this.innerWriter = innerWriter;
}
public void WriteSomething()
{
this.innerWriter.WriteSomething();
Console.WriteLine("Hello.");
}
}
public class GoodbyeWriter : IWriter
{
public readonly IWriter innerWriter;
public GoodbyeWriter(IWriter innerWriter)
{
if (innerWriter == null)
throw new ArgumentNullException("innerWriter");
this.innerWriter = innerWriter;
}
public void WriteSomething()
{
this.innerWriter.WriteSomething();
Console.WriteLine("Goodbye.");
}
}
public class SomeService : ISomeService
{
private readonly IWriter writer;
public SomeService(IWriter writer)
{
if (writer == null)
throw new ArgumentNullException("writer");
}
public void Write()
{
this.writer.WriteSomething();
}
}
And the above would be wired up like:
// Composition Root
var nullWriter = new NullWriter();
var goodbyeWriter = new GoodbyeWriter(nullWriter);
var helloWriter = new HelloWriter(goodbyeWriter);
var service = new SomeService(helloWriter);
// End Composition Root
// Execute
service.Write();
//Writes:
//Hello.
//Goodbye.
Now, that the scenario is set up, you can extend what SomeService does without altering any of the existing types. The only part of the application that needs to change is the composition root.
public class HowAreYouWriter : IWriter
{
public readonly IWriter innerWriter;
public HowAreYouWriter(IWriter innerWriter)
{
if (innerWriter == null)
throw new ArgumentNullException("innerWriter");
this.innerWriter = innerWriter;
}
public void WriteSomething()
{
this.innerWriter.WriteSomething();
Console.WriteLine("How are you?");
}
}
// Composition Root
var nullWriter = new NullWriter();
var goodbyeWriter = new GoodbyeWriter(nullWriter);
var howAreYouWriter = new HowAreYouWriter(goodbyeWriter);
var helloWriter = new HelloWriter(howAreYouWriter);
var service = new SomeService(helloWriter);
// End Composition Root
// Execute
service.Write();
//Writes:
//Hello.
//How are you?
//Goodbye.
Convention over Configuration
One additional (often overlooked) benefit of DI is Convention over Configuration. When combining constructor injection with a DI container, many of them provide the ability to map ISomeService to SomeService automatically. Some containers (like StructureMap) also have the ability to build your own conventions.
The benefit isn't obvious because it really doesn't start to pay off until you are registering dozens of types using the convention. However, you can considerably reduce the amount of code it takes to compose your application if you use them.
N-Tier App
For a single application, there is normally a single composition root as close to the entry point of the application as possible. In MVC, that would be within the HttpApplication.Start method.
However, this may vary depending on whether you consider the layers of the application design to be DI Friendly Libraries or DI Friendly Frameworks and whether you consider a piece of the application as being a "plug in" that you add to after it is built (basically, making a composition root that can load dynamic dependencies).
There are essentially 3 approaches that are commonly followed to solve this issue:
- Make all of the types public and compose them in the same project that contains your presentation layer.
- Put a composition root into each layer and make the public API of each layer use DI internally. Then compose the public API of the layer using the main project. In general, you will also need to make a public
Compose method on each layer that is called from the main Compose method.
- Make a separate composition root project to compose all of the pieces together.
I have even seen some people recommend to put all of your "layers" into a single project, and if you don't intend to use these pieces individually, there isn't that much of a downside. The NuGet Gallery is one such project that is built this way.
IMHO, making everything public and putting the composition root in the main application is usually the best option for a single multi-layer application whose parts aren't intended to be used with another application.
Final Word
If you are serious about learning DI, pick up the book Dependency Injection in .NET by Mark Seemann. You might not think of DI as being a big enough area of development to study on its own, but this book really does provide many benefits that extend beyond just DI, such as SOLID principles.