Pull to refresh
75
0
Zaur Nasibov @BasicWolf

Software Engineer

Send message

Благодарю за перевод замечательной статьи!

"Крайняя статья". Даль и Ожегов в гробу перевернулись.

Удивило, что автор ссылаясь на Хайнлайна, не вспоминает "Starship troopers", где гражданин через службу в армии показывал, что ставит интересы общества выше своих личных интересов.

На практике проброс какого бы то ни было сервиса в контроллер по интерфейсу создает только сложности в поддержке.
Проблем с пробросом сервиса с бизнес-логикой в контроллер в моей практике никогда не было.

А если, один и тот же use case вызывается не только HTTP контроллером, но например по сообщению из очереди, или через определённые интервалы?
И как вы тестируете подобную конструкцию? Как изолируете и замещаете (mocking) слои в юнит-тестах?

По вашему описанию это (Application service) выглядит как лишняя сущность. Бизнес-логика это набор конкретных действий "Делаем это, делаем это, делаем то". Тот код, где они перечисляются, и есть бизнес-логика. Делать для него еще какие-то обертки нет необходимости.

Да ну? Надо срочно Вернону передать, чего это он надумал об Application Service-ах и говорит о них с первых страниц Красной книги? А потом ещё посвящает им целую главу!

The Application Services are the direct clients of the domain model. These are responsible for task coordination of use case flows, one service method per flow. When using an ACID database, the Application Services also control transactions, ensuring that model state transitions are atomically persisted.
... Keep Application Services thin, using them only to coordinate tasks on the model.

В Синей книге конечно всё куда менее разжёванно, но тем не менее:

For example, if the banking application can convert and export our
transactions into a spreadsheet file for us to analyze, that export is an application service. There is no meaning of "file formats" in the domain of banking, and there are no business rules involved.
On the other hand, a feature that can transfer funds from one account to another is a domain service because it embeds significant business rules.

Но ещё лучше об этом написано в "другой Красной книге", ака Patterns, Principles and Practices of Domain-Driven Design [Skott Millet, Nick Tune]. Глава 25я, "Commands: Application Service Patterns for Processing Business Use Cases":

As a starting point, you can think of application services as having two general responsibilities. First, they are responsible for infrastructural concerns: managing transactions, sending e‐mails, and similar technical tasks. In addition, application services have to coordinate with the domain to carry out full business use cases. Carrying out these responsibilities correctly helps prevent domain logic from being obfuscated or incorrectly located in application services.

Хм, тут я немного засомневался, может мы просто говорим об одном и том же, но называем их разными именами? Или вы всё-таки говорите о HTTP-контроллерах?

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

Ах вот оно что! Но доступно для чего? Для каких целей? Мне не хочется, чтобы кто угодно мог писать что попало в мои поля. Я здесь не о геттерах и сеттерах, а допустим о коллекции Order.items у которой тип ArrayList и которую вы предлагаете выставлять напоказ. Где гарантии, что туда не запишут какую-нибудь белиберду? А ведь это можно сделать, т.к.
Order.items.add(...) - и здесь нет ни одного инварианта.
НО! При этом я бы оставил публичное поле (геттер) Order.items, возвращающий read-only коллекцию.

Да я вроде и не говорил, что это одна сплошная операция.

Пардон, невнимательно читал.

Это бизнес-логика создания заказа. Есть шаг "сохранить данные в базу", есть "отправить письмо"

Отправить письмо - да, бизнес-логика. Сохранить данные - да. "В базу" - реализация :) И да, это всё один use case.

Тогда модели остаются чистыми, а зависимости пробрасываются только в конструктор сервиса и являются деталями реализации бизнес-логики, а не частью интерфейса бизнес-методов.

Я вспомнил эту статью всвязи с

Inject out-of-process dependencies into the domain model — Keeps performance and domain model completeness, but at the expense of domain model purity.

Т.е. теоретически модель может также отвечать за отправку писем, причём напрямую, а не через события. Для этого достаточно передать ей функцию/коллбэк "отправь письмо".

def create_order(email_sender: Function):
    ...
    email_sender.send("Congratulations, your order have been created!")

Выглядит вырвиглазно.

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

