Apr 17 2010

Dependency Injection Flavors - The Good, The Bad and the Ugly

Category: Software DevelopmentJeff @ 08:19

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: , , , ,

Comments

1.
Michael Baun United States says:

Firefox likes it but IE 7 does not.

2.
Jeff Doolittle United States says:

@Mike - yeah, just one more reason to update to a real browser.  Smile

Apparently I need to find another code highlighter that works for IE.

Comments are closed