Language: C#
Dependency Injection Flavors - The Good, The Bad and the Ugly
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(); } } }
by
April 18, 2010 @ 8:45pm
April 18, 2010 @ 8:45pm
Report Abuse
Subscribe
Discuss
What's new
What is it
New Snippet
Recent Snippets
My Snippets
Web Code
Search

