Thursday, April 24, 2008

Specification Pattern

The Specification pattern is a very powerful design pattern which can be used to remove a lot of cruft from a class’s interface while decreasing coupling and increasing extensibility. It’s primary use is to select a subset of objects based on some criteria, and to refresh the selection at various times. Take a look at the example below:

Interface: IUser
1
2
3
4
5
6
7
8
9
namespace Specification
{
    public interface IUser
    {
        string FirstName { get; set; }
        string LastName { get; set; }
        int Age { get; set; }
    }
}
Class: SelectFromList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace Specification
{
    public class SelectFromList
    {
        private readonly List<IUser> _Users;

        public SelectFromList()
        {
            _Users = new List<IUser>();
            _Users.Add(new User() { FirstName="Mark", LastName="Nijhof", Age=31 });
            _Users.Add(new User() { FirstName = "Mona", LastName = "Nijhof", Age = 30 });
            _Users.Add(new User() { FirstName = "Milo", LastName = "Nijhof", Age = 2 });
            _Users.Add(new User() { FirstName = "Thalia", LastName = "Nijhof", Age = 1 });
        }

        public List<IUser> GetUsers()
        {
            List<IUser> result = new List<IUser>();
            foreach (IUser user in _Users)
            {
                if (user.FirstName.StartsWith("M") &&
                    (user.Age > 10 || user.Age < 2))
                {
                    result.Add(user);
                }
            }
            return (result);
        }
    }
}
Below here you will see some preparation for the Specification Pattern, this might look like a lot of work but when dealing with a lot of these selection statements you will soon see the benefit of it, especially when you need to make changes to what you are selecting on.
Interface: ISpecification
1
2
3
4
5
6
7
8
9
10
11
namespace Specification
{
    public interface ISpecification
    {
        bool IsSatisfiedBy(IUser candidate);

        ISpecification And(ISpecification other);
        ISpecification Or(ISpecification other);
        ISpecification Not(ISpecification other);
    }
}
Class: Specification : ISpecification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Specification
{
    public class Specification : ISpecification
    {
        public virtual bool IsSatisfiedBy(IUser candidate)
        {
            throw new NotImplementedException();
        }
        public ISpecification And(ISpecification other)
        {
            return new AndSpecification(this, other);
        }
        public ISpecification Or(ISpecification other)
        {
            return new OrSpecification(this, other);
        }
        public ISpecification Not(ISpecification other)
        {
            return new NotSpecification(this, other);
        }
    }
}
Class: AndSpecification : Specification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace Specification
{
    public class AndSpecification : Specification
    {
        private readonly ISpecification _One;
        private readonly ISpecification _Other;
        
        public AndSpecification(ISpecification one, ISpecification other)
        {
            _One = one;
            _Other = other;
        }

        public override bool IsSatisfiedBy(IUser candidate)
        {
            return _One.IsSatisfiedBy(candidate) && _Other.IsSatisfiedBy(candidate);
        }
    }
}
Class: OrSpecification : Specification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace Specification
{
    public class OrSpecification : Specification
    {
        private readonly ISpecification _One;
        private readonly ISpecification _Other;

        public OrSpecification(ISpecification one, ISpecification other)
        {
            _One = one;
            _Other = other;
        }

        public override bool IsSatisfiedBy(IUser candidate)
        {
            return _One.IsSatisfiedBy(candidate) || _Other.IsSatisfiedBy(candidate);
        }
    }
}
Class: NotSpecification : Specification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace Specification
{
    public class NotSpecification : Specification
    {
        private readonly ISpecification _One;
        private readonly ISpecification _Other;

        public NotSpecification(ISpecification one, ISpecification other)
        {
            _One = one;
            _Other = other;
        }

        public override bool IsSatisfiedBy(IUser candidate)
        {
            return _One.IsSatisfiedBy(candidate) && !_Other.IsSatisfiedBy(candidate);
        }
    }
}
Now take a look at how the Specification Pattern makes this much cleaner:
Class: StartWith : Specification
1
2
3
4
5
6
7
8
9
10
namespace Specification
{
    public class StartWith : Specification
    {
        public override bool IsSatisfiedBy(IUser candidate)
        {
            return (candidate.FirstName.StartsWith("M"));
        }
    }
}
Class: OlderThan : Specification
1
2
3
4
5
6
7
8
9
10
namespace Specification
{
    public class OlderThan : Specification
    {
        public override bool IsSatisfiedBy(IUser candidate)
        {
            return (candidate.Age > 10);
        }
    }
}
Class: JongerThan : Specification
1
2
3
4
5
6
7
8
9
10
namespace Specification
{
    public class JongerThan : Specification
    {
        public override bool IsSatisfiedBy(IUser candidate)
        {
            return (candidate.Age < 2);
        }
    }
}
Class: SelectFromList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace Specification
{
    public class SelectFromList
    {
        private readonly List<IUser> _Users;

        public SelectFromList()
        {
            _Users = new List<IUser>();
            _Users.Add(new User() { FirstName="Mark", LastName="Nijhof", Age=31 });
            _Users.Add(new User() { FirstName = "Mona", LastName = "Nijhof", Age = 30 });
            _Users.Add(new User() { FirstName = "Milo", LastName = "Nijhof", Age = 2 });
            _Users.Add(new User() { FirstName = "Thalia", LastName = "Nijhof", Age = 1 });
        }

        public List<IUser> GetUsers(ISpecification specification)
        {
            List<IUser> result = new List<IUser>();
            foreach (IUser user in _Users)
            {
                if (specification.IsSatisfiedBy(user))
                {
                    result.Add(user);
                }
            }
            return (result);
        }
    }
}
Usage
1
2
3
4
5
ISpecification startsWith = new StartWith();
ISpecification olderThan = new OlderThan();
ISpecification jongerThan = new JongerThan();

SFL.GetUsers(startsWith.And(olderThan.Or(jongerThan)));
When you look at the new SelectFromList class and how this is being used than you can see that it became much easier to extend or change the selection criteria.

I hope you found these examples helpful. Also take a look at the following links:

http://www.martinfowler.com/apsupp/spec.pdf
http://www.mattberther.com/2005/03/25/the-specification-pattern-a-primer/
http://en.wikipedia.org/wiki/Specification_pattern

No comments: