Encapsulating Criteria Using Specifications
"Specification provides a concise way of expressing certain kinds of rules, extricating them from conditional logic and making them explicit in the model."
Eric Evans : Domain Driven Design
In our domain model we have a normal Customer object that looks like this:
public class Customer
{
private DateTime signUpDate;
private string firstName;
private string lastName;
public Customer(DateTime signUpDate, string firstName, string lastName)
{
this.signUpDate = signUpDate;
this.firstName = firstName;
this.lastName = lastName;
}
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
public DateTime SignUpDate
{
get { return signUpDate; }
}
public bool IsAnActiveCustomer()
{
IRange<DateTime> activeRange = new Range<DateTime>(this.signUpDate, this.signUpDate.AddYears(1));
return activeRange.Contains(DateTime.Now);
}
}
With this code, we have defined a customer as something with a first name, last name, and a signup date. We have also specified (using the IsAnActiveCustomer method) that an active customer is a customer who has signed up with us within the current year.
This method that determines whether or not a customer is active as it stands isn't very flexible. To make the method more flexible we can extract the criteria that determines whether or not a customer is active or not and add it as a parameter to the IsAnActiveCustomer method so that the criteria can be passed in.
In order to encapsulate the active customer we can leverage the specification pattern. Our first attempt at making an active customer specification might look something like this:
public interface ISpecification
{
bool IsSatisfiedBy(Customer customer);
}
public class ActiveCustomerSpecification : ISpecification
{
public bool IsSatisfiedBy(Customer customer)
{
IRange<DateTime> activeRange = new Range<DateTime>(customer.SignUpDate, customer.SignUpDate.AddYears(1));
return activeRange.Contains(DateTime.Now);
}
}
and in the customer class:
public bool IsAnActiveCustomer(ISpecification activeCustomerSpecification)
{
return activeCustomerSpecification.IsSatisfiedBy(this);
}
By adding the specification as a parameter to the IsAnActiveCustomer method we have inverted the control of the definition of an active customer to consumers of the customer class. There may be different definitions of what an active customer is in different situations.
We can make one more improvement to the specification class before moving on. What happens if another class wants to leverage the specification interface? They have to create a new specification for their new type...or they can use generics to save themselves the time of writing a new specification interface for every new type. Here is the result of our change:
public interface ISpecification<T>
{
bool IsSatisfiedBy(T item);
}
public class ActiveCustomerSpecification : ISpecification<Customer>
{
public bool IsSatisfiedBy(Customer customer)
{
IRange<DateTime> activeRange = new Range<DateTime>(customer.SignUpDate, customer.SignUpDate.AddYears(1));
return activeRange.Contains(DateTime.Now);
}
}
public bool IsAnActiveCustomer(ISpecification<Customer> activeCustomerSpecification)
{
return activeCustomerSpecification.IsSatisfiedBy(this);
}