Привет, Хабр! Не так давно в издательстве «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.87%Книгу "Functional and Reactive Domain Modeling" действительно стоит перевести86
11.11%Не впечатлен11
2.02%Интересует другая книга на тему DDD (в комментарии)2
Проголосовали 99 пользователей. Воздержались 40 пользователей.

