[Перевод] Функциональные паттерны при моделировании предметной области – анемичные модели и компоновка поведений

Здравствуйте, Хабр!

Не так давно в издательстве «Manning» вышла непростая, но долгожданная и выстраданная автором книга о функциональном моделировании предметных областей.

d417eb996d514ab88c0f5b320ca8808f.jpg

Поскольку у нас готовятся книги как по 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.

Комментарии (4)

  • 24 февраля 2017 в 20:12

    0

    Перевод:
    >Наконец, проверяем Order…

    Оригинал:
    >Finally we check out the Order…

    А вот не надо так переводить. check out для заказа никогда не означает «проверяем».

    • 24 февраля 2017 в 20:17

      0

      Спасибо, поправили
      • 24 февраля 2017 в 20:23

        0

        На здоровье. А скажите, вот эти статьи — они не то чтобы плохие, наоборот, они интересные, но они производят впечатление, что автор размышляет прямо в процессе написания. Для блога это нормально. Книга наверное все-таки не такая, не таким вот языком блогов написана?
  • 24 февраля 2017 в 20:29

    0

    Нет, книга более структурированная и строгая. Складывается впечатление, что Гош как раз задумал ее, приступив к изложению подобных идей в блоге — еще в 2014 году

© Habrahabr.ru