Design Specification Template

    When attempting to comprehend DDD, you are likely to come across this pattern, which is often closely used together with another, no less interesting, Repository pattern. This pattern provides the ability to describe requirements for business objects, and then use them (and their compositions) for filtering without duplicating queries.

    Example


    For example, let's design a domain for simple group chat: we will have three entities: Group and User, between which there are many-to-many relationships (one user can be in different groups, there can be several users in a group) and Message representing a message, which the user can write in any group:

    public class Member
    {
        public string Nick { get; set; }
        public string Country { get; set; }
        public int Age { get; set; }
        public ICollection Groups { get; set; }
        public ICollection Messages { get; set; }
    }
    public class Message
    {
        public string Body { get; set; }
        public DateTime Timestamp { get; set; }
        public Member Author { get; set; }
    }
    public class Group
    {
        public string Name { get; set; }
        public string Subject { get; set; }
        public Member Owner { get; set; }
        public ICollection Messages { get; set; }
        public ICollection Members { get; set; }
    }
    

    Now imagine that you are writing two methods in the Application Service:
    /// 
    /// Все участники из заданой группы, указавшие заданую страну в профиле
    /// 
    public ICollection GetMembersInGroupFromCountry(string groupName, string country)
    {
    }
    /// 
    /// Все участники из заданой группы, которые не написал ни одного сообщения в указаный период времени
    /// 
    public ICollection GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
    {
    }


    Implementation 1 (bad):


    You can let the services build queries on top of the repository themselves:
    public ICollection GetMembersInGroupFromCountry(string groupName, string country)
    {	
        return _membersRepository.Query.Where(m => m.Groups.Any(g => g.Name == groupName) && m.Country == country);
    }
    public ICollection GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
    { 
        return _membersRepository.Query.Where(m => m.Groups.Any(g => g.Name == groupName) && 
          !m.Messages.Any(msg => msg.Timestamp > start && msg.Timestamp < end));
    }
    


    Cons:
    Opening the request object outward for the repository is comparable to opening a weapons trading store without requiring a license from buyers - you just can’t keep track of everyone and someone will definitely shoot someone. I will decipher the analogy: you will almost certainly have a lot of similar queries on top of Query in different parts of the service (s) and if you decide to add a new field (for example, filter groups by the IsDeleted attribute, and users by the IsBanned attribute) and take it into account for many samples - you run the risk of missing a method.

    Implementation 2 (not bad):


    You can simply describe in the repository contract all the methods that are needed for services by hiding filtering in them, the implementation will look like this:
    public ICollection GetMembersInGroupFromCountry(string groupName, string country)
    {
        return _membersRepository. GetMembersInGroupFromCountry(groupName, country);
    }
    public ICollection GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
    { 
        return _membersRepository. GetInactiveMembersInGroupOnDateRange(groupName, start, end);
    }
    


    Cons:
    This implementation is free from the drawback of the first, but has its own - gradually your repository will grow and swell and, in the end, will turn into something weakly supported.

    Implementation 3 (excellent):


    This is where the Specification pattern comes to the rescue, thanks to which our code will look like this:
    public ICollection GetMembersInGroupFromCountry(string groupName, string country)
    {
        return _membersRepository.AllMatching(MemberSpecifications.Group(groupName) && 
            MemberSpecs.FromCountry(country));
    }
    public ICollection GetInactiveMembersInGroupOnDateRange(string groupName, DateTime start, DateTime end)
    { 
        return _membersRepository.AllMatching(MemberSpecifications.Group(groupName) && 
            MemberSpecs.InactiveInDateRange(start, end));
    }
    


    We get a double profit: the repository is clean like a baby’s tear and does not swell, and in services there is no duplication of requests and there is no risk of missing a condition somewhere, for example, if you decide to filter groups everywhere by the IsDeleted attribute, you just need to add this condition once in the MemberSpecs specification .FromGroup

    Pattern implementation


    Martin Fowler (and Eric Evans) proposed the following specification interface:
    public abstract class Specification
    {
        public abstract bool IsSatisfiedBy(T entity);
    }
    

    The guys from linqspecs.codeplex.com made it more repository friendly (its specific implementations based on EF, nHibernate, etc.) and even serializable:
    public abstract class Specification
    {
        public abstract Expression> IsSatisfiedBy();
    }
    


    Thanks to ExpressionTree, the repository can parse the expression and translate it into SQL or something else. The basic implementation with the basic logical elements looks like this:



    Now we just have to write a static class containing specifications for each entity (aggregate). For our example, it will look like this:
    public static class MemberSpecifications
    {
        public static Specification Group(string groupName)
        {
            return new DirectSpecification(member =>
                member.Groups.Any(g => g.Name == groupName));
        }
        public static Specification Country(string country)
        {
            return new DirectSpecification(member =>
                member.Country == country);
        }
        public static Specification InactiveSinceTo(DateTime start, DateTime end)
        {
            return new DirectSpecification(member =>
                member.Messages.Any(msg => msg.Timestamp > start && msg.Timestamp < end));
        }
    }
    


    Conclusion


    That's the way we got rid of duplication of conditions even in our small example, but you can really appreciate this on large projects with complex logic. This is the trouble of DDD, that it is always explained with small examples and it is difficult to get rid of the idea that all this is over-engineering.

    References


    A great example of where the implementation of the specifications came from is here . I also draw attention to this article on the hub with a list of literature on DDD.

    Also popular now: