Привет, Хабр! Не так давно в издательстве «Manning» вышла непростая, но долгожданная и выстраданная автором книга о функциональном моделировании предметных областей.
Поскольку у нас готовятся книги как по Scala, так и по паттернам предметно-ориентированного проектирования, опубликуем одну из статей сахиба Гоша об идеях, заложенных в его книгу, и спросим, насколько эта книга была бы вам интересна
Однажды я изучил презентацию Дина Уомплера (Dean Wampler) по поводу предметно-ориентированного проектирования, анемичных предметных моделей и функционального программирования, которое позволяет сгладить некоторые из обозначенных проблем. Полагаю, от некоторых тезисов Уомплера ООП-программисты могли бы содрогнуться. Они противоречат общепринятым убеждениям, согласно которым предметно-ориентированное проектирование должно выполняться прежде всего средствами ООП.
Озвучу мысль, с которой я категорически согласен — "DDD стимулирует вас разобраться в предметной области, но не помогает в реализации моделей". DDD действительно отлично помогает разработчикам освоить предметную область, с которой приходится иметь дело и выработать общую терминологию, которая будет использоваться в ходе всего проектирования и реализации приложения. Примерно такую роль играют и паттерны проектирования – обеспечивают терминологический аппарат, при помощи которого можно по существу объяснить разработчикам задачу, не вдаваясь в детали ее реализации.
С другой стороны, когда пытаешься реализовать концепции DDD при помощи стандартных приемов ООП, где состояние сопряжено с поведением, зачастую получается путаная изменяемая модель. Модель может быть насыщенной в том смысле, что все аспекты конкретной абстракции, взятой из предметной области, могут быть заложены в моделируемый класс. Но в таком случае класс становится непрочным, поскольку абстракция выходит чрезмерно локальной, ей недостает глобальных возможностей по части многократного использования и компонуемости. В результате, когда мы пытаемся скомпоновать множество абстракций на уровне сервисов предметной области, этот уровень переполняется мусорным склеивающим кодом – такой код нужен, чтобы справиться с рассогласованием нагрузки (impedance mismatch) между границами классов.
Поэтому, когда Дин утверждает "Модели должны быть анемичны" – думаю, он призывает избегать такой спутанности состояния и поведения в объекте предметной области, которая к тому же дает ложное ощущение безопасности и насыщенности модели. Он рекомендует писать объекты предметной области так: они должны обладать состоянием лишь в том случае, если поведения моделируются при помощи автономных функций.
Есть еще один неуклюжий аргумент, который попадается мне довольно часто: состояние спутывается с поведением в процессе моделирования последнего по мере того, как нарастает инкапсуляция методов в классе. Если вы до сих пор придерживаетесь такой философии, отсылаю вас к великолепной статье Скотта Мейера, написанной еще в 2000 году. Он отказывается считать, что класс – это и есть нужный уровень модуляризации, и рекомендует писать более мощные системы модулей, так как в модулях удобнее хранить поведения предметной области.
Вот анемичная предметная модель абстракции
Ранее я писал, как реализуются паттерны DDD Specification и Aggregate при помощи принципов функционального программирования. Кроме того, мы обсуждали, как делать функциональные обновления агрегатов при помощи таких структур данных как Lens. В этой статье мы воспользуемся ими в качестве строительных элементов, применим более функциональные паттерны и реализуем более крупные поведения, моделирующие язык предметной области. В конце концов, один из базовых принципов DDD – поднимать словарь предметной области на уровень реализации, так, чтобы функциональность была очевидна для разработчика, занимающегося поддержкой модели.
Основная идея – проверить, на самом ли деле при создании поведений предметной области в виде автономных функций дает эффективную модель предметной области в соответствии с принципами DDD. Базовые классы модели содержат только такие поведения, изменить которые можно функциональными средствами. Все поведения предметной области моделируются при помощи функций, находящихся в модуле, представляющем агрегат.
Функции компонуются, и именно таким образом мы собираемся сцеплять поведения предметной области и выстраивать крупные абстракции из более мелких. Вот небольшая функция, оценивающая Order. Обратите внимание: она возвращает
Но что это нам дает? Что конкретно мы приобретаем благодаря функциональным паттернам? Мы получаем возможность выделять семейства схожих абстракций, например, аппликативы и монады. Звучит несколько отвлеченно, пожалуй, для обоснования такого подхода нужна отдельная статья. Проще говоря, они инкапсулируют эффекты и побочные эффекты вычислений, так, что вы можете сосредоточиться на реализации модели как таковой. Взгляните на функцию process ниже – вот вам и композиция монадных функций на практике. Но вся начинка, обеспечивающая обработку эффектов и побочных эффектов, абстрагируется в
Когда
Наконец, подсчитываем стоимость заказа
А вот служебный метод, компонующий все вышеописанные поведения предметной области в одну большую абстракцию. Нам не приходится инстанцировать ни одного объекта. Просто компонуем функции, и в результате удается выразить весь поток событий. Код получился таким удобочитаемым и кратким именно потому, что сама абстракция определена совершенно четко.
Если вас интересует полный исходный код к этому примеру – отсылаю вас к моему репозиторию на github.
Поскольку у нас готовятся книги как по Scala, так и по паттернам предметно-ориентированного проектирования, опубликуем одну из статей сахиба Гоша об идеях, заложенных в его книгу, и спросим, насколько эта книга была бы вам интересна
Однажды я изучил презентацию Дина Уомплера (Dean Wampler) по поводу предметно-ориентированного проектирования, анемичных предметных моделей и функционального программирования, которое позволяет сгладить некоторые из обозначенных проблем. Полагаю, от некоторых тезисов Уомплера ООП-программисты могли бы содрогнуться. Они противоречат общепринятым убеждениям, согласно которым предметно-ориентированное проектирование должно выполняться прежде всего средствами ООП.
Озвучу мысль, с которой я категорически согласен — "DDD стимулирует вас разобраться в предметной области, но не помогает в реализации моделей". DDD действительно отлично помогает разработчикам освоить предметную область, с которой приходится иметь дело и выработать общую терминологию, которая будет использоваться в ходе всего проектирования и реализации приложения. Примерно такую роль играют и паттерны проектирования – обеспечивают терминологический аппарат, при помощи которого можно по существу объяснить разработчикам задачу, не вдаваясь в детали ее реализации.
С другой стороны, когда пытаешься реализовать концепции DDD при помощи стандартных приемов ООП, где состояние сопряжено с поведением, зачастую получается путаная изменяемая модель. Модель может быть насыщенной в том смысле, что все аспекты конкретной абстракции, взятой из предметной области, могут быть заложены в моделируемый класс. Но в таком случае класс становится непрочным, поскольку абстракция выходит чрезмерно локальной, ей недостает глобальных возможностей по части многократного использования и компонуемости. В результате, когда мы пытаемся скомпоновать множество абстракций на уровне сервисов предметной области, этот уровень переполняется мусорным склеивающим кодом – такой код нужен, чтобы справиться с рассогласованием нагрузки (impedance mismatch) между границами классов.
Поэтому, когда Дин утверждает "Модели должны быть анемичны" – думаю, он призывает избегать такой спутанности состояния и поведения в объекте предметной области, которая к тому же дает ложное ощущение безопасности и насыщенности модели. Он рекомендует писать объекты предметной области так: они должны обладать состоянием лишь в том случае, если поведения моделируются при помощи автономных функций.
Иногда красивая реализация – это просто функция. Не метод. Не класс. Не каркас. Просто функция.
Джон Кармак
Есть еще один неуклюжий аргумент, который попадается мне довольно часто: состояние спутывается с поведением в процессе моделирования последнего по мере того, как нарастает инкапсуляция методов в классе. Если вы до сих пор придерживаетесь такой философии, отсылаю вас к великолепной статье Скотта Мейера, написанной еще в 2000 году. Он отказывается считать, что класс – это и есть нужный уровень модуляризации, и рекомендует писать более мощные системы модулей, так как в модулях удобнее хранить поведения предметной области.
Вот анемичная предметная модель абстракции
Order
…case class Order(orderNo: String, orderDate: Date, customer: Customer,
lineItems: Vector[LineItem], shipTo: ShipTo,
netOrderValue: Option[BigDecimal] = None, status: OrderStatus = Placed)
Ранее я писал, как реализуются паттерны DDD Specification и Aggregate при помощи принципов функционального программирования. Кроме того, мы обсуждали, как делать функциональные обновления агрегатов при помощи таких структур данных как Lens. В этой статье мы воспользуемся ими в качестве строительных элементов, применим более функциональные паттерны и реализуем более крупные поведения, моделирующие язык предметной области. В конце концов, один из базовых принципов DDD – поднимать словарь предметной области на уровень реализации, так, чтобы функциональность была очевидна для разработчика, занимающегося поддержкой модели.
Основная идея – проверить, на самом ли деле при создании поведений предметной области в виде автономных функций дает эффективную модель предметной области в соответствии с принципами DDD. Базовые классы модели содержат только такие поведения, изменить которые можно функциональными средствами. Все поведения предметной области моделируются при помощи функций, находящихся в модуле, представляющем агрегат.
Функции компонуются, и именно таким образом мы собираемся сцеплять поведения предметной области и выстраивать крупные абстракции из более мелких. Вот небольшая функция, оценивающая Order. Обратите внимание: она возвращает
Kleisli
, что фактически обеспечивает нам композицию поверх монадных функций. То есть, вместо компоновки a -> b
и b -> c
, а именно так мы бы и поступили при обычной компоновке функций, мы делаем то же самое с a -> m b
и b -> m c
, где m
– монада. Композиция с эффектами, если можно так выразиться. def valueOrder = Kleisli[ProcessingStatus, Order, Order] {order =>
val o = orderLineItems.set(
order,
setLineItemValues(order.lineItems)
)
o.lineItems.map(_.value).sequenceU match {
case Some(_) => right(o)
case _ => left("Missing value for items")
}
}
Но что это нам дает? Что конкретно мы приобретаем благодаря функциональным паттернам? Мы получаем возможность выделять семейства схожих абстракций, например, аппликативы и монады. Звучит несколько отвлеченно, пожалуй, для обоснования такого подхода нужна отдельная статья. Проще говоря, они инкапсулируют эффекты и побочные эффекты вычислений, так, что вы можете сосредоточиться на реализации модели как таковой. Взгляните на функцию process ниже – вот вам и композиция монадных функций на практике. Но вся начинка, обеспечивающая обработку эффектов и побочных эффектов, абстрагируется в
Kleisli
, поэтому реализация на уровне пользователя получается простой и лаконичной. Kleisli
демонстрирует весь потенциал компоновки монадных функций. Любое поведение в предметной области может отказать, и отказ моделируется при помощи монады Either
– здесь ProcessingStatus
– просто псевдоним типа для ..type ProcessingStatus[S] = \/[String, S]
. Работая с Kleisli
, не приходится писать никакого кода для обработки отказов. Ниже вы можете убедиться, что композиция совершенно похожа на обычные функции, альтернативные потоки выполнения учтены на уровне паттерна.Когда
Order
оценен, нужно применить скидки к входящим в него товарам. Это еще одно поведение, реализованное в соответствии с тем же паттерном, что и valueOrder
.def applyDiscounts = Kleisli[ProcessingStatus, Order, Order] {order =>
val o = orderLineItems.set(
order,
setLineItemValues(order.lineItems)
)
o.lineItems.map(_.discount).sequenceU match {
case Some(_) => right(o)
case _ => left("Missing discount for items")
}
}
Наконец, подсчитываем стоимость заказа
Order
…def checkOut = Kleisli[ProcessingStatus, Order, Order] {order =>
val netOrderValue = order.lineItems.foldLeft(BigDecimal(0).some) {(s, i) =>
s |+| (i.value |+| i.discount.map(d => Tags.Multiplication(BigDecimal(-1)) |+| Tags.Multiplication(d)))
}
right(orderNetValue.set(order, netOrderValue))
}
А вот служебный метод, компонующий все вышеописанные поведения предметной области в одну большую абстракцию. Нам не приходится инстанцировать ни одного объекта. Просто компонуем функции, и в результате удается выразить весь поток событий. Код получился таким удобочитаемым и кратким именно потому, что сама абстракция определена совершенно четко.
def process(order: Order) = {
(valueOrder andThen
applyDiscounts andThen checkOut) =<< right(orderStatus.set(order, Validated))
}
Если вас интересует полный исходный код к этому примеру – отсылаю вас к моему репозиторию на github.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Актуальность книги
86.6% Книгу «Functional and Reactive Domain Modeling» действительно стоит перевести84
11.34% Не впечатлен11
2.06% Интересует другая книга на тему DDD (в комментарии)2
Проголосовали 97 пользователей. Воздержались 40 пользователей.