Specification
Specification Pattern
Overview
The Specification Pattern is a software design pattern used to encapsulate business rules, logic, or criteria into a reusable, combinable, and testable format. It provides a clear and modular way to evaluate whether objects meet certain conditions. This pattern is particularly useful in domains with complex validation or filtering requirements.
Key Concepts
- Encapsulation of Criteria: Encapsulates rules or conditions into specification objects.
- Composability: Allows combining multiple specifications using logical operators such as `AND`, `OR`, and `NOT`.
- Reusability: Specifications can be reused across different parts of an application.
Benefits
- Improves code readability and maintainability.
- Promotes single responsibility by decoupling business rules from domain objects.
- Enables dynamic criteria evaluation at runtime.
- Simplifies unit testing for business rules.
Example Implementation
Below is an example of the Specification Pattern in C# for a domain involving filtering Products.
using System;
using System.Linq.Expressions;
// Specification Interface
public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
ISpecification<T> And(ISpecification<T> other);
ISpecification<T> Or(ISpecification<T> other);
ISpecification<T> Not();
}
// Base Specification Class
public abstract class Specification<T> : ISpecification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
{
var predicate = ToExpression().Compile();
return predicate(entity);
}
public ISpecification<T> And(ISpecification<T> other) =>
new AndSpecification<T>(this, other);
public ISpecification<T> Or(ISpecification<T> other) =>
new OrSpecification<T>(this, other);
public ISpecification<T> Not() =>
new NotSpecification<T>(this);
}
// Concrete Specification
public class ProductHasMinimumPriceSpecification : Specification<Product>
{
private readonly decimal _minPrice;
public ProductHasMinimumPriceSpecification(decimal minPrice)
{
_minPrice = minPrice;
}
public override Expression<Func<Product, bool>> ToExpression()
{
return product => product.Price >= _minPrice;
}
}
// Example of Combining Specifications
public class AndSpecification<T> : Specification<T>
{
private readonly ISpecification<T> _left;
private readonly ISpecification<T> _right;
public AndSpecification(ISpecification<T> left, ISpecification<T> right)
{
_left = left;
_right = right;
}
public override Expression<Func<T, bool>> ToExpression()
{
var leftExpression = _left.ToExpression();
var rightExpression = _right.ToExpression();
var parameter = Expression.Parameter(typeof(T));
var body = Expression.AndAlso(
Expression.Invoke(leftExpression, parameter),
Expression.Invoke(rightExpression, parameter));
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
}
// Example Usage
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Program
{
public static void Main()
{
var expensiveProductSpec = new ProductHasMinimumPriceSpecification(100);
var product = new Product { Name = "Laptop", Price = 150 };
if (expensiveProductSpec.IsSatisfiedBy(product))
{
Console.WriteLine($"{product.Name} meets the criteria.");
}
else
{
Console.WriteLine($"{product.Name} does not meet the criteria.");
}
}
}
Advantages
- Decouples complex rules from business entities.
- Supports runtime evaluation and dynamic rule composition.
- Encourages clean architecture and separation of concerns.
Use Cases
- Validation: Checking whether an object satisfies certain rules.
- Filtering: Querying data with complex criteria.
- Authorization: Determining user permissions based on rules.
Related Patterns
- Strategy Pattern: Both patterns encapsulate behavior but serve different purposes.
- Interpreter Pattern: Both patterns deal with logic encapsulation, though Specification focuses on evaluation rather than translation.