Специфікація (шаблон проєктування)

Специфікація — це шаблон проєктування, який представляє бізнес логіку у вигляді ланцюжка об'єктів зв'язних операцій булевої логіки.
- логіка фільтрації об'єктів винесена в окремі класи-специфікацій, які можна, без втрат в гнучкості системи, об'єднювати між собою
- важкий в реалізації
Додамо деякі класи, які будуть симулювати реальні об'єкти.
public class User { public string Name { get; set; } public bool IsAdmin { get; set; } public override string ToString() { return $"{Name}. Admin = {IsAdmin}"; } }
Запишемо стандартну реалізацію, яку згодом покращимо для конкретної мови програмування.
public interface ISpecification<TEntity> { bool IsSatisfiedBy(TEntity entity); // об'єднання ISpecification<TEntity> And(ISpecification<TEntity> other); ISpecification<TEntity> Or(ISpecification<TEntity> other); ISpecification<TEntity> Not(); }
Додамо абстрактний клас, який дозволить нам об'єднювати специфікації в ланцюжки за допомогою операторів булевої логіки. У C# цей клас можна замінити на перевантаження операцій чи методами розширень до ISpecification.
public abstract class CompositeSpecification<TEntity> : ISpecification<TEntity> { public abstract bool IsSatisfiedBy(TEntity entity); public ISpecification<TEntity> And(ISpecification<TEntity> other) { return new AndSpecification<TEntity>(this, other); } public ISpecification<TEntity> Or(ISpecification<TEntity> other) { return new OrSpecification<TEntity>(this, other); } public ISpecification<TEntity> Not() { return new NotSpecification<TEntity>(this); } }
Реалізацій конкретних декораторів
public class AndSpecification<TEntity> : CompositeSpecification<TEntity> { private readonly ISpecification<TEntity> spec1; private readonly ISpecification<TEntity> spec2; public AndSpecification(ISpecification<TEntity> spec1, ISpecification<TEntity> spec2) { this.spec1 = spec1; this.spec2 = spec2; } public override bool IsSatisfiedBy(TEntity candidate) { return spec1.IsSatisfiedBy(candidate) && spec2.IsSatisfiedBy(candidate); } } public class OrSpecification<TEntity> : CompositeSpecification<TEntity> { private readonly ISpecification<TEntity> spec1; private readonly ISpecification<TEntity> spec2; public OrSpecification(ISpecification<TEntity> spec1, ISpecification<TEntity> spec2) { this.spec1 = spec1; this.spec2 = spec2; } public override bool IsSatisfiedBy(TEntity candidate) { return spec1.IsSatisfiedBy(candidate) || spec2.IsSatisfiedBy(candidate); } } public class NotSpecification<TEntity> : CompositeSpecification<TEntity> { private readonly ISpecification<TEntity> wrapped; public NotSpecification(ISpecification<TEntity> spec) { wrapped = spec; } public override bool IsSatisfiedBy(TEntity candidate) { return !wrapped.IsSatisfiedBy(candidate); } }
Припустимо, що виникли наступні задачі:
- знайти користувачів, за їх статусом
- знайти користувачів по імені, за введеним значенням
Тоді конкретні специфікації матимуть наступний вигляд
public class RoleSpecification : CompositeSpecification<User> { private readonly bool isUserAdmin; public RoleSpecification(bool isUserAdmin) { this.isUserAdmin = isUserAdmin; } public override bool IsSatisfiedBy(User entity) { return entity.IsAdmin == isUserAdmin; } } public class SearchByNameSpecification : CompositeSpecification<User> { private readonly string searchSubstring; public SearchByNameSpecification(string searchSubstring) { this.searchSubstring = searchSubstring; } public override bool IsSatisfiedBy(User entity) { return entity.Name.Contains(searchSubstring); } }
Використання матиме наступний вигляд:
// задана предметна область User[] users = new User[] { new User { IsAdmin = false, Name = "User 1" }, new User { IsAdmin = false, Name = "User 2" }, new User { IsAdmin = true, Name = "User 3" }, }; // конкретні специфікації ISpecification<User> roleSpecification = new RoleSpecification(isUserAdmin: false); ISpecification<User> nameSpecification = new SearchByNameSpecification(searchSubstring: "User"); // композиції специфікації ISpecification<User> andSpecification = nameSpecification.And(roleSpecification); ISpecification<User> orSpecification = nameSpecification.Or(roleSpecification); // результати вибірки Console.WriteLine("AND Specification"); foreach (User user in users) { if (andSpecification.IsSatisfiedBy(user)) { Console.WriteLine(user); } } Console.WriteLine("OR Specification"); foreach (User user in users) { if (orSpecification.IsSatisfiedBy(user)) { Console.WriteLine(user); } }
При використанні із LINQ специфікації можна обгортати у функції, або ж забезпечити специфікації такою функціональністю:
// інтерфейс public interface ISpecification<TEntity> { bool IsSatisfiedBy(TEntity entity); Func<TEntity, bool> AsExpression(); . . . } // абстрактний клас public abstract class CompositeSpecification<TEntity> : ISpecification<TEntity> { public abstract Func<TEntity, bool> AsExpression(); public bool IsSatisfiedBy(TEntity entity) => AsExpression().Invoke(entity); . . . } // оператори булевої логіки public class AndSpecification<TEntity> : CompositeSpecification<TEntity> { . . . public override Func<TEntity, bool> AsExpression() { return (entity) => spec1.IsSatisfiedBy(entity) && spec2.IsSatisfiedBy(entity); } } // конкретні специфікації public class RoleSpecification : CompositeSpecification<User> { private readonly Func<User, bool> isUserAdminPredicate; public RoleSpecification(bool isUserAdmin) { this.isUserAdminPredicate = (user) => user.IsAdmin == isUserAdmin; } public override Func<User, bool> AsExpression() { return isUserAdminPredicate; } } // використання foreach (User user in users.Where(specification.AsExpression())) { Console.WriteLine(user); }
- Патерн проєктування Специфікація [Архівовано 19 вересня 2013 у Wayback Machine.]
- Специфікація, як заміна репозиторію [Архівовано 2 вересня 2019 у Wayback Machine.]