Software Design Blog

Journey of Rinat Abdullin

Zen Development Practices: Method-level IoC

One of the things I really like about the software development is a simple fact that having the right idea, tool or even just a proper state of mind could help you to avoid writing a lot of unnecessary code. As we all know, unnecessary code results in extra development friction and increased maintenance burden which come down to a higher TOC. This could scale from a few methods and up to the classes, micro-frameworks and even sub-systems.

All that matters in the end, is about the delivery of the unique business value within the budget, quality and time constraints.

In this article I’ll try to give you a brief overview overview of a few concepts (and a synergy between them) that have worked for me like this before. Most interesting of them I would refer to as a Method-level Inversion of Control.

Let’s have a closer look at a really short extension method that simplifies Autofac usage for me in some specific scenarios:

public static void Build(
  [NotNull] this IContainer container,
  [NotNull] Action<ContainerBuilder> registration)
{
  if (container == null) throw new ArgumentNullException("container");
  if (registration == null) throw new ArgumentNullException("registration");

  var builder = new ContainerBuilder();
  registration(builder);
  builder.Build(container);
}

As you can see, there are just a few lines of code here. Yet the amount of ideas and concepts related to them is slightly more diverse. Let’s start walking over them.

Basically this extension simplifies some in-line registrations for the existing container. You might need them from time to time, when defining some environment-based registrations, using MockContainer for unit testing or simply starting a new prototype project.

Here’s how the code looks with and without this extension method:

// old style registration
var builder = new ContainerBuilder();
builder.Register<MyComponent>().As<IMyService>();
builder.Build(container);

// Equivalent registration with the extension method
container.Build(b => b.Register<MyComponent>().As<IMyService>());

Obviously, you could have multiple registrations and anything else that could be done with a ContainerBuilder.

By the way, this simple extension method is currently in the Lokad.Stack.dll. If anybody actually wants to see it in Autofac.dll, just drop a comment and I’ll push it up to the Autofac codebase.

The statement above might seem to be an overkill for “just a mere extension method”, yet it highlights (once more) an extremely important rule of thumb that has helped me to develop and keep stable a few shared libraries so far. Here it is:

You should never put some helper class or method into the shared codebase, just because it looks cool or useful. It should stay in the original solution where it is used until two or more external solutions show distinct need to reuse it and it is obvious that this code could be reused efficiently (sometimes it is better not to reuse). Only then, we’ll move that reusable code upstream (into the common shared project), while making sure that it fits perfectly all the use-cases.

By the way, it is a good practice to have an automated continuous integration project (or a couple of them) that regularly pulls together all the different solutions used in some projects (this includes 3rd party libraries available as open source), compiles them all and then runs tests. This kind of validation helps to detect possible dormant issues (i.e.: breaking API changes) as soon as possible, simplifying their resolution.

What are these NotNull attributes? These are simple code hints for the ReSharper that are declared in Lokad.Quality.dll. They indicate that the method does not expect null values and explicitly enforces this principle of good citizenship by throwing ArgumentNullException. ReSharper will keep this in mind, while performing solution-wide analysis and highlighting warnings and suggestions (I think that’s a decent benefit for just a few short attributes).

Let’s get back to the idea behind that extension method for Autofac IoC Container. You could consider it to be an example of method-level Inversion of Control. This pattern is really useful, when we want to capture or perform some operations against the underlying object, while enforcing some context and keeping the complex logic - clear of some implementation details.

In the case of the Autofac builder extension we were capturing the component registrations, while enforcing their binding to the current IContainer in the end (that’s yet another solution to the builder syntax problem, by the way).

Here is another usage of this pattern, coming from a different domain - my sandbox prototype for working with CouchDB. Check out the snippet bellow, paying attention to the lambda expressions with the stream parameter:

public DocumentInfo SaveDocument<T>(T item, Func<T, Guid> id)
{
  using (var r = _server.Do(id(item).ToString(), "PUT",
    stream => Serializer.Serialize(stream, item)))
  {
    FailsUnless(r, CouchStatus.Code201);

    return r.ReadObject(stream =>
      Serializer.Deserialize<DocumentInfo>(stream));
  }
}

