Tuesday, July 02, 2013

Service Locator vs Dependency Injection

While I've been exclusively using DI since I wrote articles like this one or this one (and several others on the subject of Service Locators), I think I finally figured out how to explain why DI is better: it's because it forces a design change in classes.

Start with the normal mess, where your method directly initializes a concrete class, let's say in the constructor:

  public class DoesSomethingThatRequiresLogging
  {
    public DoesSomethingThatRequiresLogging()
    {
      logger = new FileLogger(@"c:\temp\log.txt");
    }

    //

    private FileLogger logger;
  }

Your methods happily call logger.Log(...) and all is good with the world.

However, at some point you have to test this, or reuse it, or change it to log to a database table instead of a file. Ouch.

The first solution is the use of a service locator. Create the logger outside of the constructor and use it as required:

  // in Main
  ServiceLocator.Register(new FileLogger(@"c:\temp\log.txt"));

  // in constructor
  logger = ServiceLocator.Resolve<FileLogger>();

This removes the creation of the object from the class, but it still leaves it with a dependency on the concrete, file logging implementation. We can change that by using interfaces.

  public interface ILogger
  {
    void Log(string message);
  }

  public class FileLogger: ILogger
  {
    ...
  }

  // in Main
  ServiceLocator.Register(new FileLogger(@"c:\temp\log.txt"));

  // in constructor
  logger = ServiceLocator.Resolve<ILogger>();

Now the logger can be changed easily to a DatabaseLogger, without affecting the class in any way. That's huge - it solves two of the three problems we started with (the class can be tested and the logger can be changed to log to some other medium). There's still the problem of reuse, though. We can't determine, just by looking at the class, what other objects it will need (what its dependencies are). It is possible to try to use it in another project only to discover that it crashes in some cases because IDoSomethingHidden wasn't registered in the ServiceLocator.

That leads us to the next step: make all dependencies explicit. This is usually done with constructor injection but there are alternatives (like property injection, used normally for dependencies that aren't required: like a class that will log something if there's a logger but will happily work without one otherwise):

  public class DoesSomethingThatRequiresLogging
  {
    public DoesSomethingThatRequiresLogging(ILogger logger)
    {
      this.logger = logger;
    }

    //

    private FileLogger logger;
  }

  // in Main
  var logger = new FileLogger(...);
  var instance = new DoesSomethingThatRequiresLogging(logger);

  // in tests
  var logger = new Mock<ILogger>();
  var instance = new DoesSomethingThatRequiresLogging(logger.Object);

(I used Moq in the testing example.)

The final step would be to use some kind of dependency injection container (I use Munq) but that's mostly required when objects are not explicitly created by you (like the controllers in the case of the MVC framework).

So, there you have it: the reason for the road from regular spaghetti code to service locator to dependency injection (to maybe a DI container).

No comments: