IoC Challenge – Ninject

In my recent post, IoC Challenge – Multiple interfaces to the same singleton instance, I posted an IoC challenge to the community. I also mentioned that on the Ninject discussion groups, a suggestion was posted on how to solve this using Ninject.

Today, I decided to try out that suggestion, and I have to admit, it is quite elegant so far. Here is the source code used.

First, the interfaces.

public interface IInitializable
{
  void Init();
}

public interface IStartable
{
  void Start();
  void Stop();
}

Then the implementations.

class ServiceOne : IInitializable, IStartable
{
  public void Init()
  {
    Console.WriteLine(GetType().Name + ".Init() " + GetHashCode().ToString());
  }
  public void Start()
  {
    Console.WriteLine(GetType().Name + ".Start() " + GetHashCode().ToString());
  }
  public void Stop()
  {
    Console.WriteLine(GetType().Name + ".Stop() " + GetHashCode().ToString());
  }
}

class ServiceTwo : IInitializable, IStartable
{
  public void Init()
  {
    Console.WriteLine(GetType().Name + ".Init() " + GetHashCode().ToString());
  }
  public void Start()
  {
    Console.WriteLine(GetType().Name + ".Start() " + GetHashCode().ToString());
  }
  public void Stop()
  {
    Console.WriteLine(GetType().Name + ".Stop() " + GetHashCode().ToString());
  }
}

Yes. I know. Really creative class names and implementations.

Now the test program.

static void Main(string[] args)
{
  IKernel kernel = new StandardKernel(new MyModule());

  foreach (IInitializable item in kernel.GetAll<IInitializable>())
  {
    item.Init();
  }

  foreach (IStartable item in kernel.GetAll<IStartable>())
  {
    item.Start();
    item.Stop();
  }

  foreach (IInitializable item in kernel.GetAll<IInitializable>())
  {
    item.Init();
  }

  foreach (IStartable item in kernel.GetAll<IStartable>())
  {
    item.Start();
    item.Stop();
  }
}

This test works exactly as desired. A run of the program will yield the following results.

ServiceOne.Init() 18643596
ServiceTwo.Init() 33736294
ServiceOne.Start() 18643596
ServiceOne.Stop() 18643596
ServiceTwo.Start() 33736294
ServiceTwo.Stop() 33736294
ServiceOne.Init() 18643596
ServiceTwo.Init() 33736294
ServiceOne.Start() 18643596
ServiceOne.Stop() 18643596
ServiceTwo.Start() 33736294
ServiceTwo.Stop() 33736294

As you can see, all calls into ServiceOne through either Init(), Start(), or Stop() reference the same instance. The same for ServiceTwo.

So how did we achieve this success? Well, though the one missing piece of source code – the Ninject module. Here is that module.

class MyModule : Ninject.Modules.NinjectModule
{
  private void RegisterServices<TConcreteClass>()
    where TConcreteClass : IInitializable, IStartable
  {
    string name = typeof(TConcreteClass).Name;
    Bind<IInitializable>().To<TConcreteClass>().InSingletonScope().Named(name);
    Bind<IStartable>().ToMethod(context => context.Kernel.Get<IInitializable>(name) as IStartable).Named(name);
  }
  public override void Load()
  {
    RegisterServices<ServiceOne>();
    RegisterServices<ServiceTwo>();
  }
}

I have generalized the code a bit from the sample on the discussion forum, this is an attempt to allow me to possibly grow the number of service interfaces supported at a later time. I would probably also pull out the lambda expression into a variable to be reused if I had more bindings, but otherwise, it is very simple. In truth, it is quite elegant the way Ninject supports bindings.

Now comes the down side of this approach. I do not like that IStartable is defined in terms of IInitializable. It sets up a pattern in which global service interfaces have a dependency model, in terms of definition, and that means I need to bind them at the same time and place. It also means that, in this model, I must have all of the interfaces supported by the instance. I would have to add some more logic to support a model of registering in which any combination of interfaces were supported – such as a class with only IStartable or only IInitializable.

So while this provides a specialized solution for a specific use case, it does not yet provide a fully generalized solution. I will keep working on it, and hopefully the very helpful people in the Ninject forums will help as well. Until then, the challenge still stands.