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