When you add dependencies into your classes you are tightly coupling your code together. Your classes have an intimate knowledge of each other, which might sound good until 6 months down the line you need to swap out some of the functionality. Have a look at this example:
public static void LogError(Exception e) {
MyLogger logger = new MyLogger();
logger.Log(e);
}
When you call new
to initialize an object, you are creating an object with a concrete implementation. Concrete is hard, and a bitch to move.
What if you didn't want a MyLogger
but instead wanted a SomeOtherLogger
? What if you wanted to write a test for the LogError method, but didn't actually want to log an error? You can't as it stands. Wouldn't it be nice if you could request your logger without caring how your message will be logged, so there is only one place you define the implementation, and you could use a different implementation in your unit tests? That's what Inversion of Control is all about. You give up control of selecting implementations to an IoC "container".
Let's look at making your code more testable and flexible by adding an IoC container which will enable you to swap out functionality in your application very easily.
How do you do that in practice?
You need to add interfaces for your concrete classes. These act as contracts for your methods and properties. You can create new implementations at any time and swap them out as you please, and your calling code doesn't need to know about the implementation, just that it can call a method, for example, called LogError
.
We also need an IoC container. This is simply an object that contains mappings from your interface contracts to your concrete class implementations. It is then used to return concrete implementations when requested, or implictly through constructor injection. Constructor injection seems to be widely regarded as the preferred method.
An IoC container should be defined at the entry point of your application - here you can register your implementations for each interface you want to enable IoC for. This is the only place the implementations need to be mentioned as once this is set up we can use constructor injection or a dependency resolver to get references to the concrete types. Let's see an example.
Hello Injector!
We'll build a simple greeting console application, that says hello. Nothing special. To start with we need to create an interface for a class that will be responsible for returning the message. It will look like this:
public interface IGreeter
{
string GetGreeting();
}
Now let's add an implementation for it.
internal class HelloWorldGreeter : IGreeter
{
public string GetGreeting()
{
return "Hello World";
}
}
Dead simple. Notice the internal access modifier. This means the implementation is only available to the containing assembly - just to prove a point later that the test suite doesn't even need to see the real implementation.
Now we need Simple Injector. Add the package via nuget: Install-Package SimpleInjector
I added a simple class to help resolve our dependencies, it looks like this:
using SimpleInjector;
public class DependencyResolver
{
public static Container Container { get; private set; }
public static void SetupContainer(Container container)
{
Container = container;
}
public static T Get<T>() where T:class
{
if (Container == null) throw new InvalidOperationException("Cannot resolve dependencies before the container has been initialized.");
return Container.GetInstance<T>();
}
}
Now let's add a messenger class that will contain our messenging functionality:
public class Messenger
{
private readonly IGreeter _greeter;
public Messenger(IGreeter greeter)
{
_greeter = greeter;
}
public string GetWelcomeMessage()
{
return _greeter.GetGreeting();
}
}
This class uses Constructor Injection. Since we will have a mapping for IGreeter
in our container, if we request a Messenger object from the container, this dependency will be automatically populated for us, because any child objects will also utilize the container. Nice yes?
Now let's add the actual program:
class Program
{
static void Main(string[] args)
{
var container = new Container();
container.Register<IGreeter, HelloWorldGreeter>();
container.Verify();
DependencyResolver.SetupContainer(container);
RunProgram();
}
static void RunProgram()
{
var messenger = DependencyResolver.Get<Messenger>();
Console.Write(messenger.GetWelcomeMessage());
Console.ReadLine();
}
}
So, we set up an IoC container and register the implementation of HelloWorldGreeter
for IGreeter
. Then we verify the container is set up correctly and store it for use in our program. The magic happens here: DependencyResolver.Get<Messenger>()
. Our container will inject the implementation of IGreeter
automagically into the constructor. When you run the program, you can guess what is printed out:
Yep, it's one of te more complicated hello worlds out there, but boy does this give you some benefits in your applications. Look what we can do next.
Adding a Different Implementation for Testing
Let's say we want a different implementation for tests, we don't need to know about the normal implementation, let's make our own. Add your unit tests project, and add an initializer to set up a new container:
[TestClass]
public class Setup
{
[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
var container = new Container();
container.Register<IGreeter, HelloTesterGreeter>();
container.Verify();
DependencyResolver.SetupContainer(container);
}
}
Look, we can have a separate container for our tests, with completely different implementations. Now we can add the HelloTesterGreeter
:
internal class HelloTesterGreeter : IGreeter
{
public string GetGreeting()
{
return "Hello Tester!";
}
}
And a test for the messenger:
[TestMethod]
public void MessengerDisplaysCorrectMessage()
{
var messenger = DependencyResolver.Get<Messenger>();
Assert.AreEqual("Hello Tester!", messenger.GetWelcomeMessage());
}
And guess what, it passes.
Conclusion
I hope this has helped your understanding of IoC and given you a good start to go and do this in your applications.
For a full sample, clone my repo: https://github.com/timcodesdotnet/ioc-samples