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

Шаблон проєктування у вигляді UML діаграми

Специфікація — це шаблон проєктування, який представляє бізнес логіку у вигляді ланцюжка об'єктів зв'язних операцій булевої логіки.

Переваги та недоліки

[ред. | ред. код]

Переваги

[ред. | ред. код]
  • логіка фільтрації об'єктів винесена в окремі класи-специфікацій, які можна, без втрат в гнучкості системи, об'єднювати між собою

Недоліки

[ред. | ред. код]
  • важкий в реалізації

Опис мовою C#

[ред. | ред. код]

Додамо деякі класи, які будуть симулювати реальні об'єкти.

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);
}

Див. також

[ред. | ред. код]

Джерела

[ред. | ред. код]