Monday, August 4, 2008

Making Simple.IoC Even More Simple

Believe it or not, the following code represents most of LinFu.IoC's functionality when managing service instances:

// The SimpleContainer will handle unnamed services
public class SimpleContainer : IContainer
{
private readonly Dictionary _factories = new Dictionary();

public virtual bool SuppressErrors
{
get; set;
}

public virtual void AddFactory(Type serviceType, IFactory factory)
{
_factories[serviceType] = factory;
}

public virtual bool Contains(Type serviceType)
{
return _factories.ContainsKey(serviceType);
}

public virtual object GetService(Type serviceType)
{
object result = null;
if (!_factories.ContainsKey(serviceType) && !SuppressErrors)
throw new ServiceNotFoundException(serviceType);

if (!_factories.ContainsKey(serviceType) && SuppressErrors)
return null;

// Use the corresponding factory
// and create the service instance
var factory = _factories[serviceType];
if (factory != null)
result = factory.CreateInstance(this);

return result;
}
}

// The named container will handle named services
public class NamedContainer : SimpleContainer, INamedContainer
{
protected readonly Dictionary> _namedFactories =
new Dictionary>();

public virtual void AddFactory(string serviceName, Type serviceType, IFactory factory)
{
if (serviceName == string.Empty)
{
AddFactory(serviceType, factory);
return;
}
// Create the entry, if necessary
if (!_namedFactories.ContainsKey(serviceName))
_namedFactories[serviceName] = new Dictionary();

_namedFactories[serviceName][serviceType] = factory;
}

public virtual bool Contains(string serviceName, Type serviceType)
{
// Use the standard IContainer.Contains(Type)
// if the service name is blank
if (serviceName == string.Empty)
return Contains(serviceType);

return _namedFactories.ContainsKey(serviceName) &&
_namedFactories[serviceName].ContainsKey(serviceType);
}

public virtual object GetService(string serviceName, Type serviceType)
{
// Used the other GetService method if
// the name is blank
if (serviceName == string.Empty)
return GetService(serviceType);

// Determine if the service exists, and
// suppress the errors if necessary
bool exists = Contains(serviceName, serviceType);
if (!exists && SuppressErrors)
return null;

if (!exists && SuppressErrors != true)
throw new NamedServiceNotFoundException(serviceName, serviceType);

var factory = _namedFactories[serviceName][serviceType];

// Make sure that the factory exists
if (factory == null)
return null;

return factory.CreateInstance(this);
}
}


As you can see, there's nothing special about this code. Some might even scoff at it since it's too minimalistic to be useful. After all, one might say that an IoC container has far more responsibilities than just object instantiation. Ninject, for example has features such as contextual binding and method interception. Surely this isn't all there is to LinFu.IoC's features, is it?

Bend it like Ockham

Despite the varying complexity of most (if not all) IoC containers in the field, logically speaking, the code listed above is the absolute minimum amount of code necessary to separate the instantiation of a service instance from its actual client code. When you request a service instance from a given IoC container (whether it be LinFu, Ninject or countless other containers out there), there has to be some point where the container has to decide if that service instance can be created, as well as manage the lifetime of that service once it is already out of the container. As most of us IoC container developers know, there's quite a lot more to a container than just managing the lifetime of its individual services. Creating the instance is only the first step, and I'll show you how LinFu version 2's new IoC container will implement the some of the same features without sacrificing its relatively-horizontal learning curve.

The Pattern of Other Containers

Typically, these containers will use either property setter injection or constructor injection to autowire together all of the dependencies that an application might need during its lifetime. In addition, they might implement additional features such as interception, logging, and AOP. However, despite the differences in features among containers, there are two points where these features are commonly applied:
  • When a service is about to be created (e.g. constructor injection) or,
  • When a service is already instantiated (e.g. property setter injection, or interception)
This implies that a developer can add additional features to their respective IoC container simply by controlling the point where the service is going to be created as well as controlling the point where the service has been recently instantiated. In fact, if you can isolate those two points from the rest of the container, you can effectively add new features without affecting the rest of the code.

Now the reason why LinFu's IoC container can get away with such simple code is because it actually delegates its factory methods to an instance of the IFactory interface:

   public interface IFactory
{
object CreateInstance(IContainer container);
}

