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

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



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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Актуальность книги

  • +15
  • 11,5k
  • 8
Издательский дом «Питер»
277,29
Компания
Поделиться публикацией

Комментарии 8

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

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

    А вот не надо так переводить. check out для заказа никогда не означает «проверяем».
      0
      Спасибо, поправили
        0
        На здоровье. А скажите, вот эти статьи — они не то чтобы плохие, наоборот, они интересные, но они производят впечатление, что автор размышляет прямо в процессе написания. Для блога это нормально. Книга наверное все-таки не такая, не таким вот языком блогов написана?
      0
      Нет, книга более структурированная и строгая. Складывается впечатление, что Гош как раз задумал ее, приступив к изложению подобных идей в блоге — еще в 2014 году
        0
        Часто авторов серий (обычно неформальных) постов просят изложить их в виде книги. Иногда читатели, иногда издатели :)
        –1

        This is a dogmatic screed that is probably worthless to the working programmer. Any reader would be better off just reading a bit about functional programming and incorporating it into their work as they see fit.


        Further, a book of this type is no place to be using terminology from universal algebra, category theory and mathematical logic which, while possibly impressive to the uninitiated, will likely only leave the average reader dazed and confused. Worse, it is not clear that the author understands the mathematics as is witnessed by their definition of an algebra which, among other peculiarities, contains this statement: "[an algebra contains] a few axioms or laws that are assumed to be true and can be used to derive other theorems."


        Be warned, this book assumes the axiom of Mutability is Evil without substantive reason.

          0
          Ждем перевода!
            +1
            Это хорошая и нужная книга.

            Пользуясь случаем, хочу сообщить, что на протяжении 2016 года я тоже писал для Manning книгу под кодовым названием «Functional Design and Architecture». В ней я рассматриваю много важных вопросов:

            • Approaches to architecture modeling using diagrams;
            • Requirements analysis;
            • Embedded DSL domain modeling;
            • External DSL design and implementation;
            • Monads as subsystems with effects;
            • Free monads as functional interfaces;
            • Arrowised eDSLs;
            • Inversion of Control using Free monadic eDSLs;
            • Software Transactional Memory;
            • Lenses;
            • State, Reader, Writer, RWS, ST monads;
            • Impure state: IORef, MVar, STM;
            • Multithreading and concurrent domain modeling;
            • GUI;
            • Applicability of mainstream techiques and approaches such as UML, SOLID, GRASP;
            • Interaction with impure subsystems.
            • Functional design patterns: Abstract interpreter, MVar request-response, State monad dependency injection, etc.


            Книга построена вокруг центрального примера — SCADA-приложение Andromeda об управлении космическим кораблем, написанное на Haskell (GitHub).

            На данный момент в черновую готовы 5 глав (50% книги):

            1. What is software design?
            2. Architecture of the application
            3. Subsystems and services
            4. Domain model design
            5. Applicatoin state


            На моем гитхабе доступны материалы: здесь.

            Предполагаются также следующие главы:

            • Business logic design
            • FRP
            • Reactive streams
            • Persistence
            • Type level design
            • Design patterns and idioms


            К сожалению, в январе 2017 издательство Manning Publications решило прекратить проект, потому что их не устраивают сроки и общее содержание книги. В моих текущих планах предложить книгу другим издательствам и закончить работу над манускриптом за 2017 год.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое