UPDATE: check out the code at http://codepaste.net/t4z78p if the stuff below isn't formatted well for your browser.
using System;
using NUnit.Framework;
namespace DependencyInjectionApproaches
{
/// <summary>
/// There are multiple flavors of Dependency Injection -
///
/// 1) Constructor injection - dependencies are passed in as constructor parameters
/// 2) Setter injection - dependencies are passed in to setter methods
/// 3) Service locator - dependencies are registered with a static gateway which serves up
/// the dependencies (not really dependency *injection* per se, but let's not be sticklers)
/// 4) PMDI - Poor Man's Dependency Injection (more to come on this one later)
///
/// I've found that Setter Injection (SI) and Service Locator (SL) have an inherent problem
/// because they hide dependencies. If my class-under-test uses SI or SL, then I often have
/// to inspect the source of the class to understand which dependencies must be wired up for
/// testing the class. With Constructor Injection (CI), all of the dependencies are right
/// there in the constructor. There are quite a few other issues related to this matter
/// which I will discuss below in the context of demonstrating each flavor of dependency
/// injection.
///
/// I've defined an interface "IService" and with multiple implementations. Each concrete
/// implementation depends on "ILogger". This dependency is resolved in different ways
/// for each concrete implementation of IService.
/// </summary>
[TestFixture]
public class DependencyInjectionSpecs
{
[Test]
public void using_constructor_injection()
{
var testLogger = new TestLogger(); // Moq would be better, but trying to focus on DI here
var sut = new ConstructorDependentService(testLogger);
sut.DoWork();
testLogger.LastMessage().ShouldEqual("ConstructorDependentService did work");
// (*) when I build the System-Under-Test (sut) see how I can't even call the constructor
// without the dependency? that's good.
// also notice that I'm only testing my System-Under-Test (SUT) in its interaction with
// a single dependency. Good unit tests usually look like this. They only test one class
// in its interactions with one abstraction of a dependency.
}
[Test]
public void forgetting_to_wire_up_dependency_in_setter()
{
var sut = new SetterDependentService();
sut.DoWork(); // throws a NullReferenceException
// Oops. I'm able to create my SUT, but I can't execute the DoWork method on it. Why not?
}
[Test]
public void remembering_to_wire_up_dependency_in_setter()
{
var testLogger = new TestLogger();
var sut = new SetterDependentService();
sut.Logger = testLogger; // Note this extra requirement for the test to work (*)
sut.DoWork();
testLogger.LastMessage().ShouldEqual("SetterDependentService did work");
// (*) That's better than the last test, but more work than the first one.
// I can still make this test pass, but there's an extra setup cost. Note that the
// dependency is hard to see immediately (we missed it in the previous test).
// With constructor injection I get a compliation error if I don't set up the dependency.
// With setter injection, the missed dependency is not discovered until run time. Oops.
}
[Test]
public void forgetting_to_wire_up_dependency_in_service_locator()
{
var sut = new ServiceLocatorDependentService(); // Crud! - KeyNotFoundException (*)
// (*) The test throws immediately due to lack of service registration. Even worse, the
// dependency is completely opaque. At least with setter injection I could inspect the
// SUT with intellisense, now I'm totally hosed. I have to look at the implementation
// of ServiceLocatorDependentService to figure out how to test it (or if I can at all).
}
[Test]
public void remembering_to_wire_up_dependency_in_service_locator()
{
var testLogger = new TestLogger();
ServiceLocator.Configure<ILogger>(() => testLogger); // additional complexity - see below (*)
var sut = new ServiceLocatorDependentService();
sut.DoWork();
testLogger.LastMessage().ShouldEqual("ServiceLocatorDependentService did work");
// (*) I'm no longer testing my System-Under-Test in isolation with its dependency,
// but I'm also testing my ability to register and resolve dependencies with my Service Locator.
// This gets worse when my SUT has multiple dependencies which must be resolved from the
// Service Locator.
}
// PMDI = "Poor Man's Dependency Injection"
// PMDI may seem like a nice compromise. You get a default parameterless constructor which uses
// default implementations of dependencies. The trouble arises when you want to modify the
// constructor of one of *those* dependencies. What if DefaultLogger changes to require a
// constructor parameter? Now changes ripple to all classes that use PMDI with DefaultLogger.
[Test]
public void PMDI_is_tempting_you()
{
var sut = new PoorMansDependencyInjectedService();
sut.DoWork();
// There is nothing to assert, nothing I can test. I could visually check my console output,
// but doesn't that defeat the whole purpose of an *automated* unit test?
}
[Test]
public void PMDI_wants_you_to_come_to_the_dark_side()
{
var testLogger = new TestLogger();
var sut = new PoorMansDependencyInjectedService(testLogger);
sut.DoWork();
testLogger.LastMessage().ShouldEqual("PoorMansDependencyInjectedService did work");
// yes, it passes. and with the *same* number of lines as the first example with
// constructor injection. but consider the costs.
// 1) DefaultLogger might change to have constructor arguments (rippling problems,
// but at least caught at compile time)
// 2) DefaultLogger might change to have setter injected dependencies (now you're
// really hosed! runtime exception!)
// 3) this is really an SRP violation (Single Responsibility Principle). Your class
// is not only doing its own job, but is also tasked with determining its appropriate
// defaults. I suggest you centralize this code in an application bootstrapper and
// find a better way to resolve default implementations (incidendally, StructureMap
// has some spectacular abilities in this regard).
// my last word on PMDI:
// it seems to me that PMDI is just an attempt to avoid teaching Dependency Injection
// to people. I think we're better off showing people all the options and explaining
// the pros/cons of each. Perhaps even PMDI has it's place, but it shouldn't just be
// the knee-jerk default. For a shining example of the abuse of PMDI, check out
// many of the samples Microsoft released for ASP.NET MVC 1
}
#region test support code not relevant to the discussion of dependency injection
[TearDown]
public void after_each_test_method_executes()
{
// make sure we have a clean service locator after each test run.
// otherwise a registered service from a previous test run can affect
// the results of other tests, causing false positives or false failures
// (both really, really bad).
ServiceLocator.Reset();
}
#endregion
}
#region more test support code not relevant to the discussion of dependency injection
public static class SpecificationExtensions
{
public static void ShouldEqual(this string actual, string expected)
{
Assert.AreEqual(expected, actual);
}
// check out the NUnit.Specs project for a library of fluent wrappers
// for NUnit assertions:
// http://nunitspecs.codeplex.com/
// I think they are much more clear and concise than the out-of-the-box
// assertion syntax of NUnit.
}
#endregion
public interface ILogger
{
void Log(string message);
}
public class DefaultLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
/// <summary>
/// Yes!!! - I would much rather use a Mocking framework like Moq, but I
/// don't want to muddy the waters since the focus here is on learning
/// dependency injection.
/// </summary>
public class TestLogger : ILogger
{
string _lastMessage;
public void Log(string message)
{
_lastMessage = message;
}
public string LastMessage()
{
return _lastMessage;
}
}
/// <summary>
/// I hate xml comments, but in this context they just might help you. :)
/// </summary>
public interface IService
{
void DoWork();
}
/// <summary>
/// Implementation of IService using Constructor Injection
/// </summary>
public class ConstructorDependentService : IService
{
ILogger _logger;
public ConstructorDependentService(ILogger logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.Log("ConstructorDependentService did work");
}
}
/// <summary>
/// Implementation of IService using Setter Injection
/// </summary>
public class SetterDependentService : IService
{
ILogger _logger;
public SetterDependentService() { }
public ILogger Logger { internal get { return _logger; } set { _logger = value; } }
public void DoWork()
{
_logger.Log("SetterDependentService did work");
}
}
/// <summary>
/// Implementation of IService using Service Locator
/// </summary>
public class ServiceLocatorDependentService : IService
{
ILogger _logger;
public ServiceLocatorDependentService()
{
_logger = ServiceLocator.GetInstance<ILogger>();
}
public void DoWork()
{
_logger.Log("ServiceLocatorDependentService did work");
}
}
/// <summary>
/// Bonus! Implementation of IService using Poor Man's Dependency Injection
/// </summary>
public class PoorMansDependencyInjectedService : IService
{
ILogger _logger;
public PoorMansDependencyInjectedService() : this(new DefaultLogger()) { }
public PoorMansDependencyInjectedService(ILogger logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.Log("PoorMansDependencyInjectedService did work");
}
}
/// <summary>
/// NOTE: this is a terrible implementation of a service locator the only purpose of which
/// is to demonstrate the pitfalls of overreliance on ServiceLocation in your classes.
/// *IF* you are going to do service location, you're better off using the implementation
/// from a Dependency Injection framework (like StructureMap's "ObjectFactory").
/// You might also consider the CommonServiceLocator project:
/// http://commonservicelocator.codeplex.com/
/// In general, however, I recommend you steer clear of service locator until you grok
/// dependency injection enough that you are using constructor injection the majority of
/// the time.
/// </summary>
public static class ServiceLocator
{
static readonly System.Collections.Generic.Dictionary<Type, Func<object>> Configuration
= new System.Collections.Generic.Dictionary<Type, Func<object>>();
public static void Configure<TService>(Func<TService> factory)
{
Configuration.Add(typeof(TService), () => factory());
}
public static TService GetInstance<TService>()
{
var factory = Configuration[typeof(TService)];
return (TService)factory();
}
public static void Reset()
{
Configuration.Clear();
}
}
}
Tags: dependency injection, structuremap, s.o.l.i.d., asp.net mvc, c#