Sunday, September 9, 2012

Refactoring implicit language elements to form interpreters


In business applications, while creating searching algorithms you need to create a search context which consists of a combination of multiple algebraic expressions. A simple example is given below where the filter for searching books is created by combining multiple expressions as in the code sample below.

[TestMethod]
public void GetAllBooksInRangeByTopicShouldReturnAllBooksInThePriceRangeFilteredByTopic()
{
    var amazonService = new BookStore();
    const decimal fromRange = 50;
    const decimal toRange = 100;
    const string genre = "Travel Guide";
    var books = amazonService.GetBooksByPriceRangeAndGenre(fromRange, toRange, genre);
    Assert.IsTrue(books.Any());
}

public IEnumerable<Book> GetBooksByPriceRangeAndGenre(decimal from, decimal to, string genre)
{
    return GetAll().Where(x => x.Genre.Name.ToLower().Contains(genre.Trim().ToLower())
                                && (x.Price >= from && x.Price <= to));
}

We can refactor these expressions to a more readable and maintainable structure by creating specifications using the interpreter pattern as given below.


public IEnumerable<Book> GetBooksByPriceRangeAndGenre(decimal from, decimal to, string genre)
{
    var bookSpec = GetBookSpec(genre, from, to);
    return GetAll().Where(bookSpec.SatisfiedBy());
}

private BookSpecification GetBookSpec(string genre, decimal from, decimal to)
{
    return new BookSpecification(from, to, genre);
}

public class BookSpecification : ISpecification<Book>
{
    private readonly decimal _from;
    private readonly decimal _to;
    private readonly string _genre;

    public BookSpecification(decimal from, decimal to, string genre)
    {
        _from = from;
        _to = to;
        _genre = genre;
    }

    public Expression<Func<Book, bool>> SatisfiedBy()
    {
        return new GenreSpecification(_genre).AND(new PriceRangeSpecification(_from, _to)).SatisfiedBy();
    }
}

public class GenreSpecification : ISpecification<Book>
{
    private readonly string _genre;

    public GenreSpecification(string genre)
    {
        _genre = genre.Trim().ToLower();
    }
       
    public Expression<Func<Book, bool>> SatisfiedBy()
    {
        return new Specification<Book>(x => x.Genre.Name.ToLower().Contains(_genre)).SatisfiedBy();
    }
}

public class PriceRangeSpecification : ISpecification<Book>
{
    private readonly decimal _from;
    private readonly decimal _to;

    public PriceRangeSpecification(decimal from, decimal to)
    {
        _from = from;
        _to = to;
    }

    public Expression<Func<Book, bool>> SatisfiedBy()
    {
        return new Specification<Book>(x => x.Price >= _from && x.Price <= _to).SatisfiedBy();
    }
}

No comments: