Статьи

Функциональные паттерны в доменном моделировании — паттерн спецификации

Когда вы моделируете домен, вы моделируете его сущности и поведение. Как упоминает Эрик Эванс в своей книге «  Дизайн , управляемый доменом» , основное внимание уделяется самому домену. Модель, которую вы разрабатываете и реализуете, должна говорить на вездесущем языке, чтобы сущность домена не терялась из-за множества случайных сложностей, которые обеспечивает ваша реализация. Будучи выразительной, модель должна быть также расширяемой. И когда мы говорим о расширяемости, одним связанным атрибутом является композиционность. 

Функции составляются более естественно, чем объекты, и в этом посте я буду использовать идиомы функционального программирования для реализации одного из шаблонов, которые составляют ядро ​​доменного дизайна —  спецификации шаблон, наиболее распространенный вариант использования которого заключается в реализации проверки домена. Книга Эрика о DDD говорит о шаблоне спецификации.


Он имеет многократное использование, но тот, который передает основную концепцию, заключается в том, что СПЕЦИФИКАЦИЯ может проверить любой объект, чтобы увидеть, удовлетворяет ли он указанным критериям.

Спецификация определяется как предикат, при котором бизнес-правила можно объединять, объединяя их в цепочку с использованием логической логики. Итак, есть концепция композиции, и мы можем говорить о Composite Specification, когда будем говорить об этом шаблоне. В различной литературе по DDD это реализовано с использованием шаблона Composite, который обычно реализуется с использованием иерархий классов и композиции. В этом посте мы будем использовать функцию композиции вместо.

Спецификация — Где?

Одно из самых распространенных заблуждений, которые возникают у нас при разработке модели, заключается в том, где хранить код проверки сводного корня или любой сущности.

  • Должны ли мы иметь подтверждение как часть объекта? Нет, это делает объект раздутым. Также проверки могут варьироваться в зависимости от некоторого контекста, в то время как ядро ​​сущности остается тем же.
  • Должны ли мы иметь проверки как часть интерфейса? Может быть, мы используем JSON и строим из него сущности. Действительно,  некоторые  проверки могут принадлежать интерфейсу, и не стесняйтесь размещать их там.
  • Но наиболее интересные проверки — это те, которые относятся к слою домена. Это бизнес-валидации (или спецификации), которые Эрик Эванс определяет как нечто,  «устанавливающее ограничение на состояние другого объекта» . Это бизнес-правила, которые необходимо соблюдать организации, чтобы перейти к следующему этапу обработки.

Рассмотрим следующий простой пример. Мы берем  Order сущность, и модель идентифицирует следующие «спецификации» домена, которые Order должны удовлетворять новые,  прежде чем они будут брошены в конвейер обработки:

  1. это должен быть  действительный  заказ, подчиняющийся ограничениям, которые требует домен, например, действительная дата, действительное количество позиций и т. д.
  2. он должен быть  утвержден  действующим утверждающим органом — только тогда он переходит к следующему этапу конвейера
  3. необходимо проверить статус клиента, чтобы убедиться, что клиент не занесен в черный список
  4. отдельные позиции из заказа должны быть проверены по инвентарю, чтобы увидеть, можно ли выполнить заказ

Это отдельные шаги, которые должны быть последовательно выполнены конвейером обработки заказов в качестве предварительных проверок перед тем, как фактический заказ будет готов к выполнению. Сбой в любом из них выводит заказ из конвейера, и процесс останавливается там. Таким образом, модель, которую мы разработаем, должна учитывать последовательность, а также проверять все ограничения, которые необходимо выполнить как часть каждого шага.

Здесь важно отметить, что ни один из вышеперечисленных шагов не изменяет порядок, поэтому каждая спецификация получает в Order качестве входных данных копию исходного  объекта, в которой она проверяет некоторые правила домена и определяет, подходит ли она для передачи на следующий шаг. трубопровод.

Переходя к реализации ..

Давайте возьмем некоторые замечания по реализации из того, что мы узнали выше ..

  • Order Может быть непреложным лицо по крайней мере , для этой последовательности операций
  • Каждой спецификации нужен порядок, можем ли мы вытащить какой-то трюк из нашей шляпы, который предотвращает это загромождение API, передавая  Order экземпляр каждой спецификации в последовательности?
  • Поскольку мы планируем использовать принципы функционального программирования, как мы можем смоделировать вышеуказанную последовательность как  выражение,  чтобы наш конечный результат все еще оставался совместимым со следующим процессом выполнения заказа (который мы обсудим в будущем посте)?
  • Все эти функции выглядят как имеющие одинаковые подписи — нам нужно заставить их сочиняться друг с другом

