DaedTech

Stories about Software

By

Enforcing Immutability in Multi-Threaded Projects with NDepend

Editorial Note: I originally wrote this post for the NDepend blog.  You can check out the original here, at their site.  While you’re there, have a look around at some of the features you get by downloading NDepend.

Early in the days of object oriented programming, we really got a lot of mileage out of the language and paradigm features.  For instance, if your memory extends back far enough (or your application is legacy enough), you’ll see really, really deep inheritance hierarchies.  Since inheritance was a language feature, it stood to reason that we should get our money’s worth, and boy did we ever.  We look back on that as an anti-pattern now with our 20/20 hindsight.

The same was true of application state.  The classic notion of object oriented programming was one in which objects of state and of behaviors.  For perhaps the most iconic early conception of this notion, consider the active record design pattern.  This pattern addressed the so-called impedance mismatch by presenting us with an in-memory, object-based representation of a database table.  The object’s state was the table and the behaviors were records that you could perform on it, such as saving or deleting or what have you.

While active record, particularly in some communities, has not been as roundly rejected as deep inheritance hierarchies, it no longer enjoys the favor that it did more than a decade ago.  And a big part of the reason that it, and other state-based patterns don’t, is that the object-oriented community has come to favor immutability, meaning that any data stored in an object is read-only for the lifetime of that object.

vendingmachine

Immutable objects are, first and foremost, easier to reason about.  Take, for instance, the humble object collaborator.

public class ServiceConsumer
{
    public Service TheService { get; set; }

    public void UseTheService()
    {
        var aBeer = TheService.GetMeABeer();
    }
}

This may or may not work, depending on what people come along and do with TheService.  What happens if they null it out?  To make life easier, we move away from mutable state implementations in favor of approaches like this.

public class ServiceConsumer
{
    private readonly Service _service;
        
    public ServiceConsumer(Service service)
    {
        if (service == null)
            throw new ArgumentNullException(nameof(service));

        _service = service;
    }

    public void UseTheService()
    {
        var aBeer = _service.GetMeABeer();
    }
}

Now there’s no reason to worry about the service being valid.  At the time of object construction, we enforce any preconditions, and then we don’t need to worry that _service.GetMeABeer() is going to generate a null reference exception or wind up calling a different implementation than the last time it was invoked.  ServiceConsumer is now immutable with respect to Service.

This approach is particularly valued in the multi-threaded world, where the ability to reason about a program is at a real premium.  Multi-threaded programming is really difficult, so people tasked with it need all of the help they can get.  As such, they rely heavily on immutable objects.

Immutable objects do not guarantee that problems won’t happen with threading, but they certainly help.  If you have objects that are “write once, read after that,” the possibility for race conditions is dramatically reduced by this up-front management of when things can be modified.

Detecting Immutability

If your group then adopts a “favor immutability whenever possible” approach, how is it enforced?  A day of training and then faith in the team members?  Human code review?  Angry, passive-aggressive emails following human code review?

If you’re using NDepend, that gets pretty easy.  Out of the box, NDepend’s CQLinq offers the concept of “IsImmutable”.  Here is the CQLinq for “Structures should be immutable,” which is one of the standard/default project rules.  The idea here is that developers may get behavior they don’t expect with mutable structures if they’re passing them into methods (where copies are created, rather than references being passed).

warnif count > 0 from t in Application.Types where 
   t.IsStructure && 
  !t.IsImmutable

let mutableFields = t.Fields.Where(f => !f.IsImmutable)

select new { t, t.NbLinesOfCode, mutableFields }

But in our case, we’re interested not just in mutable structures, but mutable anything.  What’s the fix?  Pretty straightforward — just get rid of the restriction that the thing needs to be a structure, and then give it a new name.

// <Name>Favor immutability</Name>
warnif count > 0 from t in Application.Types where !t.IsImmutable

let mutableFields = t.Fields.Where(f => !f.IsImmutable)

select new { t, t.NbLinesOfCode, mutableFields }

This will give you a warning for any type that’s mutable, whether class or structure.  To see it in action for yourself, observe that the original ServiceConsumer class is flagged, but the new, improved, and immutable one is not.

Enforcing Immutability

Static analysis tools are powerful for understanding and learning, but confined to the IDE, they don’t rise to their full potential.  Even if everyone had NDepend and were running occasional analysis with it, that might not fix matters.  After all, there are teams that allow compiler warnings to pile up, to say nothing of static analysis warnings.

If you want to get serious about enforcing immutability, add the static analysis tooling to your build, and fail the build if a developer introduces mutable state.  Or, to take it one step further, you could reject such commits with a gated build.  NDepend supports a number of build options/configurations and, even if you’re using something exotic, you can always execute the analysis as a shell command.

As you can see, it’s simple with NDepend to see if your team is creating immutable objects or not.  But I would definitely recommend going the extra step and enforcing the policy via the build.  It cuts down on interpersonal sniping and it guarantees more rigorous enforcement.  And, in a multi-threaded world, you need all the help you can get.