Rob Smyth

Wednesday, 26 March 2008

Dependency Injection (IoC) & NDependencyInjection

In the last couple of months we have, at work, started using a new dependency injection (IoC) framework (NDependencyInjection) on a code base that had not been fully using IoC. As this is a more advanced pattern being retrospectively applied to an existing code base, it has been difficult to demonstrate the benefits. It had been a bit of leap of faith. But, in the last few days we have reached that 'critical mass' point were we are repeatedly finding that it has reduced our cost of change. Adding new features and refactoring has become easier. It is already saving us time ($$).

I'm finding that IoC simplifies:
  • Object life cycle control.
  • Object wiring.
  • Object construction - Automates code generation for circular dependencies.
I've seen this again today while working on my CFAReader project. A few days ago I reached the point were the complexity of constructing objects, especially those with circular dependencies, became a significant burden. So I implemented NDependencyInjection. A fragment of before and after code is show below.

Before:
  CfaRegionsChangedListenerConduit regionChangedConduit = new CfaRegionsChangedListenerConduit();
ICfaRegions cfaRegions = new CfaRegions(regionChangedConduit, persistenceService);

FormatterListenerConduit formatterListenerConduit = new FormatterListenerConduit();
IncidentGridViewCellFormatter incidentGridViewCellFormatter =
new IncidentGridViewCellFormatter(cfaRegions, formatterListenerConduit);
regionChangedConduit.SetTarget(incidentGridViewCellFormatter);

IncidentsGridViewController incidentsGridViewController = new IncidentsGridViewController();
IncidentsGridView incidentsGridView =
new IncidentsGridView(cfaDataSet, incidentsGridViewController, incidentGridViewCellFormatter);
incidentsGridViewController.Inject(incidentsGridView, new BrowserMapView());
formatterListenerConduit.SetTarget(incidentsGridViewController);

incidentsView = new IncidentsView(new RegionSelectionControl(cfaRegions), incidentsGridView);
hostServices.Show(incidentsView, DockState.Document);

After:
   system.HasSubsystem(new IncidentsViewBuilder()).Provides<DockContent>();
hostServices.Show(system.Get<ContentForm>(), DockState.Document);

:

public class IncidentsViewBuilder : ISubsystemBuilder
{
public void Build(ISystemDefinition system)
{
system.HasSingleton<CfaRegions>().Provides<ICfaRegions>();
system.HasSingleton<IncidentGridViewCellFormatter>().Provides<IncidentGridViewCellFormatter>();
system.HasSingleton<IncidentsGridViewController>().Provides<IncidentsGridViewController>();
system.HasSingleton<IncidentsGridView>().Provides<IncidentsGridView>();
system.HasSingleton<BrowserMapView>().Provides<BrowserMapView>();
system.HasSingleton<RegionSelectionControl>().Provides<RegionSelectionControl>();
system.HasSingleton<IncidentsView>().Provides<ContentForm>();
}
}
Although a trivial example, I find the after code to be more readable. It has also separated the object wiring from the usage code. The wiring necessary to build a ContentForm object is hidden in the builder. This makes code reuse easier. It also abstracts the use of common objects (in this case the CFADataSet and the PersistenceService. Traditionally this is done by using a factory, but that would required common objects to be passed explicitly down in a series of constructors/methods. This makes managing object life cycles much easier.

What I'm now finding is that I can add a parameter to a constructor and the application 'just works' without any other code changes as the IoC framework just finds the required objects using its wiring rules. No need to work out life cycles and walk up the ladder of factories. This means that the product's architecture is less likely to become corrupted as developers add more features. So it becomes an enabler for less skilled developers to work on the code.

One 'gotcha', and perhaps a future feature for NDependencyInjection is that adding a new parameter to a constructor does mean that unit tests must be updated to provide the mocked object. Wouldn't it be great if NDependencyInjection could be set to a unit testing mode for a given type so that when and instance of the type is requested all required types are generated as mocked objects? Perhaps a 'GetTestObject' method to compliment the current 'Get' method? Further code generation automation ... write less code and reduce the risk of less skilled developers introducing integration tests disguised as unit tests.

NDependencyInjection is well worth the effort. Great work Nigel!.

No comments: