Поваренная книга разработчика: DDD-рецепты (4-я часть, Структуры)

    Введение


    Итак, мы уже определились с областью применения, методологией и архитектурой. Перейдем от теории к практике, к написанию кода. Хотелось бы начать с шаблонов проектирования, которые описывают бизнес логику — Service и Interactor. Но прежде чем приступить к ним, изучим структурные паттерны — ValueObject и Entity. Разрабатывать мы будем на языке ruby. В дальнейших статьях разберем все паттерны, необходимые для разработки с использованием Вариативной архитектуры. Все наработки, являющиеся приложениями к данному циклу статей, соберем в отдельный фреймворк.


    Blacjack & hockers


    И мы уже подобрали подходящее название — LunaPark.
    Текущие наработки выложенны на Github.
    Разобрав все шаблоны, соберем один полноценный микросервис.


    Так исторически сложилось


    Была необходимость в рефакторинге сложного корпоративного приложения, написанного на Ruby on Rails. Была готовая команда ruby-разработчиков. Методология Domain Driven Development прекрасно подходила для этих задач, но готового решения на используемом языке не было. Не смотря на то, что выбор языка, в основном, был обусловлен нашей специализацией, он оказался достаточно удачным. Среди всех языков, что принято использовать для web-приложений, ruby, на мой взгляд, является самым выразительным. И поэтому больше других подходит для моделирования реальных объектов. Это не только мое мнение.


    That is the Java world. Then you have the new-comers like Ruby. Ruby has a very expressive syntax, and at this basic level it should be a very good language for DDD (although I haven't heard of much actual use of it in those sorts of applications yet). Rails has generated a lot of excitement because it finally seems to make creation of Web UIs as easy as UIs were back in the early 1990s, before the Web. Right now, this capability has mostly been applied to building some of the vast number of Web applications which don't have much domain richness behind them, since even these have been painfully difficult in the past. But my hope is that, as the UI implementation part of the problem is reduced, that people will see this as an opportunity to focus more of their attention on the domain. If Ruby usage ever starts going in that direction, I think it could provide an excellent platform for DDD. (A few infrastructure pieces would probably have to be filled in.)

    Eric Evans 2006

    К сожалению, за прошедшие 13 лет ничего особо не изменилось. В интернете можно найти попытки приспособить для этого Rails, но все они выглядят ужасно. Фреймворк Rails тяжелый, медленный и не соответствует принципам SOLID. Смотреть без слез, как кто-то пытается изобразить на основе AсtiveRecord реализацию паттерна Репозиторий, очень тяжело. Мы решили взять на вооружение какой-нибудь микрофреймворк и доработать его до наших потребностей. Попробовали Grape, идея с авто-документированием показалась удачной, но в остальном он был заброшенным и мы быстро отказались от идеи его использования. И почти сразу стали использовать другое решение — Sinatra. Мы до сих пор продолжаем его использовать для REST Контроллеров и Эндпоинтов.


    REST ?

    Если вы разрабатывали web-приложения, то уже имеете представление о технологии. У нее есть свои плюсы и минусы, полное перечисление которых выходит за рамки данной статьи. Но для нас, как разработчиков корпоративных приложений, самым главным недостатком будет то, что REST (это понятно даже из названия) отражает не процесс, а его состояние. А преимуществом будет его понятность — технология ясна как back-end разработчикам, так и разработчикам front-end'a.
    Но может тогда не ориентироваться на REST, а реализовать свое решение http + json? Если даже вам удасться разработать свой сервисный API, то предоставляя его описание третьим лицам вы получите много вопросов. Гораздо больше, чем если вы предоставите привычный REST.
    Будем считать использование REST компромиссным решением. Мы используем JSON для лаконичности и jsonapi стандарт, чтобы не тратить время разработчиков на священные войны по поводу формата запросов.
    В дальнейшем, когда мы будем разбирать Endpoint, мы увидим, что для того, чтобы избавится от rest, достаточно переписать всего один класс. Так что REST не должен вообще беспокоить, если остались сомнения на его счет.


    В ходе написания нескольких микросервисов у нас появились наработки — набор абстрактных классов. Каждый такой класс можно написать за пол часа, его код легко понять, если знать для чего этот код предназначен.


    Тут и возникли основные трудности. Новые сотрудники, не имевшие дело с практиками DDD и чистой архитектурой, не могли понять код и его предназначение. Если бы я сам увидел этот код впервые до того как прочитал Эванса, я бы воспринял его как legacy, over-engineering.


    Чтобы побороть это препятствие было принято решение написать документацию (guideline), описывающую философию используемых подходов. Наброски этой документации показались удачными и было решено выложить их на Хабре. Абстрактные классы, которые повторялись из проекта в проект, было решено вынести в отдельный gem.


    Философия


    legacy-way
    Если вспомнить какой-нибудь классический фильм про боевые искусства, то там будет крутой парень, который очень ловко обращается с шестом. Шест — это по сути палка, очень примитивный инструмент, один из первых, который попал человеку в руки. Но в руках мастера он становится грозным оружием.
    Можно потратить время на создание пистолета, который не стреляет тебе в ногу, а можно потратить время на обучение технике стрельбы. Мы выделили 4 основных принципа:


    • Нужно делать сложные вещи простыми.
    • Знания важнее технологии. Документация понятнее человеку чем код, не следует подменять одно другим.
    • Прагматичность важнее догматизма. Стандарты должны подсказывать путь, а не устанавливать ограничительные рамки.
    • Структурность в архитектуре, гибкость в выборе решений.

    Схожую философию можно проследить например у ОС ArchLinux — The Arch Way. На моем ноутбуке Linux долго не приживался, рано или поздно он ломался и мне постоянно приходилось его переустанавливать. Это вызывало ряд проблем, иногда серьезных вроде срыва deadline по работе. Но потратив один раз 2-3 дня на установку Arch я разобрался с тем как моя ОС работает. После этого она стала работать стабильнее, без сбоев. Мои заметки помогли мне устанавливать ее на новые ПК за пару часов. А обильная документация помогала мне решать новые задачи.


    Фреймворк имеет абсолютно высокоуровневый характер. Классы, которые его описывают, отвечают за структуру приложения. Для взаимодействия с базами данных, реализации http протокола и других низкоуровневых вещей используются сторонние решения. Нам хотелось бы, чтобы программист при возникновении вопроса мог подсмотреть в код и понять как тот или иной класс работает, а документация позволила бы понять как ими управлять. Понимание устройства двигателя не позволит вам водить автомобиль.


    Фреймворк


    Сложно назвать LunaPark фреймворком в привычном смысле. Frame — рамка, Work — работа. Мы же призываем не ограничивать себя рамками. Единственная рамка, которую мы декларируем, это та, которая подсказывает класс, в котором должна быть описана та или иная логика. Это скорее набор инструментов с объемной инструкцией к ним.
    Каждый класс — абстрактный и имеет три уровня:


    module LunaPark  # Фреймворк
      module Forms   # Паттерн
        class Single # Реализация/вариант
        end
      end
    end

    Если вы хотите реализовать форму, которая создает один элемент, вы наследуетесь от данного класса:


    module Forms
      class Create < LunaPark::Forms::Single

    Если несколько элементов, воспользуемся другой Реализацией.


    module Forms
      class Create < LunaPark::Forms::Multiple

    На данный момент не все наработки приведены в идеальный порядок и gem находится в состоянии альфа-версии. Мы будем приводить его поэтапно, согласованно с выходом статей. Т.е. если вы видите статью про ValueObject и Entity, то эти два шаблона уже реализованы. К окончанию цикла все они будут пригодны к использованию на проекте. Поскольку сам по себе фреймворк малополезен без связки с sinatra \ roda, будет сделан отдельный репозиторий, который покажет как все "прикрутить" для быстрого старта вашего проекта.


    Фреймворк является прежде всего приложением к документации. Не стоит воспринимать данные статьи как документацию к фреймворку.


    Итак, перейдем к делу.


    Объект-Значение (Value)


    — Какого роста твоя подруга?
    — 151
    — Ты стал встречаться со статуей свободы?

    Примерно такой разговор мог бы произойти в штате Индиана. Рост человека это не просто число, но еще и единица измерения. Не всегда атрибуты объекта можно описать только примитивами (Integer, String, Boolean и т.п.), иногда требуются их комбинации:


    • Деньги это не просто число, это число (сумма) + валюта.
    • Дата состоит из числа, месяца и года.
    • Чтобы измерить вес нам недостаточно одного числа, требуется еще и единица измерения.
    • Номер паспорта состоит из серии и, собственно, из номера.

    С другой стороны это не всегда комбинация, возможно это некое расширение примитива.
    Телефонный номер зачастую воспринимается как число. С другой стороны, вряд ли у него должен быть метод сложения или деления. Возможно, есть метод, который будет выдавать код страны и метод, определяющий код города. Возможно, будет некий декоративный метод, который представит его не просто строкой чисел 79001231212, а читаемой строкой: 7-900-123-12-12.


    а может в декоратор?

    Если исходить из догм, то бесспорно — да. Если подходить к этой дилемме со стороны здравого смысла, то когда мы решим позвонить по этому номеру, то передадим телефону сам объект:


    phone.call Values::PhoneNumber.new(79001231212)

    А если мы решили его представить в виде строки, то это явно сделано для человека. Так почему бы нам не сделать эту строку для человека сразу читаемой?


    Values::PhoneNumber.new(79001231212).to_s

    Представим, что мы создаем сайт онлайн-казино "Три топора" и реализуем карточные игры. Нам понадобится класс 'игральная карта'.


    module Values
      class PlayingCard < Lunapark::Values::Compound
        attr_reader :suit, :rank
      end
    end

    Итак, у нашего класса есть два атрибута только для чтения:


    • suit — масть карты
    • rank — достоинство карты

    Эти атрибуты задаются только при создании карты и не могут изменятся при ее использовании. Вы конечно можете взять игральную карту и перечеркнуть 8, написать Q, но это недопустимо. В приличном обществе вас, скорее всего, пристрелят. Невозможность менять атрибуты после создания объекта определяет первое свойство Объекта-значения — иммутабельность.
    Вторым важным свойством Объекта-Значения будет то, как мы их сравниваем.


    module Values
      RSpec.describe PlayingCard  do
        let(:card)  { described_class.new suit: :clubs, rank: 10 }
        let(:other) { described_class.new suit: :clubs, rank: 10 }
        it 'should be eql' do
          expect(card).to eq other
        end
      end
    end

    Такой тест не пройдет, так как они будут сравниваться по адресу. Чтобы тест прошел, мы должны сравнивать Value-Obects по значению, для этого допишем метод сравнения:


    def ==(other)
      suit == other.suit &&
      rank == other.rank
    end

    Теперь наш тест пройдет. Мы также можем дописать методы, которые отвечают за сравнение, но как нам сравнить 10 и K? Как вы уже, наверное, догадались, мы тоже их представим в виде Объектов-Значений. Ок, значит теперь мы должны будем инициировать десятку трефа так:


    ten       = Values::Rank.new('10')
    clubs     = Values::Suits.new(:clubs)
    ten_clubs = Values::PlayingCards.new(rank: ten, clubs: clubs)

    Три строчки это достаточно много для ruby. Для того, чтобы обойти это ограничение, мы введем третье свойство Объекта-Значения — оборачиваемость. Пусть у нас появится специальный метод класса .wrap, который может принимать значения различного типа и преобразовывать их в нужный.


    class PlayingCard < Lunapark::Values::Compound
      def self.wrap(obj)
        case obj.is_a? self.class # Если мы получили объект класса PlayingCard
          obj                     # то мы его и вернем
        case obj.is_a? Hash       # Если мы получили хэш, то создадим на его основе
          new(obj)                # Новую игральную карту
        case obj.is_a String      # Если мы получили строку, то последний символ будет
          new rank: obj[0..-2], suit:[-1]  # мастью, остальные - достоинством карты.
        else                      # если тип не совпадает с ожидаемым
          raise ArgumentError     # выдаем ошибку.
        end
      end
      def initialize(suit:, rank:) # Еще модифицируем инициализатор класса
         @suit = Suit.wrap(suit)   # Это позволит нам оборачивать значения
         @rank = Rank.wrap(rank)
      end
     end

    Такой подход дает большое преимущество:


    ten         = Values::Rank.new('10')
    clubs       = Values::Suits.new(:clubs)
    from_values = Values::PlayingCard.wrap rank: ten,  suit: clubs
    from_hash   = Values::PlayingCard.wrap rank: '10', suit: :clubs
    from_obj    = Values::PlayingCard.wrap from_values
    from_str    = Values::PlayingCard.wrap '10C' # тут хотелось бы использовать симол треф из utf кодировки, но хабр, их обрезает.

    Все эти карты будут равны между собой. Если метод wrap разрастается хорошей практикой, будет вынесение его в отдельный класс. С точки зрения догматического подхода отдельный класс так же будет обязательным.
    Хм, а как насчет места в колоде? Как узнать, является ли данная карта козырем? Это не игральная карта. Это Значение игральной карты. Это именно та надпись 10, которую вы ведите на углу картона.
    К Объекту-Значению нужно относится также, как и к примитиву, который почему-то не реализовали в ruby. Отсюда возникает последнее свойство — Объект-Значение не привязан ни к какому домену.


    Рекомендации


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

    Фредерик Тейлор 1914

    Арифметические операции должны возвращать новый объект

    # GOOD
    class Money < LunaPark::Values::Compound
      def +(other)
        other = self.class.wrap(other)
        raise ArgumentError unless same_currency? other
        self.class.new(
          amount: amount + other.amount,
          currency: currency
        )
      end
    end

    Атрибуты Объекта-Значения могу быть только примитивами или другими Объектами-значения

    # GOOD
    class Weight < LunaPark::Values::Compound
      def intialize(value:, unit:)
        @value = value
        @unit  = Unit.wrap(unit)
      end
    end
    
    # BAD
    class PlaingCard < LunaPark::Value
      def initialize(rank:, suit:, deck:)
        ...
        @deck = Entity::Deck.wrap(deck) # зависимость от сущности
      end
    end

    Простые операции держите внутри методов класса

    # GOOD
    class Weight < LunaPark::Values::Compound
      def >(other)
        value > other.convert_to(unit).value
      end
    end

    Если операция "конвертация" большая, то возможно есть смысл вынести ее в отдельный класс

    # UGLY
    class Weight < LunaPark::Values::Compound
      def convert_to(unit)
        unit = Unit.wrap(unit)
        case { self.unit.to_sym => unit.to_sym }
        when { :kg => :ft }
          Weight.new(value: 2.2046 * value, unit.to_sym)
        when 
          # ...
        end
      end
    end
    
    # GOOD
    #./lib/values/weight/converter.rb
    class Weight
      class Converter < LunaPark::Services::Simple
        def initialize(weight, to:)
          ...
        end
      end
    end
    #./lib/values/weight.rb
    class Weight < LunaPark::Values::Compound
      def convert_to(unit)
        Converter.call! self, to: unit
      end
    end

    Такое вынесение логики в отдельный Сервис возможно только при условии того, что Сервис изолирован: он не использует данные ни с каких внешних источников. Этот сервис должен быть ограничен контекстом самого Объекта-Значения


    Объект значение не может ничего знать о доменной логике

    Предположим, что мы пишем интернет магазин, и у нас есть рейтинг товаров. Чтобы его получить, необходимо сделать запрос в БД через Репозиторий.


    # DEADLY BAD
    class Rate < LunaPark::Values::Single
      def top?(10)
        Repository::Rates.top(first: 10).include? self
      end
    end

    Сущность (Entity)


    Класс Сущность отвечает за какой-то реальный объект. Это может быть договор, стул, агент недвижимости, пирог, утюг, кот, холодильник — всё что угодно. Любой объект, который может вам понадобиться для моделирования ваших бизнес-процессов, — это Сущность.
    Понятие Сущности по Эвансу и по Мартину отличаются. С точки зрения Эванса, сущность — это объект, характеризующийся чем-то, что подчеркивает ее индивидуальность.


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

    С точки зрения Мартина, Entity — это не объект, а слой. Этот слой объединят как объект, так и бизнес-логику по его изменению.


    Разъеснение от Мартина
    My view of Entities is that they contain Application Independent Business rules. They are not simply data objects. They may hold references to data objects; but their purpose is to implement business rule methods that can be used by many different applications.

    Gateways return Entities. The implementation (below the line) fetches the data from the database, and uses it to construct data structures which are then passed to the Entities. This can be done either with containment or inheritance.

    For example:

    public class MyEntity { private MyDataStructure data;}

    or

    public class MyEntity extends MyDataStructure {...}

    And remember, we are all pirates by nature; and the rules I'm talking about here are really more like guidelines...

    Мы под Сущностью будем иметь в виду только структуру. В простейшем варианте класс Entity будет выглядеть так:


    module Entities
      class MeatBag < LunaPark::Entities::Simple
        attr_accessor :id, :name, :hegiht, :weight, :birthday
      end
    end

    Мутабельный объект, описывающий структуры бизнес модели, может содержать примитивные типы и Значения.
    Класс LunaPark::Entites::Simple невероятно прост, вы можете посмотреть его код, он дает нам только одну вещь — легкую инициализацию.


    LunaPark::Entites::Simple
    module LunaPark
      module Entities
        class Simple
          def initialize(params)
            set_attributes params
          end
    
          private
    
          def set_attributes(hash)
            hash.each { |k, v| send(:"#{k}=", v) }
          end
        end
      end
    end

    Вы можете написать:


    john_doe = Entity::MeatBag.new(
      id:        42,
      name:     'John Doe',
      height:   '180cm',
      weight:   '80kg',
      birthday: '01-01-1970'
    )

    Как вы уже наверное догадались вес, рост и дату рождения мы хотим обернуть в Объекты-значения.


    module Entities
      class MeatBag < LunaPark::Entites::Simple    
        attr_accessor :id, :name
        attr_reader   :heiht, :wight, :birthday
    
        def height=(height)
            @height = Values::Height.wrap(height)
        end
        def weight=(height)
            @height = Values::Weight.wrap(weight)
        end
        def birthday=(day)
          @birthday = Date.parse(day)
        end   
      end
    end

    Чтобы не тратить время на подобные конструкторы, у нас подготовлена более сложная Реализация LunaPark::Entites::Nested:


    module Entities
      class MeatBag < LunaPark::Entities::Nested
        attr :id
        attr :name
    
        attr :heiht,    Values::Height, :wrap
        attr :weight,   Values::Weight, :wrap
        attr :birthday, Values::Date,   :parse
      end
    end

    Как можно догадаться из названия, данная Реализация позволяет делать древовидные структуры.


    Давайте удовлетворим мою страсть к крупногабаритной бытовой технике. В прошлой статье мы проводили аналогию между "крутилкой" стиральной машины и архитектурой. А сейчас мы опишем такой важный бизнес-объект как холодильник:


    Refregerator


    class Refregerator < LunaPark::Entites::Nested
      attr :id, 
      attr :brand
      attr :title
    
      namespace :fridge do
        namespace :door do
          attr :upper, Shelf, :wrap
              attr :lower, Shelf, :wrap  
        end
        attr :upper, Shelf, :wrap
        attr :lower, Shelf, :wrap
      end
    
      namespace :main do
        namespace :door do
            attr :first,  Shelf, :wrap
            attr :second, Shelf, :wrap  
          attr :third,  Shelf, :wrap
        end
    
        namespace :boxes do
            attr :left,  Box, :wrap
            attr :right, Box, :wrap
        end
    
        attr :first,  Shelf, :wrap
        attr :second, Shelf, :wrap  
        attr :third,  Shelf, :wrap
        attr :fourth, Shelf, :wrap
      end
    
      attr :last_open_at, comparable: false
    end  

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


    У класса LunaPark::Entites::Nested есть еще 2 важных свойства:


    Сравнимость:


    module Entites
      class User < LunaPark::Entites::Nested
        attr :email
        attr :registred_at
      end
    end
    
    u1 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now)
    u2 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now)
    
    u1 == u2 # => false

    Два указанных пользователя не эквивалентны, т.к. они были созданы в разное время и поэтому значение атрибута registred_at будет отличаться. Но если мы вычеркнем атрибут из списка сравниваемых:


    module Entites
      class User < LunaPark::Entites::Nested
        attr :email
        attr :registred_at, comparable: false
      end
    end

    то получим два сопоставимых объекта.


    Эта Реализация так же обладает свойством оборачиваемости — мы можем использовать метод класса`wrap


    Entites::User.wrap(email: 'john.doe@mail.com', registred_at: Time.now)

    Вы можете использовать в качестве Entity — Hash, OpenStruct или любой понравившийся вам gem, который поможет вам реализовать структуру вашей сущности.


    Сущность — это модель бизнес объекта, оставьте ее простой. Если какое-то свойство не используется вашим бизнесом, не описывайте его.


    Изменения сущности


    Как вы заметили, класс Сущность не имеет никаких методов собственного изменения. Все изменения делаются из вне. Объект-значения тоже иммутабелен. Все те функции, которые в нем присутствуют, по большому счету декорируют сущность или создают новые объекты. Сама сущность остается неизменной. Для разработчика Ruby on Rails такой подход будет непривычен. Со стороны может показаться, что мы вообще используем ООП-язык для чего-то другого. Но если присмотреться поглубже — это не так. Разве окно может открыться само по себе? Автомобиль доехать до работы, гостиница забронироваться, милый котик получить нового подписчика? Это все внешние воздействия. Что-то происходит в реальном мире, а мы отражаем это у себя. По каждому запросу мы вносим изменения в свою модель. И тем самым поддерживаем ее в актуальном состоянии, достаточном для наших бизнес задач. Стоит разделять состояние модели и процессы, вызывающие изменения этого состояния. Как это сделать, мы рассмотрим в следующей статье.

    Поделиться публикацией

    Похожие публикации

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

      0
      Деньги это не просто число, это число (сумма) + валюта

      Ну да, есть такой паттерн. Только вот допустим, если посмотреть количество скачиваний Nuget-пакета для c# — можно удивиться какая низкая популярность. Видимо, большинство фигачит через decimal.


      Представим, что мы создаем сайт онлайн-казино "Три топора"

      А вы не хотите напомнить читателям, что игорная деятельность в рунете запрещена законом? Вот буквально сегодня прошла новость "ФНС нашла способ закрыть крупнейшее нелегальное интернет-казино".


      Не знаю какая позиция самого издательского дома ТМ по отношению к таким статьям (вряд ли это проплаченная реклама), но вот допустим во многих новостных изданиях я вижу постоянные скобки после названий террористических организаций — мол, это запрещённая в РФ организация. В общем, прямо по краю упоминание. Можно было бы и более нейтральный пример подобрать к статье.

        0

        Касательно денег, зачастую, в базе данных, для того чтобы сохранить деньги, используются поля с плавающей запятой. Это не совсем удобно, в дальнейшем возникают ряд ошибок связанных с округлением. По факту, не может в учете указваться половина копейки. Удобнее все хранить в фракциях — копейках, центах и.т.д., использовать целочисленный тип. Валюта тоже важна, кроме собственно валюты, она показывает разрядность тех или иных фракций. Если у рубля и доллара это одна сотая, то у иены ее нет, у сатоши одна девятимиллионная. Валюты могу так же разделять тестовые и реальные платежи. Если в проекте все эти проблемы не стоят, проще использовать decimal и не усложнять свою жизнь. В руби gem 'money' достаточно популярен, 2000 звезд на GitHub хороший показатель.


        Относительно онлайн казино, я не смотрел с этой точки зрения. Для меня как разработчика, это просто сложная система, где можно применить DDD. Возможно, у кого-то, легкий сарказм, в сторону надоедливой рекламы, вызовет улыбку. Объемную сухую статью без вставок читать тяжело. Никого не призываю разрабатывать онлайн казино и нарушать законы.

        0

        После последнего абзаца, как-то сильно "запахло" анемичными моделями.

          0

          Не хочется скатываться до холивара. Ко всему надо подходить рационально и не уходить в крайности. Давайте разберем пример.


          Задача


          1) Вы пишете демон для холодильника. У вас есть некий цикл который проверят открыт ли холодильник или нет, если открыт то шлем смс.


          loop do
            Service::SendSms(master.phone, 'Fridge is open') if fridge.open? && fridge.open_time > 15.sec
            sleep 30
          end

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


          2) Вы пишете демона, и холодильник у вас достаточно умный там встроен моторчик, для которого реализован какой-то драйвер, вынесенный в стороннюю библиотеку.


          class FridgeEntity
            def close!
              driver.close_door
            end
          
            #...
          end
          
          loop do
            fridge.close! if fridge.open? && fridge.open_time > 15.sec
            sleep 30
          end

          Создавать отдельный сервис который инициирует работу драйвера излишне. Но с другой стороны, правила проверки (раз в 30 секунд, если уже открыта более 15 секунд) не следует хранить в Сущности, так как это внешний процесс.


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

          0
          Два заявления
          Ко всему надо подходить рационально и не уходить в крайности.
          и
          класс Сущность не имеет никаких методов собственного изменения
          не совместимы. Согласно ООП и DDD, сущности точно могут менять собственное состояние. А вы им это не даете делать в принципе — это точно ошибка.
          Вы пытаетесь подменить понятие «Сущность/Агрегат» на понятие «Состояние сущности/агрегата». Сущность — это данные и поведение.
            0
            Приведите конкретный пример.
            0

            dddsample-core — это реализация на Java примеров, о которых говорит Эрик Эванс в своей книге. Они хорошо прокомментированы, так что будет легко разобраться.
            Вот например реализация сущности Cargo содержит как данные (TrackingId, origin, delivery), причем доступ к ним защищен согласно принципу инкапсуляция ООП, таки поведение (specifyNewRoute, assignToRoute,...)

              0

              Хороший пример, методы которые описываются в приведенном классе, изменяют его. Это такие своеобразные, сеттеры + обработка. Но они никогда не вызываются напрямую, вся работа с ними ведется через сервисы: assignToRoute, specifyNewRoute, deriveDeliveryProgress.


              Данный пример и позволил выявить точку нашего взаимонепонимания. Я ни в коем случае не приываю вынести подобные методы изменения атрибутов из класса Сущности. Я призываю к тому что бизнес операции не могу быть описаны в модели и их необходимо выносить на слой бизнес-логики. Пользователь не должен менять сущность напрямую, он должен вызвать процесс, который приведет к изменению Сущности. Есть еще некоторые особенности от типа системы которую мы пишем Запрос-Ответ или Событийная. Тут мы говорим про web и rest, т.е. Запрос-Ответ.


              Руби не позволяет обращаться к атрибуту напрямую все это делается только через методы, инкапсуляция никогда не нарушается.


              class Foo
                arr_accessor :bar
              end
              
              # это по факту:
              
              class AnotherFoo
                def bar=(val)
                  @bar = val
                end
              
                def bar
                  @bar
                end
              end

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

                0

                Согласно концепции DDD отделять доменную логику от агрегата в сервис (Domain Service) нужно в том случае, если ее нельзя отнести к какой-то конкретной сущности или объекту-значению.


                • Операция не принадлежит ни одному из объектов предметной области
                • Операция выполняется над различными объектами предметной области

                Злоупотребление приводит к «анемичной модели предметной области», как я говори ранее. Так что оставьте возможность реализации доменной логики в агрегате, если не хотите расстраивать Мартина и Эрика :)

                  0

                  Чтобы расстроить Мартина, надо понять что они имеют ввиду в этой статье как раз делается вывод, что к Entity нет доступа напрямую.


                  Сервисы добавляются потому, что есть разные сценарии использования _Сущности.


                  Сценарий "Включение телевизора": У нас есть девочка, которая включает телевизор. Она может включить его кнопкой, а может с пульта. Причем кнопкой она может и не дотянуться, т.к. у нее маленький рост.


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


                  Методы изменения в сущности — ок. Сценарии использования — не ок. И сущность не может изменить себя сама.

                    0
                    В вашем примере как минимум три сущности: девочка, пульт и телевизор. Именно девочка выполняет действие «включить телевизор», в рамках которого она понимает, что не может достать до кнопки (зная свой рост), она «ищет пульт» (еще одно действие девочки), и нажимает на нем кнопку. Пульт посылает сигнал телевизору. Телевизор включается, в ответ на сигнал пульта. Все операции — это поведение сущностей, а не сервисов.
                      0
                      Не соглашусь. Во-первых такая схема работает ровно до того момента, пока не появится мальчик — или дублирования кода, или миксины или множественное наследование.
                      Во-вторых девочка возможно имеет методы use, take, age, но точно она не должна знать как включать телевизор, это явный RDM.
                        0

                        В таком случае вы не до конца понимаете концепции DDD: единый язык, изолированные контексты, сущности и агрегаты — это части предметной области (домена), которые должны отражать моделируемые объекты из реального мира и одинаково трактоваться и бизнес экспертами и аналитиками и разработчиками ("девочка" — это сущность, а не сервис) и находить соответствующее отражение в коде.


                        А вы все сводите только сервисам и "голым" моделям. Это точно не DDD и не ООП, а возврат к процедурному программирование.


                        Посмотрите как Верон моделирует домен с помощью Event Storming. Он выделяет события, агрегаты и команды. Агрегаты исполняют команды (реализуют поведение), "выбрасывая" события. Сервисы вторичны в этой истории, и используются для моделирования процессов над агрегатами (бизнес логики вне агрегатов).

                          0

                            0
                            Да, именно это! Агрегаты исполняют команды (реализуют поведение), «выбрасывая» события. Наконец мы друг друга поняли.
                        0
                        Взаимодействие девочки с окружающими её системами, будь оно заключено в классе девочки так как в этом примере, экспоненциально увеличивает сопряженность системы.
                        Как будет изменяться ответственность девочки когда у телевизора появится «переключить канал», «вставить кассету», «настроить звук», а далее девочке надо будет управлять чайником, автомобилем, хахалем? Сколько связей будет у этого класса?
                          0
                          Связей будет столько — сколько есть в вашем бизнес домене. Для упрощения реализации используйте разбиение домена на изолированные контексты (в каждом из них будет своя девочка со связями нужными только в этом конексте, а значит их будет меньше). А вы для упрощения решения задачи предлагаете не разбивать домен на изолированные контексты, а создавать кучу разных сервисов в одном контексте. Это не DDD!

                          Не заменяйте сущности предметной области сервисами для упрощения разработки — это «коверкает» единый язык контекста и противоречит самой концепции DDD, где мы пытаемся моделировать предметную область на одном языке между программистами и экспертами.
                      0
                      И соглашусь, что последний абзац стоит переписать.

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

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