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:
// snip
// we're now injecting Interfaces, this loosens our coupling to our "injected" dependencies
public Magic8BallSimulator(IMessageService messageService, IInputService inputService,
IOutputService outputService) {
_messageService = messageService;
_inputService = inputService;
_outputService = outputService;
}
// snip
Here's our previous implementation of the the
SimulationRunner program (I dropped our crazy
MultipleOutputService class in favor of
ConsoleOutputService for the moment):
// snip
public class SimulationRunner {
static void Main(string[] args) {
Magic8BallSimulator simulator = new Magic8BallSimulator(
new MessageService(),
new ConsoleInputService(),
new ConsoleOutputService()
);
simulator.Run();
}
}
// snip
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:
- Removing our application's setup responsibility from SimulationRunner, and
- 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 (
https://github.com/autofac/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.
//snip
public class ContainerSetup {
private ContainerBuilder builder;
public IContainer BuildContainer() {
builder = new ContainerBuilder();
// in English this reads, setup ConsoleInputService as the implementation of
// IInputService
builder.RegisterType().As();
// Magic8BallSimulator doesn't implement an interface, its registered as-is
builder.RegisterType();
builder.RegisterType().As();
builder.RegisterType().As();
return builder.Build();
}
}
//snip
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):
//snip
public class SimulationRunner {
static void Main(string[] args) {
ContainerSetup containerSetup = new ContainerSetup();
IContainer container = containerSetup.BuildContainer();
container.Resolve().Run();
}
}
//snip
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:
//snip
//builder.RegisterType().As();
string outputFilePath = Path.Combine(Path.GetTempPath(), "magic8BallOutput.txt");
builder.Register(c => new MultipleOutputService(outputFilePath)).As();
//snip
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:
- 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.
- 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".
- 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:
//snip
// an enumeration expressing our different output modes
public enum OutputModes { console, popup, file, all };
// holds our configuration, this class will be easy to expand later on
// if we need to
public class Config {
public List OutputModes { get; private set; }
public Config(List outputPreferences) {
OutputModes = outputPreferences;
}
}
//snip
Here's
ArgumentsParser its whole point in life is to sanity-check and parse our command-line arguments:
//snip
public class ArgumentsParser {
private string[] arguments;
private string requestedOutputMode = string.Empty;
// default output mode when no args specified
private List outputModes = new List(){ OutputModes.console };
public ArgumentsParser(string[] args) {
arguments = args;
}
public Config GetConfig() {
SanityCheckArgs();
return new Config(outputModes);
}
private void SanityCheckArgs() {
// no-args to check
if (arguments.Length == 0)
return;
// too many args
if (arguments.Length > 3)
throw new ArgumentException(
"Too many arguments were specified, expected 'console','popup','file', or 'all'");
SetupOutputModes();
}
private void SetupOutputModes() {
outputModes = new List();
// see if each requested output-mode exists in our struct
foreach (string outputModeRequested in arguments) {
try {
outputModes.Add(
(OutputModes)Enum.Parse(typeof(OutputModes), outputModeRequested, true)
);
} catch {
throw new ArgumentException(String.Format(
"Illegal output mode '{0}' requested, expected 'console','popup','file', or 'all'",
outputModeRequested));
}
}
// handles odd input like "console all"
if( outputModes.Contains(OutputModes.all) )
outputModes = new List() { OutputModes.all };
}
}
//snip
Here are our change to
ContainerSetup.BuildContainer, notice the new
config parameter:
//snip
public IContainer BuildContainer(Config config) {
_config = config;
// snip - previous code is still valid
// we're now registering our IOutputService based on our pass config configuration class
if (_config.OutputModes.Contains(OutputModes.console) || _config.OutputModes.Contains(OutputModes.all))
builder.RegisterType().As();
if (_config.OutputModes.Contains(OutputModes.popup) || _config.OutputModes.Contains(OutputModes.all))
builder.RegisterType().As();
if (_config.OutputModes.Contains(OutputModes.file) || _config.OutputModes.Contains(OutputModes.all)) {
string outputFilePath = Path.Combine(Path.GetTempPath(), "magic8BallOutput.txt");
builder.Register(c => new FileOutputService(outputFilePath)).As();
}
return builder.Build();
}
//snip
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:
//snip
private IEnumerable _outputServices;
// we're now injecting Interfaces, this loosens our coupling to our "injected" dependencies
public Magic8BallSimulator(IMessageService messageService, IInputService inputService,
IEnumerable outputServices) {
_messageService = messageService;
_inputService = inputService;
_outputServices = outputServices;
}
public void Run() {
PrintWelcome();
string message = string.Empty;
PrintInputPrompt();
_inputService.GetInput();
while (!_inputService.ExitWasRequested()) {
message = _messageService.GetMessage();
PrintMessage(message);
PrintInputPrompt();
_inputService.GetInput();
}
PrintExit();
}
private void PrintWelcome() {
foreach (IOutputService outputService in _outputServices)
outputService.PrintWelcome();
}
private void PrintInputPrompt() {
foreach (IOutputService outputService in _outputServices)
outputService.PrintInputPrompt();
}
private void PrintMessage(string message) {
foreach (IOutputService outputService in _outputServices)
outputService.PrintMessage(message);
}
private void PrintExit() {
foreach (IOutputService outputService in _outputServices)
outputService.PrintExit();
}
//snip
And finally, here's our new
SimulationRunner tying it all together:
//snip
static void Main(string[] args) {
ContainerSetup containerSetup = new ContainerSetup();
IContainer container = containerSetup.BuildContainer();
container.Resolve().Run();
}
//snip
The
PopupOutputService and
FileOutputService classes were trivial, I've added those to 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
I'm surprised to hear that this didn't "just work" with the RoboGuice 2.0 jar and ActionBarSherlock as a library project. ABS was meant to seamlessly replace the official compat library with little-to-no code changes (which only deal with Menu/MenuItem incidentally).
ReplyDeleteIf you are using maven you can use the <excludes> specification on the RoboGuice dependency to remove its transitive dependency on the official compat lib. Then ActionBarSherlock 3.x should fulfill that requirement since its classes exist in the same package. This was reported to have worked on the Google Group for ABS.
I haven't yet had time to experiment with implementing both of these libraries in the same project (yet!) but hopefully the coming changes to ADT and library projects will allow the inclusion of both to be much easier.
After all, it is without a doubt a winning combination.
Hey Jake,
ReplyDeleteThanks for the info, resolving this when building sure beats writing a few awful one-off classes. I'll give that a shot and edit my post with the results.