This snippet simply implements a wrapper for the RESTful document management API of CouchDB.

As you can see, there are two places, where we need to access request and response streams. Yet, there is no explicit stream management and disposal. That’s because we’ve implemented this logic already in the code that is actually responsible for that. This results in more compact and reliable code that frees the developer from remembering about stream management and other technical details that are not important at this layer. We also know that all the required extensibility points are already considered and enforced in the design (see previous CouchDB article for details).

Zen image under Creative Commons Attribution ShareAlike 3.0 License from Wikipedia

Given that, implementing some sort of wrapper for a specific RESTful API becomes more peaceful and enjoyable experience. That’s the way of Zen.

As you can guess, this pattern could be applied to other resource-management scenarios or situations as well.

Another usage scenario of this pattern involved developing scheduling logic for a piece of concurrent data processing engine. We wanted to write and run complex queries against the complex domain model which was updated by a few asynchronous processes. Sometimes we even wanted to modify this model from within the query. Another obvious requirement was: query code (already complex) had to be atomic and without any code for managing transactions. All this resulted in a simple interfaces like the ones below (I’m extremely oversimplifying the domain, while keeping the primary idea):

interface IQueryableDomainModel
{
  void RunQuery(Action<IQueryTarget> query);
}

interface IQueryTarget
{
  int NumberOfTasks { get; }
  int ScheduleSize { get; }
  Task[] GetTasks(int count);
  void EnqueueTasks(IEnumerable<Task> tasks);
}

while the query was able to be declared in a manner:

static class SchedulingQueries
{
  void OptimisticScheduling(IQueryTarget target)
  {
    var tasks = target.GetTasks(target.NumberOfTasks);
    if (tasks.Length > 0)
    {
      target.EnqueueTasks(tasks);
    }
  }

  void SchedulingApproach2(IQueryTarget target)
  {
     if (target.ScheduleSize < 10)
     {
       OptimisticScheduling(target);
       return;
     }
     HistoricalScheduling(target); 
  }
}

In order to execute the query you just had to use the code like the one below:

IQueryableDomainModel model = ...
model.RunQuery(OptimisticScheduling.SchedulingApproach2);

IQueryableDomainModel model implementation takes the responsibility of ensuring concurrency by calling locks before executing the captured lambda argument and then releasing them after (similar to how we created ContainerBuilder before running lambda against it and then applied it to the container after).

If we were in need for some serious performance optimization, we could even start by inspecting the captured query statement and using fine-tuned locking scenarios (without even changing a single line in the original query).

You may have already noticed that method-level IoC basically allows to create micro DSL in C#. That’s precisely how this pattern has worked out in Lokad.Rules (yet another sample of the pattern), where each business rule is some compact code running against the IScope interface. Core logic is encapsulated in the rules, but it is the IScope (and the implementatios) that is to control the context and determine what to do with the rules.

This results in rules that could be expressed and combined in a code like:

/// <summary>
/// User name should have length within 6 and 256 characters 
/// and should be a valid email.
/// </summary>
internal static readonly Rule<string>[] UserName = new[]
  {
    StringIs.Limited(6, 256),
    StringIs.ValidEmail
  };

You can check out Lokad.Rules for more details, if interested. One thing worth of mentioning (if you decide to dive into the source code) is that the entire framework for business rules and validation is really small. We do not have anything classes like IntGreaterThanValidator or CompositeValidator simply because they are not really needed for this scenario.

Well, this turned out to be quite some text about an extremely simple extension method. Yet, hopefully, the journal post hints that software development is not about coding or size of the codebase. It is about the ideas and concepts that help to leverage the technology in order to achieve some business goals. If used efficiently, they can result in a small codebase. There could even be no code at all (but that’s a pure Zen).

I know a few other interesting development concepts that are not so popular for some reason and yet helped a lot in complex projects by reducing the codebase, while making everything more simple and flexible. Eventually they might crystallize into more articles (like this one) with patterns, practices and theory. Although this is not going to happen any time soon, you might subscribe to this journal to get the updates.

Meanwhile, I’d love to hear your thoughts and feedback on this article.