Комментарии 18
Деньги это не просто число, это число (сумма) + валюта
Ну да, есть такой паттерн. Только вот допустим, если посмотреть количество скачиваний Nuget-пакета для c# — можно удивиться какая низкая популярность. Видимо, большинство фигачит через decimal.
Представим, что мы создаем сайт онлайн-казино "Три топора"
А вы не хотите напомнить читателям, что игорная деятельность в рунете запрещена законом? Вот буквально сегодня прошла новость "ФНС нашла способ закрыть крупнейшее нелегальное интернет-казино".
Не знаю какая позиция самого издательского дома ТМ по отношению к таким статьям (вряд ли это проплаченная реклама), но вот допустим во многих новостных изданиях я вижу постоянные скобки после названий террористических организаций — мол, это запрещённая в РФ организация. В общем, прямо по краю упоминание. Можно было бы и более нейтральный пример подобрать к статье.
Касательно денег, зачастую, в базе данных, для того чтобы сохранить деньги, используются поля с плавающей запятой. Это не совсем удобно, в дальнейшем возникают ряд ошибок связанных с округлением. По факту, не может в учете указваться половина копейки. Удобнее все хранить в фракциях — копейках, центах и.т.д., использовать целочисленный тип. Валюта тоже важна, кроме собственно валюты, она показывает разрядность тех или иных фракций. Если у рубля и доллара это одна сотая, то у иены ее нет, у сатоши одна девятимиллионная. Валюты могу так же разделять тестовые и реальные платежи. Если в проекте все эти проблемы не стоят, проще использовать decimal и не усложнять свою жизнь. В руби gem 'money' достаточно популярен, 2000 звезд на GitHub хороший показатель.
Относительно онлайн казино, я не смотрел с этой точки зрения. Для меня как разработчика, это просто сложная система, где можно применить DDD. Возможно, у кого-то, легкий сарказм, в сторону надоедливой рекламы, вызовет улыбку. Объемную сухую статью без вставок читать тяжело. Никого не призываю разрабатывать онлайн казино и нарушать законы.
После последнего абзаца, как-то сильно "запахло" анемичными моделями.
Не хочется скатываться до холивара. Ко всему надо подходить рационально и не уходить в крайности. Давайте разберем пример.
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 секунд) не следует хранить в Сущности, так как это внешний процесс.
Если предложенный мной пример не раскрывает сути вопроса, предложите пожалуйста свой, попробуем его разобрать.
Ко всему надо подходить рационально и не уходить в крайности.и
класс Сущность не имеет никаких методов собственного измененияне совместимы. Согласно ООП и DDD, сущности точно могут менять собственное состояние. А вы им это не даете делать в принципе — это точно ошибка.
Вы пытаетесь подменить понятие «Сущность/Агрегат» на понятие «Состояние сущности/агрегата». Сущность — это данные и поведение.
dddsample-core — это реализация на Java примеров, о которых говорит Эрик Эванс в своей книге. Они хорошо прокомментированы, так что будет легко разобраться.
Вот например реализация сущности Cargo содержит как данные (TrackingId, origin, delivery), причем доступ к ним защищен согласно принципу инкапсуляция ООП, таки поведение (specifyNewRoute, assignToRoute,...)
Хороший пример, методы которые описываются в приведенном классе, изменяют его. Это такие своеобразные, сеттеры + обработка. Но они никогда не вызываются напрямую, вся работа с ними ведется через сервисы: assignToRoute, specifyNewRoute, deriveDeliveryProgress.
Данный пример и позволил выявить точку нашего взаимонепонимания. Я ни в коем случае не приываю вынести подобные методы изменения атрибутов из класса Сущности. Я призываю к тому что бизнес операции не могу быть описаны в модели и их необходимо выносить на слой бизнес-логики. Пользователь не должен менять сущность напрямую, он должен вызвать процесс, который приведет к изменению Сущности. Есть еще некоторые особенности от типа системы которую мы пишем Запрос-Ответ или Событийная. Тут мы говорим про web и rest, т.е. Запрос-Ответ.
Руби не позволяет обращаться к атрибуту напрямую все это делается только через методы, инкапсуляция никогда не нарушается.
class Foo
arr_accessor :bar
end
# это по факту:
class AnotherFoo
def bar=(val)
@bar = val
end
def bar
@bar
end
end
На хабре есть кнопка Ответить, под каждым комментарием, чтобы не создавать новую ветвь дискуссии.
Согласно концепции DDD отделять доменную логику от агрегата в сервис (Domain Service) нужно в том случае, если ее нельзя отнести к какой-то конкретной сущности или объекту-значению.
- Операция не принадлежит ни одному из объектов предметной области
- Операция выполняется над различными объектами предметной области
Злоупотребление приводит к «анемичной модели предметной области», как я говори ранее. Так что оставьте возможность реализации доменной логики в агрегате, если не хотите расстраивать Мартина и Эрика :)
Чтобы расстроить Мартина, надо понять что они имеют ввиду в этой статье как раз делается вывод, что к Entity нет доступа напрямую.
Сервисы добавляются потому, что есть разные сценарии использования _Сущности.
Сценарий "Включение телевизора": У нас есть девочка, которая включает телевизор. Она может включить его кнопкой, а может с пульта. Причем кнопкой она может и не дотянуться, т.к. у нее маленький рост.
Проверка роста, точно, не должна находится в модели телевизора — выносим в сервис. Поиск пульта в другой сервис. Метод включения для Сущности Телевизор который включает светодиод и матрицу — ок.
Методы изменения в сущности — ок. Сценарии использования — не ок. И сущность не может изменить себя сама.
Во-вторых девочка возможно имеет методы use, take, age, но точно она не должна знать как включать телевизор, это явный RDM.
В таком случае вы не до конца понимаете концепции DDD: единый язык, изолированные контексты, сущности и агрегаты — это части предметной области (домена), которые должны отражать моделируемые объекты из реального мира и одинаково трактоваться и бизнес экспертами и аналитиками и разработчиками ("девочка" — это сущность, а не сервис) и находить соответствующее отражение в коде.
А вы все сводите только сервисам и "голым" моделям. Это точно не DDD и не ООП, а возврат к процедурному программирование.
Посмотрите как Верон моделирует домен с помощью Event Storming. Он выделяет события, агрегаты и команды. Агрегаты исполняют команды (реализуют поведение), "выбрасывая" события. Сервисы вторичны в этой истории, и используются для моделирования процессов над агрегатами (бизнес логики вне агрегатов).
Как будет изменяться ответственность девочки когда у телевизора появится «переключить канал», «вставить кассету», «настроить звук», а далее девочке надо будет управлять чайником, автомобилем, хахалем? Сколько связей будет у этого класса?
Не заменяйте сущности предметной области сервисами для упрощения разработки — это «коверкает» единый язык контекста и противоречит самой концепции DDD, где мы пытаемся моделировать предметную область на одном языке между программистами и экспертами.
Поваренная книга разработчика: DDD-рецепты (4-я часть, Структуры)