Специфікація — це шаблон проєктування, який представляє бізнес логіку у вигляді ланцюжка об'єктів зв'язних операцій булевої логіки.
Додамо деякі класи, які будуть симулювати реальні об'єкти.
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);
}