Прежде чем я представлю более подробное объяснение или теорию, вот основные строительные блоки, которые будут реализовывать замечания, которые мы сделали после разговора с экспертами в области.

?
type ValidationStatus[S] = \/[String, S]
type ReaderTStatus = ReaderT[ValidationStatus, A, S]
object ReaderTStatus extends KleisliInstances with KleisliFunctions {
def apply[A, S](f: A => ValidationStatus[S]): ReaderTStatus[A, S] = kleisli(f)
}

ValidationStatus определяет тип, который мы будем возвращать из каждой функции. Это либо какой-то статус,  S либо строка ошибки, которая объясняет, что пошло не так. На самом деле это  Either тип (с правым смещением), реализованный в [a href = «https://github.com/scalaz/scalaz» style = «text-ornament: none; color: rgb (136, 136, 136); font- семейство: Arial, Tahoma, Helvetica, FreeSans, без засечек; размер шрифта: 13px; высота строки: 18.479999542236328px; «] scalaz.

Одна из вещей, которые мы сочли крутыми, — это избегать повторения  Order параметра для каждого метода при вызове последовательности. И один из идиотских способов сделать это — использовать монаду Reader. Но здесь у нас уже есть монада —  \/ это монада. Поэтому нам нужно сложить их вместе, используя монадный преобразователь.  ReaderT делает эту работу и ReaderTStatus определяет тип, который каким-то образом облегчает нашу жизнь, объединяя их.

Следующим шагом является реализация  ReaderTStatus, которую мы делаем в терминах другой называемой абстракции  Kleisli. Для этого мы будем использовать библиотеку scalaz, которая реализуется  ReaderT в терминах  Kleisli. Я не буду вдаваться в подробности этой реализации — в случае , если вам интересно, обратитесь к этой прекрасной  части  Евгений.

Итак, как же выглядит одна примерная спецификация?

Прежде чем углубляться в это, вот несколько основных абстракций, упрощенно только для иллюстрации.

?
// the base abstraction
sealed trait Item {
def itemCode: String
}
// sample implementations
case class ItemA(itemCode: String, desc: Option[String],
minPurchaseUnit: Int) extends Item
case class ItemB(itemCode: String, desc: Option[String],
nutritionInfo: String) extends Item
case class LineItem(item: Item, quantity: Int)
case class Customer(custId: String, name: String, category: Int)
// a skeleton order
case class Order(orderNo: String, orderDate: Date, customer: Customer, 
lineItems: List[LineItem])

And here’s a specification that checks some of the constraints on the Order object ..

?
// a basic validation
private def validate = ReaderTStatus[Order, Boolean] {order =>
if (order.lineItems isEmpty) left(s"Validation failed for order $order")
else right(true)
}

It’s just for illustration and does not contain much domain rules. The important part is how we use the above defined types to implement the function. Order is not an explicit argument to the function — it’s curried. The function returns a ReaderTStatus, which itself is a monad and hence allows us to sequence in the pipeline with other specifications. So we get the requirement of sequencing without breaking out of the expression oriented programming style.

Here are a few other specifications based on the domain knowledge that we have gathered ..

?
private def approve = ReaderTStatus[Order, Boolean] {order =>
right(true)
}
private def checkCustomerStatus(customer: Customer) = ReaderTStatus[Order, Boolean] {order =>
right(true)
}
private def checkInventory = ReaderTStatus[Order, Boolean] {order =>
right(true)
}

Wiring them together

But how do we wire these pieces together so that we have the sequence of operations that the domain mandates and yet all goodness of compositionality in our model ? It’s actually quite easy since we have already done the hard work of defining the appropriate types that compose ..

Here’s the isReadyForFulfilment method that defines the composite specification and invokes all the individual specifications in sequence using for-comprehension, which, as you all know does the monadic bind in Scala and gives us the final expression that needs to be evaluated for the Order supplied.

?
def isReadyForFulfilment(order: Order) = {
val s = for {
_ <- validate
_ <- approve
_ <- checkCustomerStatus(order.customer)
c <- checkInventory
} yield c
s(order)
}

So we have the monadic bind implement the sequencing without breaking the compositionality of the abstractions. In the next instalment we will see how this can be composed with the downstream processing of the order that will not only read stuff from the entity but mutate it too, of course in a functional way.