Вот вы прочли статью того же Хорикова. При всей многогранности и сложности, Domain Service не предлагается как решение в принципе (хотя в обсуждении к статье спрашивают, почему бы их не использовать? http://disq.us/p/2b0xejg)

Но ладно Хориков. Во ВСЕХ книгах, приведённых выше, анемичная модель рассматривается как негативная практика. Исключение делается в функциональном подходе, но не в ООП.

Тот же Миллет:

A common opinion that many DDD practitioners share is that entities should be behavior oriented. This means that an entity’s interface should expose expressive methods that communicate domain behaviors instead of exposing state. More generally, this is closely related to the OOP principle of “Tell Don’t Ask.”

A Фаулер написал на эту тему ещё в 2003м году: https://www.martinfowler.com/bliki/AnemicDomainModel.html

Я говорю про domain service, он вызывается из контроллера. Application service в этой схеме не нужен.

С точки зрения гексагональной архитектуры вы жёстко связываете адаптер и application. Адаптер должен общаться с приложением через интерфейс.

Так это уже реализация бизнес-логики. И email оттуда же можно отправлять.

Почти. Application Service - это координатор, он скорее дирижирует бизнес-логикой.

Он завязан на сущность, на то он и сервис с бизнес-логикой, но не на реализацию.

Говоря о реализации я имею ввиду, что сервису приходится напрямую связываться с полем. Но эту связь можно опустить, если у модели будут методы, отражающие бизнес-операции.

Нет, на уровне бизнес-требований это одна вещь, порядок создания заказа.

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

Поэтому вынесение этого в обработчик сообщений это размазывание бизнес-логики по разным классам.

Безусловно, пихать всё в один класс - ещё то извращение. Технически реализуемо - можно, если передавать функцию отправки письма из Application Service-a в доменную модель или сервис. У Хорикова на эту тему хорошая статья Domain model purity vs. Domain model completeness.

Но я всё-таки вернусь к дискуссии об использовании Domain Service вкупе с анемичной моделью против богатой доменной модели. Не поленился заглянуть в Синюю книгу. Вот что пишет Эванс:

It can be harder to distinguish application SERVICES from domain SERVICES. The application layer is responsible for ordering the notification. The domain layer is responsible for determining if a threshold was met— though this task probably does not call for a SERVICE, because it would fit the responsibility of an "account" object.

Здесь, как видите, Эванс явно указывает на реализацию логики в доменной модели, а не в сервисе. Однако далее:

On the other hand, a feature that can transfer funds from one account to another is a domain SERVICE because it embeds significant business rules (crediting and debiting the appropriate accounts, for example) and because a "funds transfer" is a meaningful banking term. In this case, the SERVICE does not do much on its own; it would ask the two Account objects to do most of the work. But to put the "transfer" operation on the Account object would be awkward, because the operation involves two accounts and some global rules.

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

Этого показалось недостаточно, пришлось открыть Красную книгу.

Under what conditions would an operation not belong on an existing Entity or Value Object? It is difficult to give an exhaustive list of reasons, but I’ve listed a few here. You can use a Domain Service to:
* Perform a significant business process
* Transform a domain object from one composition to another
* Calculate a Value requiring input from more than one domain object
...
It’s a very common one, and that kind of operation can require two, and possibly many, different Aggregates or their composed parts as input. And when it is just plain clumsy to place the method on any one Entity or Value, it works out best to define a Service.

Это в целом то же, что говорил Эванс. Только Вернон явно указывает на when it is just plain clumsy to place the method on any one Entity.... Clumsy - неуклюжий, неловкий, неповоротливый, топорный, но для кода, мы скорее скажем "неуместный".
Т.е. по Вернону писать

orderService.addItem(order, addItemCommand);

имеет смысл тогда, когда

order.addItem(item)

- неуместно.

И здесь мы расходимся во мениях. Ну чтож, плюрализм мнений - благо! Засим откланяюсь.

orderService.addItem(order, addItemCommand);

Отлично! Копнём на один уровень глубже - что же будет происходить в этом методе? И вы сами отвечаете

При этом там вполне можно написать и так:

order.addItem(item)
entityManager.save(order);

Так я и о том же. Сервис (application service, не domain service) не несёт в себе бизнес-логики. Он связывает сущности и остальную инфраструктуру, но он не несёт в себе бизнес правил.

Тут вот кстати появляется вопрос, где в вашем подходе запускать транзакцию и вызывать entityManager.save(). Не в контроллере же, неужели тоже в сущности?

В Application Service, который реализует определённые use case-ы. T.e.:

class OrderService(
    AddItemToOrderUseCase,
    ...
)
    get_or_create_order: GetOrCreateOrderPort
    save_order: SaveOrderPort

    @transactional
    def add_item_to_order(self, order_id, item):
        order = self.get_or_create_order(order_id)
        order.add_item(item)
        self.save_order(order)

Но сам по себе набор свойств сущности это не детали реализации

А что же? Если сервис будет напрямую модифицировать поля объекта - значит от уже завязан на их реализацию и наоборот.

Почему бизнес-требование "При создании отправить письмо" может быть реализовано вне сущности, а бизнес-требование "При создании установить текущую дату создания" не может?

Потому что "создание" в первом и втором случае - разные вещи. Я предполагаю, что письмо можно отправить лишь после того, как "заказ будет принят", т.е. будет тем или иным образом сохранён в системе. Значит "при создании заказа отправить письмо" - сформулировано не верно и надо обновлять Ubiquitous Language на который опираются все участники разработки продукта :)

