Wednesday, September 29, 2010

Dependency Injection Part 3: Making our lives easier

In my last post, we implemented a set of interfaces in our service-classes and injected those interfaces into the Magic8BallSimulator class. This de-coupling of Magic8BallSimulator and its dependencies, allowed us to radically change how we output text in our application without breaking it. Here's the Magic8BallSimulator constructor we ended up with last time, it's nice and de-coupled:Here's our previous implementation of the the SimulationRunner program (I dropped our crazy MultipleOutputService class in favor of ConsoleOutputService for the moment):

Wait a moment

Have a look at the SimulationRunner class above one more time. SimulationRunner is closely coupled to several specific classes, namely Magic8BallSimulator, MessageService, ConsoleInputService, and ConsoleOutputService. There's also a more subtle problem with SimulationRunner, it has more than 1 responsibility. SimulationRunner is currently:
  • Setting up our application, in fact it creates a new instance of every class in the application
  • Running our application (via simulator.Run())
Certainly SimulationRunner ought to have the responsibility of running our application, but how can solve our two remaining problems of:
  1. Removing our application's setup responsibility from SimulationRunner, and
  2. de-coupling SimulationRunner from specific instances of our service classes?
Our attack will involve using a Dependency Injection framework and a new class whose sole responsibility is setting up our application.

Autofac (http://code.google.com/p/autofac/) - a badass Dependency Injection framework for .NET.

Here's an absurdly brief overview of what Autofac can do for us (if you want more information, check-out the above link to Autofac's google-code site or this article on CodeProject). Autofac allows us to register classes like MessageService as implementations of specific interfaces (like IMessageService). Here's the best part, if we register all our dependencies for Magic8BallSimulator as well as Magic8BallSimulator itself with Autofac, Autofac will also examine Magic8BallSimulator's constructor parameters and inject the registered dependent classes for us. That kicks ass!

Here's our new ContainerSetup class, it registers our application classes with Autofac and does our application setup work.
Inside BuildContainer() we're registering the classes that Autofac will provide when their relevant interfaces are requested. For example, on line 10, we're registering ConsoleInputService as the class to use when one of our classes has a dependency on the IInputService interface (hey, Magic8BallSimulator depends on that interface!). Notice that we didn't map Magic8BallSimulator to anything, it doesn't implement an interface.

Problem Solved!

Here's our new SimulationRunner class. It's de-coupled from the specific classes in our application and is free of all setup responsibilities. It's asking Autofac for the class to run (that's the call to Resolve() on line 6):As I mentioned before, since we've registered our Magic8BallSimulator class with Autofac as well as its dependencies (MessageService, ConsoleInputService, and ConsoleOutputService), Autofac will create and inject instances of those dependent classes into Magic8BallSimulator for us. Scroll up and look at the old Magic8BallSimulator constructor, it hasn't changed at all :)

For the curious

To keep this post relatively simple, I dropped our crazy MultipleOutputService class in favor of ConsoleOutputService. Remember MultipleOutputService? We used that class to show how de-coupling with interfaces allowed us to radically change a whole section of our application (and even its behavior) without breaking anything. It's also worth noting that MultipleOutputService's constructor takes a file-path string as it's argument.

If we wanted to use MultipleOutputService as our implementation of IOutputService we'd make the following change to our ContainerSetup class:In the code above, we're still specifying MultipleOutputService as our implementation of IOutputService, but we're also providing Autofac with a simple lambda expression to use when creating MultipleOutputService. The good news is, if we keep the code above, nothing else has to change (we'll still get radically different output behavior).

Source code for this post.

but wait, there's more

Steve asked a great question: But what if I want to use both MultipleOutputService and ConsoleOutputService? Say there is a user option to select which method to output with, can a DI framework handle this for me?

Since it's trivial to register more than one implementation of IOutputService with the dependency injection framework, the framework can still help us here. We'll have to write the user-activated output switch option though :) Here's the approach I see:
  1. Remove MultipleOutputService and add PopupOutputService and FileOutputService to express our new modes of output. We'll also keep ConsoleOutputService around since one of our options is to print to the console.

  2. Since this silly example currently runs in the console, I'm going to let users pass a few command-line parameters specifying how they'd like their output delivered. Legal parameter values will be: "popup","file","console", and "all". If we get odd combinations like "console all" we'll assume "all".

  3. We'll also need a class to hold our configuration. That class we'll be constructed by parsing our command-line arguments and will be passed into our ContainerSetup.BuildContainer method so we'll know which classes to regsiter as implementations of IOutputService.
Here's our configuration class, aptly named Config:Here's ArgumentsParser its whole point in life is to sanity-check and parse our command-line arguments: Here are our change to ContainerSetup.BuildContainer, notice the new config parameter:We're now expecting a collection of IOutputService's in Magic8BallSimulator, so we'll modify that class as follows (notice the new IEnumerable constructor parameter and the _outputServices property:And finally, here's our new SimulationRunner tying it all together:The PopupOutputService and FileOutputService classes were trivial, I've posted those here. Here's the new source-code for this section of the post as well.

Unit Tests up next

Although our new code is de-coupled, to ensure that its solid, we'll need to test it a bit. We'll start by testing each class separately. >> Next Post

3 comments:

Steve M said...

But what if I want to use both MultipleOutputService and ConsoleOutputService? Say there is a user option to select which method to output with, can a DI framework handle this for me?

Max said...

Thanks for the question. That's such a good question in fact, that I've modified my post to answer it.

Anonymous said...

I appreciate your demonstration of how to tackle the need for multiple output service implementations, and it seems your answer was to, well, not. Instead you define up front which implementation you need.

For the sake of discourse, is this to say Autofac can't handle multiple implementations of the same interface? Is this a limitation of Autofac or a deliberate design policy?

Thanks! Great article by the way, I'll be bookmarking it for the next time someone asks me about Autofac.