Aggregated specifications

An example from my inversion of control talk involves a message formatter.  It applies formatting rules to a string.

public interface IMessageFomatter
{
    string Format(string message);
}

Instead of doing all the work in the implementation of this interface, the message formatter will aggregate several distinct rules.  An inversion of control tool is configured to compose these rules and provide them to the formatter.

public class MessageFomatter : IMessageFomatter
{
    private readonly IFormatRule[] _rules;

    public MessageFomatter(IFormatRule[] rules)
    {
        _rules = rules;
    }

    public string Format(string message)
    {
        foreach (var action in _rules)
        {
            message = action.ApplyRule(message);
        }
        return message;
    }
}

In this way each rule can be small, testable, and have only one job.  I can add functionality to the formatting process without changing the formatter or any existing rule. 

public interface IFormatRule
{
    string ApplyRule(string text);
}

public class DisclaimerRule : IFormatRule
{
    public string ApplyRule(string text)
    {
        return text + "\nDisclaimer: This message will self-destruct";
    }
}

public class UppercaseRule : IFormatRule
{
    public string ApplyRule(string text)
    {
        return text.ToUpper();
    }
}

This technique is extremely powerful and we use it all over the place in our projects.

Recently we were building a search screen.  It has several text input boxes and select lists and radio button groups, you know, the standard enterprisey UI mess.  Anyway, that’s what it was and it’s a common feature in the apps I work on. I’ve seen the scenario over and over. But armed with the above technique and LINQ to NHibernate, we were able to craft a much more elegant solution than we had previously created.

First we have a type that represents the search query.  It could be implemented by the class that represents the user interface form or be built by the user interface layer.  It’s sent to a service that will perform the query.

public interface IProductSearchQuery
{
    decimal? Price { get; set; }
    int? PriceRange { get; set; }
    string Name { get; set; }
    ProductCategory Category { get; set; }
    // ...
}

public interface IProductRepository
{
    Product[] Search(IProductSearchQuery query);
    // ...
}

What we don’t want is a huge method that checks to see if a search criteria exists and then conditionally appends predicates to a database query.  That’s the old school way, and it’s a beast to test and to change.  Adding a user interface element, like a new text input field, would require us to change that big method by introducing repetitive and dangerous new code.

What we do want are small, isolated specifications that we can aggregate like the message formatter.

public interface IProductSearchFilter
{
    bool ShouldApply(IProductSearchQuery query);
    IQueryable<Product> Filter(IQueryable<Product> candidates, IProductSearchQuery query);
}

An example filter:

public class PriceRangeFilter : IProductSearchFilter
{
    public bool ShouldApply(IProductSearchQuery query)
    {
        return query.Price.HasValue && query.PriceRange.HasValue;
    }

    public IQueryable<Product> Filter(IQueryable<Product> candidates, IProductSearchQuery query)
    {
        return from product in candidates
               where product.Price <= (query.Price + query.PriceRange)
               && product.Price >= (query.Price - query.PriceRange)
               select product;
    }
}

You can test drive this thing.  It’s small and distinct.  And using LINQ to NHibernate you can combine several of them in a way that produces one select.

public class ProductRepository : IProductRepository
{
    private readonly ISession _session;
    private readonly IProductSearchFilter[] _filters;

    public ProductRepository(ISession session, IProductSearchFilter[] filters)
    {
        _session = session;
        _filters = filters;
    }

    public Product[] Search(IProductSearchQuery query)
    {
        var products = _session.Linq<Product>();
        return _filters
            .Where(x => x.ShouldApply(query))
            .Aggregate(products, (candidates, filter) =>
                filter.Filter(candidates, query)).ToArray();
    }

    // ...
}

One tricky spot for a lot of people is that Aggregate call.  It does the exact same thing as the foreach in the message formatter: passes the seed (products, in this case) through the filters aggregating the result.  The lambda parameter variable candidates is the result of the previous filter (or the seed for the first filter).  In other ecosystems besides .NET, Aggregate is called reduce.  You can see why – each filter reduces the candidates according to its specification.

When you think about it, an entire architecture could be a reduce. That would be really interesting.

About these ads
This entry was posted in C#, Domain-Driven Design. Bookmark the permalink.

7 Responses to Aggregated specifications

  1. Samuel Jack says:

    Matt,
    When you say you can test drive PriceRangeFilter, I can see how you test ShouldApply, but how do you test the Filter method? Surely that would mean unpicking the IQueryable to find out what criteria had been added to it?

  2. Matt says:

    No, you just do a regular state-based test on the return value using AsQueryable and ToArray, etc. The following code isn’t a good test. This example is descriptive NOT prescriptive:

    [Test]
    public void Should_filter_products_in_range()
    {
    	var query = MockRepository.GenerateStub<IProductSearchQuery>();
    	query.Price = 6m;
    	query.PriceRange = 3;
    
    	var product1 = new Product {Price = 1m};
    	var product2 = new Product {Price = 5m};
    	var product3 = new Product {Price = 10m};
    
    	var filter = new PriceRangeFilter();
    
    	IQueryable<Product> candidates = new[]{product1, product2, product3}.AsQueryable();
    
    	var products = filter.Filter(candidates, query).ToArray();
    
    	Assert.That(products.Length, Is.EqualTo(1));
    	Assert.That(products[0], Is.SameAs(product2));
    }

  3. Luca says:

    Very good article!

  4. Samuel Jack says:

    Matt,
    Of course – should have thought of that! Nice article, by the way.

  5. Matt,

    Thanks for this post. We’ve put Fluent NHibernate + LINQ-To-NHibernate + StructureMap + Aggregated Specifications to use on our current project with great success so far. The power of aggregated specifications with LINQ is incredible. The expressiveness without loss of testability has been a huge win for our team.

    –Jeff

  6. peter says:

    Hi!
    So in this method…
    public Product[] Search(IProductSearchQuery query)
    {
    var products = _session.Linq();
    return _filters
    .Where(x => x.ShouldApply(query))
    .Aggregate(products, (candidates, filter) =>
    filter.Filter(candidates, query)).ToArray();
    }

    You are tying yourself to the ENTITY Product or is that an IProduct? If its an implementation of IProduct, why not return IProduct[]
    Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s