New Snippet New Snippet Recent Snippets Recent Snippets My Snippets My Snippets Web Code Search Snippets Search
Sign inor Register
Language: C#

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

138 Views
Copy Code Show/Hide Line Numbers
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

Add a comment


Report Abuse
brought to you by:
West Wind Techologies


If you find this site useful and use it frequently please consider making a donation to support this free service.
Donate