Заказ можно создать независимо от того, анемичная у вас модель или нет. Создание заказа это бизнес-действие.
Бумажный документ сам себя не заполняет и не проверяет правильность своего заполнения.

Интересное замечание. Но ведь "создание заказа" - это совершенно разные операции в зависимости от контекста. Рассмотрим модель, в которой "созданый заказ" означает, что клиент заказал товар, но пока его не оплатил. Если клиент захочет изменить заказ - добавить товар, неужто вы напишите что-то вроде

client.add_item_to_order(order, item)

Или, всё-таки

order.add_item(item)

А почему вы считаете, что "поведение" это именно методы внутри сущности?

Потому что за поведением гораздо проще наблюдать и управлять, когда оно описано рядом с данными, от которых и которые от него зависят. Если же оно раскидано по коду, то будет гораздо сложнее понять поведение модели в целом, да и от инкапсуляции данных не останется и следа:

class Client:
    def add_item_to_order(self, order, item):
        order.items.append(item)  # ой, а с чего это вдруг Client знает о внутренностях Order?

Вы говорите о типизации - я согласен! Но ведь типизация может быть утиной. Она может быть описана в интерфейсе. Мой же комментарий, в первую очередь - об избыточности Гексагональной архитектуры и DDD в приложении типа "Что получил в POST , то и записал в базу данных, что лежит в базе данных, то и вернул в GET".

У нас есть бизнес-требование "После создания заказа отправить письмо на электронную почту пользователя". Как вы будете отправлять его из сущности, как будете пробрасывать зависимости, которые это делают?

События домена. Что-то вроде:

order, domain_events = shopping_cart.create_order()
event_dispatcher.dispatch(domain_events)

Здесь, domain_events будет содержать одно (или более!) событий:

[OrderCreatedEvent(id, client_id, items), ...]

- Ну уж извините, Паганель! - вмешался майор. - Вы никогда не заставите меня поверить, что дикие звери полезны. Какая от них польза?
- Какая польза? - воскликнул Паганель. - Да хотя бы та, что они необходимы и для классификации: все эти разряды, семейства, роды, виды...
(Жюль Верн, "Дети капитана Гранта").

Если у вашей сущности нет поведения, то это либо Value-object, либо анемичная Entity-object, у которой и с которой не проводятся бизнес-действия. Если такая модель служит проводящей прослойкой между публичным API и SPI/driven-портами - нет в ней смысла.

@anny_anny, отличная и очень объёмная работа!
Есть несколько замечаний, надеюсь не будут лишними. Выше в комментариях @vasyakolobok77 спросил у вас про анемичные доменные модели. Вам не кажется, что ваши доменные модели противоречат написанному в статье? Ведь если всё что в есть в доменной модели - это геттеры и сеттеры, в чём её смысл? Если всё что нужно - передавать данные от API до Data-layer'a и обратно, не проще было бы связать контроллер напрямую с портом данных? Вы при этом ничего не нарушаете, т.к. зависимости остаются направленными в центр!

