Большинство паттернов проектирования довольно изощренные. Иногда схема, объясняющая, как работает паттерн и для чего он нужен, может занять несколько страниц. Что отличает паттерн "Спецификация", придуманный Мартином Фаулером и Эриком Эвансом, так это его простота. Его и паттерном-то назвать трудно, так, паттерныш какой-то, однако польза от него несомненная. Применить его можно практически в любом приложении.
Для примера возьмем излюбленный пример системы учета заказов. В системе имеются объекты заказчик (Customer) и заказ (Order).
Некоторые заказы требуют особого внимания, так как они являются критическими - пусть это будут заказы на сумму, большую миллиона долларов. Это условие выражается в коде
Скорее всего, подобное условие будет дублироваться в коде в нескольких местах. Чтобы избежать дублирования, введем спецификацию CriticalOrder.
Начнем, как всегда, с написания теста:
И собственно имплементация
Какой прикуп мы получили от введения спецификации? Имеет ли смысл ли вводить новый класс из-за какого-то одного условия? Возможно, не всегда, но в этом случае имеет. Ценность спецификации в том, что она
Преймущества инкапсуляции, я думаю, объяснять не надо. Бизнес-правила могут измениться, а лучше сказать, что они точно изменятся. Например, в следующей итерации нужно будет выделить крупных заказчиков в особую категорию "KeyCustomers" и считать критическими все заказы этой категории, независимо от суммы. Или по прежнему зависимо, но от другой величины, например $200 000 (сказываются последствия кризиса). Можно сделать спецификацию параметризуемой пределом суммы:
Композитные спецификации
Часто возникает задача выборки элементов, отвечающим каким-то условиям. К примеру, может понадобиться выбрать все заказы определенного заказчика или все заказы за год или все заказы в Россию и так далее. Можно было бы сделать несколько методов в фасаде слоя доступа к данным (Data Access Layer = DAL) типа GetOrdersByCustomer(Customer), GetOrdersByYear(int), GetOrdersByCountry(string) и т.д. А что делать, если условия нужно комбинировать, например получить все заказы в Россию за год? Если разных вариантов запросов много, интерфейс DAL будет неоправданно разрастаться. Можно эту проблему решить с помощью паттерна Query, а можно использовать спецификации.
Спецификация легко комбинируется с паттерном Composite, что позволяет объединять спецификации в сложные условия. В этом случае все спецификации будут наследоваться от базового класса Specification<T>
Это позволят создавать сложные спецификации. Вот эта, к примеру, выберет все критические заказы в Россию в 2009 году:
Фасад DAL в этом случае может иметь всего лишь один метод, принимающий спецификацию и возвращающий список объектов соответствующего типа
Чтобы получить заказы, удовлетворяющие нашей спецификации, достаточно вызвать
Спецификации валидации
Спецификации удобно использовать для проверки валидности объекта. К примеру, заказ должен удовлетворять требованиям:
Хотелось бы, помимо собственно проверки, валиден объект или нет, иметь возможность показать пользователю внятное сообщение, что именно не в порядке. Естественно, сообщение будет зависеть от того, какое именно условие нарушено. Если такое требование возникает в нескольких местах (а как правило, так оно и есть), имеет смысл выделить базовый класс для спецификаций, отвечающих за валидацию.
Теперь мы можем создавать валидационные спецификации для каждого случая:
Теперь можно построить композитную спецификацию ValidOrder
которую можно использовать в нескольких местах:
Интеграция с расширенными методами .Net
Добавление к базовому классу Specification явного преобразования к Predicate
волшебным образом позволяет легко и просто использовать спецификации в расширенных методах:
Что мы узнали?
Спецификация - это абстракция условия, накладываемое на объект. Типичные области применения
Спецификация часто может использоваться совместно с паттерном Composite, что позволяет создавать сложные вложенные спецификации. Спецификации легко использовать в расширенных методах языков .Net
Огромное спасибо тем, кто помогал при создании этого материала:
Стас Неверов
Женя Сорокин
Дима Новоселов
Ссылки
/sites/default/files/m/4/2/c/spec.pdf
Уфф :)
Для примера возьмем излюбленный пример системы учета заказов. В системе имеются объекты заказчик (Customer) и заказ (Order).
Некоторые заказы требуют особого внимания, так как они являются критическими - пусть это будут заказы на сумму, большую миллиона долларов. Это условие выражается в коде
order.Amount >= 1000000MСкорее всего, подобное условие будет дублироваться в коде в нескольких местах. Чтобы избежать дублирования, введем спецификацию CriticalOrder.
Начнем, как всегда, с написания теста:
[TestClass]
public class SpecificationTests {
[TestMethod]
public void CriticalOrderTest() {
var specification = new CriticalOrder();
Assert.IsFalse(specification.IsSatisfiedBy(new Order() {Amount = 0}));
Assert.IsFalse(specification.IsSatisfiedBy(new Order() {Amount = 999999}));
Assert.IsTrue(specification.IsSatisfiedBy (new Order() {Amount = 1000000}));
Assert.IsTrue(specification.IsSatisfiedBy (new Order() {Amount = 2000000}));
}
}И собственно имплементация
public class CriticalOrder {
public bool IsSatisfiedBy(Order order) {
return order.Amount >= 1000000;
}
}Какой прикуп мы получили от введения спецификации? Имеет ли смысл ли вводить новый класс из-за какого-то одного условия? Возможно, не всегда, но в этом случае имеет. Ценность спецификации в том, что она
- Отделяет логику условия от объекта, хранящего данные
- Легко тестируется в изоляции
- Помогает избежать дублирования одинаковых условий
- Дает условиям понятные мнемонические имена, отражающие их бизнес смысл
Преймущества инкапсуляции, я думаю, объяснять не надо. Бизнес-правила могут измениться, а лучше сказать, что они точно изменятся. Например, в следующей итерации нужно будет выделить крупных заказчиков в особую категорию "KeyCustomers" и считать критическими все заказы этой категории, независимо от суммы. Или по прежнему зависимо, но от другой величины, например $200 000 (сказываются последствия кризиса). Можно сделать спецификацию параметризуемой пределом суммы:
[TestClass]
public class SpecificationTests {
[TestMethod]
public void CriticalOrderTest() {
var specification = new CriticalOrder(200000);
var keyCustomer = new Customer {Type = CustomerType.KeyCustomer};
var otherCustomer = new Customer {Type = CustomerType.Other};
Assert.IsFalse(specification.IsSatisfiedBy(new Order(keyCustomer) {Amount = 0}));
Assert.IsFalse(specification.IsSatisfiedBy(new Order(keyCustomer) {Amount = 199999}));
Assert.IsTrue(specification.IsSatisfiedBy(new Order(keyCustomer) {Amount = 200000}));
Assert.IsFalse(specification.IsSatisfiedBy(new Order(otherCustomer) {Amount = 200000}));
}
}public class CriticalOrder {
private readonly int minimalAmount;
public CriticalOrder(int minimalAmount) {
this.minimalAmount = minimalAmount;
}
public bool IsSatisfiedBy(Order order) {
return order.Customer.Type == CustomerType.KeyCustomer && order.Amount >= minimalAmount;
}
}Композитные спецификации
Часто возникает задача выборки элементов, отвечающим каким-то условиям. К примеру, может понадобиться выбрать все заказы определенного заказчика или все заказы за год или все заказы в Россию и так далее. Можно было бы сделать несколько методов в фасаде слоя доступа к данным (Data Access Layer = DAL) типа GetOrdersByCustomer(Customer), GetOrdersByYear(int), GetOrdersByCountry(string) и т.д. А что делать, если условия нужно комбинировать, например получить все заказы в Россию за год? Если разных вариантов запросов много, интерфейс DAL будет неоправданно разрастаться. Можно эту проблему решить с помощью паттерна Query, а можно использовать спецификации.
Спецификация легко комбинируется с паттерном Composite, что позволяет объединять спецификации в сложные условия. В этом случае все спецификации будут наследоваться от базового класса Specification<T>
public abstract class Specification<T> {
public abstract bool IsSatisfiedBy(T item);
public static Specification<T> operator !(Specification<T> specification) {
return new NotSpecification<T>(specification);
}
public static Specification<T> operator |(Specification<T> left, Specification<T> right) {
return new OrSpecification<T>(left, right);
}
public static Specification<T> operator &(Specification<T> left, Specification<T> right) {
return new AndSpecification<T>(left, right);
}
}
public abstract class CompositeSpecification<T> : Specification<T> {
protected readonly List<Specification<T>> specifications;
protected CompositeSpecification(params Specification<T>[] specifications) {
this.specifications = new List<Specification<T>>(specifications);
}
public ReadOnlyCollection<Specification<T>> Specifications {
get { return specifications.AsReadOnly(); }
}
public void Add(Specification<T> specification) {
specifications.Add(specification);
}
public void Remove(Specification<T> specification) {
specifications.Remove(specification);
}
}
public class AndSpecification<T> : CompositeSpecification<T> {
public AndSpecification() {}
public AndSpecification(params Specification<T>[] specifications) : base(specifications) {}
public override bool IsSatisfiedBy(T item) {
foreach (var specification in specifications) {
if (!specification.IsSatisfiedBy(item)) {
return false;
}
}
return true;
}
}
public class OrSpecification<T> : CompositeSpecification<T> {
public OrSpecification() {}
public OrSpecification(params Specification<T>[] specifications) : base(specifications) {}
public override bool IsSatisfiedBy(T item) {
foreach (var specification in specifications) {
if (specification.IsSatisfiedBy(item)) {
return true;
}
}
return false;
}
}
public class NotSpecification<T> : Specification<T> {
private readonly Specification<T> specification;
public Specification<T> Specification {
get { return specification; }
}
public NotSpecification(Specification<T> specification) {
this.specification = specification;
}
public override bool IsSatisfiedBy(T item) {
return !specification.IsSatisfiedBy(item);
}
}
Это позволят создавать сложные спецификации. Вот эта, к примеру, выберет все критические заказы в Россию в 2009 году:
var specification =
new CriticalOrder(CustomerType.KeyCustomer, 200000) &
new SoldTo(new Country("Russia")) &
new AsOf(2009);
Фасад DAL в этом случае может иметь всего лишь один метод, принимающий спецификацию и возвращающий список объектов соответствующего типа
public List<T> Get<T>(Specification<T> specification);
Чтобы получить заказы, удовлетворяющие нашей спецификации, достаточно вызвать
infrastructureFacade.Get(specification);
Спецификации валидации
Спецификации удобно использовать для проверки валидности объекта. К примеру, заказ должен удовлетворять требованиям:
- Сумма от $0 до $10 000 000
- Дата заказа >= 1 Января 2008
- Дата отгрузки >= Даты заказа
- Дата оплаты >= Даты заказа
Хотелось бы, помимо собственно проверки, валиден объект или нет, иметь возможность показать пользователю внятное сообщение, что именно не в порядке. Естественно, сообщение будет зависеть от того, какое именно условие нарушено. Если такое требование возникает в нескольких местах (а как правило, так оно и есть), имеет смысл выделить базовый класс для спецификаций, отвечающих за валидацию.
public abstract class ValidationSpecification<T> : Specification<T> {
public string Validate(T item) {
return IsSatisfiedBy(item) ? string.Empty : BuildErrorMessage(item);
}
protected abstract string BuildErrorMessage(T item);
}
Теперь мы можем создавать валидационные спецификации для каждого случая:
public class ValidAmount : ValidationSpecification {
public override bool IsSatisfiedBy(Order order) {
return order.Amount >= 0 && order.Amount < 10000000;
}
public override string BuildErrorMessage(Order order) {
return "Сумма заказа должна лежать в пределах от 0 до 10000000.";
}
}
public class ValidDate : ValidationSpecification {
public override bool IsSatisfiedBy(Order order) {
return order.Date >= new DateTime(2008, 1, 1);
}
public override string BuildErrorMessage(Order order) {
return "Дата заказа должна быть позже 1 Января 2008.";
}
}
public class ValidShipDate : ValidationSpecification {
public override bool IsSatisfiedBy(Order order) {
return order.ShipDate >= order.Date;
}
public override string BuildErrorMessage(Order order) {
return string.Format("Дата отгрузки {0} не может быть меньше даты заказа {1}", order.ShipDate, order.Date);
}
}
public class ValidPayDate : ValidationSpecification {
public override bool IsSatisfiedBy(Order order) {
return order.PayDate >= order.Date;
}
public override string BuildErrorMessage(Order order) {
return string.Format("Дата оплаты {0} не может быть меньше даты заказа {1}", order.PayDate, order.Date);
}
}
Теперь можно построить композитную спецификацию ValidOrder
public class ValidOrder : AndSpecification {
public ValidOrder() : base(
new ValidAmount(),
new ValidDate(),
new ValidShipDate(),
new ValidPayDate()
) {}
}
которую можно использовать в нескольких местах:
- На экране при создании/редактировании заказа
- При массовом импорте из внешней системы
- При реконструировании из базы данных
Интеграция с расширенными методами .Net
Добавление к базовому классу Specification явного преобразования к Predicate
public static implicit operator Predicate(Specification specification) {
return specification.IsSatisfiedBy;
}волшебным образом позволяет легко и просто использовать спецификации в расширенных методах:
orders.FindAll(new CriticalOrder());
Что мы узнали?
Спецификация - это абстракция условия, накладываемое на объект. Типичные области применения
- Фильтрация, например при получении списка объектов, удовлетворяющих некоторому условию
- Валидация, например перед сохранением нужно проверить, что объект находится в корректном состоянии
Спецификация часто может использоваться совместно с паттерном Composite, что позволяет создавать сложные вложенные спецификации. Спецификации легко использовать в расширенных методах языков .Net
Огромное спасибо тем, кто помогал при создании этого материала:
Стас Неверов
Женя Сорокин
Дима Новоселов
Ссылки
/sites/default/files/m/4/2/c/spec.pdf
Уфф :)