LinFu's IoC container uses each factory instance to determine how a service implementation should be instantiated, and each factory instance, in turn, is responsible for managing the lifetime of each component that it creates. With that in mind, the only thing that you have to do to extend LinFu's IoC container is to control how each factory creates its object instances (such as deciding which constructor and constructor arguments to use when implementing constructor injection) and control the instances that come out of each factory.

For example, if I wanted to add interception to LinFu.IoC, all I have to do is wrap a Decorator around an IContainer (or INamedContainer) instance that wraps each service instance in a proxy:



public class ContainerDecorator : IContainer
{
private IContainer _container;
public ContainerDecorator(IContainer realContainer)
{
_container = realContainer;
}
// Other methods skipped for brevity
public object GetService(Type serviceType);
{
// Grab the service instance
var result = container.GetService(serviceType);

// Wrap the instance, if possible
if (result != null)
return SomeProxyFactory.Wrap(result);

// Otherwise return the original instance
return result;
}
}


...and in the client code, using the additional decorator is as easy as:

var container = new ContainerDecorator(new SimpleContainer());

// Use the container somehow and transparently use the decorator to wrap the
// result
var service = container.GetService<ISomeServiceType>();

// ...

As you can see, the implementation of LinFu.IoC is very straightforward, and there's really nothing exotic about the design. For those of you who were probably wondering why LinFu.IoC isn't using generics, here's the list of extension methods that completes the design:


public static class ContainerExtensions
{
public static T GetService(this IContainer container)
where T : class
{
var serviceType = typeof (T);
return container.GetService(serviceType) as T;
}
public static T GetService(this INamedContainer container, string serviceName)
where T : class
{
return container.GetService(serviceName, typeof (T)) as T;
}
public static void AddFactory(this INamedContainer container, string serviceName, IFactory factory)
{
IFactory adapter = new FactoryAdapter(factory);
container.AddFactory(serviceName, typeof (T), adapter);
}

public static void AddFactory(this IContainer container, IFactory factory)
{
IFactory adapter = new FactoryAdapter(factory);
container.AddFactory(typeof(T), adapter);
}
public static void AddService(this IContainer container, T instance)
{
container.AddFactory(typeof (T), new InstanceFactory(instance));
}
}


Again, there's nothing unconventional in the design. In the end, it's the simplicity that matters most, and that is the difference that LinFu.IoC offers.

4 comments:

  1. What's the point of having the extension methods? IMHO they pollute the design and are just a fancy way for having static methods in a utility class.

    And what's the point of having ctor injection? How can this work if I need to inject dependencies in a 3rd party component? Besides that the .NET framework already has an IoC container that is by far the most simple container out there. And the best part is that it follows a design pattern based on a couple of simple interfaces (IContainer, ISite and IComponent). A good read is http://www.urbanpotato.net/default.aspx/document/1757

    ReplyDelete
  2. @anonymous:

    The extension methods are merely a facade to add generic support to ServiceContainer instances. I suppose I could have baked it in directly to the class, but it makes things more complicated then it needs to be.

    The whole purpose of constructor injection is so that you can initialize a type using a particular service instance from the container. I personally prefer property setter injection, but there are others out there who prefer construction injection instead.

    If you need to inject dependencies into a third party component, then all you have to do is instantiate the dependency using the container and then use a simple Lambda function to inject that dependency into the third party component itself. (That description might be pretty terse, but it's actually more simple than it seems).

    As for the .NET Framework's IContainer, I suppose one could use it and it is simple--but then again, aside from using System.ComponentModel to do some Designer Surface work, when was the last time anyone actually used it as a full-blown IoC container?

    ReplyDelete
  3. Both ctor and setter injection can lead to serious problems when there is no well defined interface contract. You cannot define a contract for a ctor (well at least not in .NET or Java) so setter injection has my preference too. But again there is often no contract and if there is, it pollutes the single responsibility principle.

    What I like about the .NET IoC container is that it is based on interface contracts, and there is a clear distinction between services and components. And it's already part of the .NET framework itself so everyone could benefit from its power.

    The last 2 (very big) projects I've been working on are based on this container and it's really an eye opener if you're used with full blown IoC containers. One could argue that it's based service locator, and AFAIK this is often seen as old-style IoC. However, since the service locator is dynamically injected by the container into a component when you add it to the container, I don't see why this is not preferred.

    ReplyDelete
  4. Good posts.

    Congratulations.

    touzas.wordpress.com // www.touzas.es

    ReplyDelete

Ratings by outbrain