Кстати, когда программисты осваивают DDD и Гексагональную архитектуру, анемичные модели и тонны маппингов встречаются очень часто! А знаете почему? Сейчас скажу страшную вещь - DDD вообще не имеет никакого отношения к Гексагональной архитектуре!.

Вы наверняка читали статью Алистара Кокбёрна, которая популяризовала этот термин. Кокбёрн упоминает "presentation layer" единожды, в контексте "слоя API". Но ВСЁ, что лежит по внутреннюю сторону портов называется "Application". Без каких-либо application layer, domain layer, infrastructure layer и т.д. Просто DDD очень удачно вписывается в Гексагональную архитектуру - отсюда и вытекает идея, что они идут рука в руку (я кстати сам до недавных пор был в этом абсолютно уверен!).

def set_vacancy_count(count):
    if abs(count) == count:
        self._vacancy_count = count

Ну зачем такое зло? Я понимаю, что это пример, но даже в примере такое писать нельзя. Это же чистой воды "неочевидное поведение". Почему операция сеттера молча проглатывает неправильные значения?

Вообще, называть произведения Master "замками" - это неуважение к настоящим замкам. Классика от LockPickingLawyer: https://www.youtube.com/watch?v=bfDPtt-bnAI&t=31s

Вы всё детально разложили по полочкам! Из личного - в зависимости от характера ПО, стоит объяснять заказчику что это не проект типа "сделал-запустил-забыл", а продукт "сделал-запустил-развиваю". И техническая составляющая - большАя часть этого развития, от которой никуда не деться.

P.S. тем паче, частые релизы помогают узнать, не сломалось ли что-то из технической составляющей.

А как вы узнаете, сделали ли вы "то что надо клиенту", или нет, не релизнув? :)

Андрей, я вам очень рекомендую посмотреть это гениальное выступление Кевина Хенней под названием Agility ≠ Speed. А после - выступление Аллена Холуба: The Death of Agile. Они внесут немного хаоса в ваши взгляды и помогут посмотреть на знакомые вещи под другим углом.

В University of Joensuu, 2008м году, на курсе С, домашние задания заливались на сервер и прогонялись через тесты. Но это была личная инициатива и разработка препода.

Взрыв из прошлого. Как-будто вернулся в начало 2000-х и открыл свежий номер "Хакера".

Спасибо за статью! Вы очень хорошо описали преимущества команд взаимодействие которых основано на доверии и стремлении к выполнению общей цели.
Одна из любимых книг на эту тему - "Turn the ship around". У Дэвида Маргуеэта, капитана атомной подводной лодки, получилось из строго иерархической структуры воспитать культуру доверия, взаимодействия и самостоятельности в принятии решений. Это потребовало немало сил и стоического терпения. Тем не менее, лишь за пол-года лодка прошла путь от "худшего места во всём флоте" до "лучшей во всех отношениях".
Если такая трансформация возможна на подводной лодке, то значит она возможна в любой организации.

Питер, вам не стыдно? Давайте ещё статью о том как объявлять переменные опубликуем на Хабре. Более того, в статье даются откровенно вредные советы.

Из коротенького параграфа документации посвящённому лямбда-функциям:

Small anonymous functions can be created with the lambda keyword.  Semantically, they are just syntactic sugar for a normal function definition.

Код вроде этого в корне не верен:

sorted_list = lambda x: (sorted(i) for i in x)
second_largest = lambda x, func: [y[len(y)-2] for y in func(x)]

Не уверен насчёт взломать, но программировать так судя по этому комиксу можно:

Поймите же, история повторяется! Невозможно её изменить оставаясь в порочном круге. Вырезали в начале 1917 "врагов народа" (причём дважды за год!), потом в 30-х стали вырезать других "врагов народа", в 50-х оказалось что те, кто вырезал врагов - тоже враги, а предыдущих надо амнистировать, в 90-х амнистировали тех, кого вырезали в самом начале, потом начали амнистировать тех, кого резали в 50-х...

Чтобы остановить это сумасшедствие нужен другой механизм. Без насилия и ненависти. Иначе всё опять вернётся на круги своя.

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

(А. и Б. Стругацкие, "Трудно быть Богом")

1
23 ...

Information

Rating
Does not participate
Location
Азербайджан
Registered
Activity