Сервисный слой, как правильно?

Обсуждаем, как правильно строить приложения
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

ElisDN писал(а):
Melodic писал(а):Ну и как тогда быть?
Потребность в связях и lazy load обычно возникает при первой же попытке отображать на страницах сами сущности домена. Проблема полностью исчезает, если для листингов и вывода использовать не сами сущности, а отдельные DTO с нужными для каждого листинга наборами полей. В итоге в Repository можно оставить только методы find(), add(), save() и remove(), нужные для непосредственной работы с сущностями, а все остальные для выборок с критериями, паджинациями и сортировками переместить в отдельный ReadRepository.
это понятно если мы говорим о презентационном слое. А в доменном слое у нас может быть такое $post->addTag(Tag $tag) и мы не сможем корректно обработать эту ситуацию без остальных загруженных в $post десяти тегов.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

zelenin писал(а):А в доменном слое у нас может быть такое $post->addTag(Tag $tag) и мы не сможем корректно обработать эту ситуацию без остальных загруженных в $post десяти тегов.
Для сущностей Post и Tag со своими репозиториями в Post::$tags будет всего лишь коллекция идентификаторов с добавлением по $post->addTag($tag->getId()). Сами теги для этого в посте не нужны. Так что загружать всю сущность с VO и айдишниками тегов жадно - не проблема.

А если VO слишком много для жадной загрузки, то у некоторых встречал в репозитории помимо find($id) ещё и методы вроде findWithTags($id) и т.п. для выборочной прогрузки только нужных для каждого юзкейса фрагментов.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

ElisDN писал(а):Для агрегатов Post и Tag со своими репозиториями в Post::$tags будет всего лишь коллекция идентификаторов с добавлением по $post->addTag($tag->getId()). Сами теги для этого в посте не нужны. Так что загружать всю сущность с VO и айдишниками тегов жадно - не проблема.
Post не есть агрегат без тегов. Хранение id тегов - это протечка слоя хранения в домен. Ты на книжной полке хранишь книги, а не корешки от них. Агрегат - это то, из чего можно получить коллекцию тегов $post->getTags().
ElisDN писал(а):А если VO слишком много для жадной загрузки, то у некоторых встречал в репозитории помимо find($id) ещё и методы вроде findWithTags($id) и т.п. для выборочной прогрузки только нужных для каждого кейса фрагментов.
ну собственно варианты lazy load я разбирал выше. Это не lazy load. Ищем самый лучший вариант, чтобы по $post->getTags() получить коллекцию тегов.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

zelenin писал(а):Post не есть агрегат без тегов.
Если Tag имеется только в посте, то его место в агрегате Post с записью по $post->addTag($tagName) и получением через $post->getTags().

Если Tag - это отдельный агрегат (как Category) со своим репозиторием, то он полноценный объект с getId() и получением через $tagRepository->getAllByPostId($post->getId());

Иногда мы не грузим всё в Category и не делаем $category->addProduct($product) и $category->getProducts(), когда у нас миллиарды товаров. Логичнее делать $product->assignToCaregory($category->getId()) и получать $productRepository->getAllByCategory($category->getId(), $pager).

При таком подходе жадно подгружаем только айдишники. Ни ленивая, ни жадная загрузка самих связанных сущностей в сам агрегат не пригождается.
Последний раз редактировалось ElisDN 2016.08.04, 00:06, всего редактировалось 2 раза.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

ElisDN писал(а):
zelenin писал(а):Post не есть агрегат без тегов.
Если Tag - это VO, то его место в агрегате Post с записью по $post->addTag($tagName) и получением через $post->getTags().

Если Tag - это сущность (как Category), то он полноценный объект с getId() и получением через $tagRepository->getAllByPostId($post->getId());

Мы же не грузим всё в Category и не делаем $category->addProduct($product) и $category->getProducts(), когда у нас миллиарды товаров. Логичнее делать $product->assignToCaregory($category->getId()) и получать $productRepository->getAllByCategory($category->getId(), $pager).
Что происходит в методе $product->assignToCategory($category->getId())? Там все id товаров, которые принадлежат категории?
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

Melodic писал(а):Что происходит в методе $product->assignToCategory($category->getId())? Там все id товаров, которые принадлежат категории?
Наоборот. Один или несколько id категорий, в которых этот товар.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

ElisDN писал(а):
Melodic писал(а):Что происходит в методе $product->assignToCategory($category->getId())? Там все id товаров, которые принадлежат категории?
Наоборот. Один или несколько id категорий, в которых этот товар.
Ой, не внимательно прочитал название метода.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

ElisDN писал(а):Если Tag - это сущность (как Category) со своим репозиторием, то он полноценный объект с getId() и получением через $tagRepository->getAllByPostId($post->getId());
репозитории создаются для агрегатов. В агрегат Post входит сущность Tag. Агрегат Post выбирается из PostRepository. На выходе я должен получить объект, который с помощью метода getTags() вернет коллекцию сущностей Tag.
ElisDN писал(а):Мы же не грузим всё в Category и не делаем $category->addProduct($product) и $category->getProducts(), когда у нас миллиарды товаров. Логичнее делать $product->assignToCaregory($category->getId()) и получать $productRepository->getAllByCategory($category->getId(), $pager).
Все верно, только ты перевернул ситуацию. Мы не в категорию добавляем товар, а товару присваиваем ОДНУ категорию. В случае с постом все ровно наоборот - мы посту присваиваем много тегов. То есть не тегу присваиваем много постов, а посту много тегов.
Агрегат - это логическая или транзакционная единица. Пост - это сущность с тегами и автором. Она существует как единица. Единицей вытаскивается из репозитория, единицей сохраняется в репозитории. Собственно это базисное понятие DDD. Классический пример Order+OrderLines

UPD http://stackoverflow.com/a/1958765/1320921
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

Melodic писал(а):Т.е. для в каждом экшене контроллера формировать свой DTO, который и будет отображаться во вьюхе?
Виджету последних статей нужны только title, date, photo, description и вычисляемое commentsCount. Гриду в админке - title, date, categoryName, authorName, status. Странице самого поста - title, text, date, photo и все связи category, tags, author, comments, relatedPosts.

Возвращать массивы оригинальных сущностей Post с полной грудой данных - слишком прожорливо. Логично для каждой выборки сделать отдельные DTO только с нужными данными и возвращать их из getLatestPosts($limit) и т.п.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

zelenin писал(а):Агрегат - это логическая или транзакционная единица. Пост - это сущность с тегами и автором. Она существует как единица. Единицей вытаскивается из репозитория, единицей сохраняется в репозитории. Собственно это базисное понятие DDD. Классический пример Order+OrderLines.
Да. Всё верно. У Эванса:
The root is the only member of the AGGREGATE that outside objects are allowed to hold references to.
и в ответе:
This means that aggregate roots are the only objects that can be loaded from a repository.

An example is a model containing a Customer entity and an Address entity. We would never access an Address entity directly from the model as it does not make sense without the context of an associated Customer. So we could say that Customer and Address together form an aggregate and that Customer is an aggregate root.
Сущностью (ну и агрегатом) является объект, обладающий идентификатором. По нему и извлекается и сохраниется в репозиторий. Только на сущности и агрегаты можно ссылаться извне из других объектов и создавать репозитории (так как есть идентификатор).

А агрегат состоит из вложенных сущностей с идентификатором и ValueObjects без глобального идентификатора, существующих только внутри агрегата. Методами самого агрегата они создаются, редактируется и удаляются. Ни одна другая сущность не может получить доступ к чужому VO в обход самого агрегата.

Как Address - это просто часть в Customer, так и массив из OrderLine у нас имеется только в Order. Мы их создаём через методы агрегата $customer->changeAddress($country, $city) и $order->addLine($product->getId(), $price, $count). И просматриваем их от $customer->getAddress() и $order->getLines(). Мы никогда не выводим в админке напрямую грид из Address без Customer или OrderLines без Order. Это не имеет смысла. У OrderLines будет локальный идентификатор, но он снаружи никому не интересен. И, соответственно, для них у нас нет репозитория.

Если Tag и Author - просто обёртки над string, то да, их сохраняем прямо в Post в $postRepository.
А если Tag, Author (User) и Category являются полноценными агрегатами со своими первичными getId(), то это уже сложно. Мы выводим их в отдельных CRUDах, добавляем, переименовываем, удаляем. Получаем их список для вывода облака или списка в виджете и ссылаемся на них из разных постов, следим за уникальностью. Это отдельные полноценные сущности или агрегаты, имеющие свои репозитории.

Так что свои запчасти VO мы храним внутри агрегата, а на другие агрегаты для экономии связей удобнее ссылаться по id. Если вдруг из двух агрегатов потребуется сослаться на одну запчасть или выводить и редактировать её отдельно, то это повод вынести её в отдельный агрегат с репозиторием. А в оригинальном DDD действительно можно всё друг в друга вкладывать.
Последний раз редактировалось ElisDN 2016.08.04, 00:13, всего редактировалось 3 раза.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

ElisDN писал(а):Сущностью (ну и агрегатом) является объект, обладающий идентификатором. По нему и извлекается и сохраниется в репозиторий. Только на сущности и агрегаты можно ссылаться извне из других объектов и создавать репозитории (так как есть идентификатор).
все верно: Post может быть агрегатом, состоящим из сущностей Post и Tag, извлекаемым из PostRepository. Tag может быть агрегатом из одной сущности Tag, извлекаемым из TagRepository.
ElisDN писал(а):А агрегат состоит из вложенных ValueObjects - объектов без глобального идентификатора, существующих только внутри агрегата.
Агрегат состоит и из VO, не имеющих id, и из Entity, имеющих id. Агрегат состоит из объектов. Агрегат не содержит Id вместо сущностей.
http://i.imgur.com/SEq8DP2.png
https://github.com/idr0id/ddd-blog/blob ... y/User.php
ElisDN писал(а):Как Address - это просто часть в Customer, так и массив из OrderLine у нас имеется только в Order. Мы их создаём через методы агрегата $customer->changeAddress($country, $city) и $order->addLine($product->getId(), $price, $count). И просматриваем их от $customer->getAddress() и $order->getLines(). Мы никогда не выводим в админке напрямую грид из Address без Customer или OrderLines без Order. Это не имеет смысла. У OrderLines будет локальный идентификатор, но он снаружи никому не интересен. И, соответственно, для них у нас нет репозитория.
у OrderLine есть не локальный идентификатор, а локальная идентифицируемость с помощью id, в отличии от VO, которые локально не идентфиицируемы. То есть локальный иднетификатор - это не какой id, который отличается от глобального, а тот же самый, по которому мы можем orderline изменить напрямую в составе собственного агрегата. поэтому это сущность, она однозначна идентифицируема, и конкретная одна из десяти может быть мною изменена. Я могу увеличить count OrderLine(id=15) у Order(id=2) с 3 на 4, однозначно выбрав определенный OrderLine из имеющихся. С VO я так сделать не могу.
ElisDN писал(а): А если Tag, Author (User) и Category являются полноценными сущностями со своими первичными getId(), то это уже не VO.
а оно и не должно быть VO. Tag может быть частью агрегата Post, когда мы редактируем пост (/post/16/edit), а может быть частью агрегата Tag, когда мы добавляем теги в гриде (/tag/index), как ты и сказал.
ElisDN писал(а):Так что свои запчасти VO мы храним внутри агрегата, а на другие сущности или агрегаты ссылаемся по id. Если вдруг из двух агрегатов потребуется сослаться на одну запчасть или выводить и редактировать её отдельно, то это повод вынести её в отдельную сущность с getId() и репозиторием.
но вот это все измышления. В DDD мы оперируем сущностями, а не ID.

ну и Вон Вернон, автор второй популярной книги по ddd, разжевавший все вводные обтекаемые концепты Эванса - https://github.com/VaughnVernon/IDDD_Sa ... /Team.java
Есть агрегат Team, есть связанные сущности - однозначно идентифицируемые сущности, без намека на VO-шность - члены команды и владелец продукта. Связанные сущности добавляются в корень агрегата объектами. В этом весь ddd. Никаких id.

http://blog.byndyu.ru/2010/05/domain-driven-design.html решение 3 - аналогично добавляем сущность Заказ к юзеру.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

zelenin писал(а):В DDD мы оперируем сущностями, а не ID.
Да, в идеальном DDD в каждом контексте всё нужно делать вложенными сущностями и коллекциями по решению 3. Но на практике с точки зрения производительности часто приходится нарушать инкапсуляцию и уступать в сторону варианта 2 c Service::addOrder($account, $order). Иначе, как уже затронули в теме, нужно либо реализовывать ленивые загрузки через прокси, либо загружать все связи сразу жадно, либо указывать дополнительным параметром в find(), какие именно связи сейчас нужны; либо для проверок в addOrder() инъектить сервис с репозиторием в сущность вроде addOrder(Order $order, OrderChecker $checker), который бы вместо foreach ($this->orders) { if (...) } дёргал $repository->hasOrders($criteria) или в $this->orders хранить Query-объект, либо для каждого контекста делать отдельную сущность только со своими связями... Ведь непонятно, как с этим жить, когда связанных данных тысячи или миллионы и ещё есть связи связей. Здесь я, увы, ещё не познал дзен :)
Последний раз редактировалось ElisDN 2016.05.26, 09:55, всего редактировалось 2 раза.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

ElisDN писал(а):
zelenin писал(а):В DDD мы оперируем сущностями, а не ID.
Да, в идеальном DDD в каждом контексте всё нужно делать вложенными сущностями и коллекциями по решению 3. Но на практике с точки зрения производительности часто приходится нарушать инкапсуляцию и уступать в сторону варианта 2 c Service::addOrder($account, $order). Иначе, как уже затронули в теме, нужно либо реализовывать ленивые загрузки через прокси, либо загружать все связи сразу жадно, либо указывать дополнительным параметром в find(), какие именно связи сейчас нужны; либо для проверок в addOrder() инъектить сервис с репозиторием в сущность вроде addOrder(Order $order, OrderChecker $checker), который бы вместо foreach ($this->orders) { if (...) } дёргал $repository->hasOrders($criteria) или в $this->orders хранить Query-объект, либо для каждого контекста делать отдельную сущность только со своими связями... Ведь непонятно, как с этим жить, когда связанных данных тысячи или миллионы и ещё есть связи связей. Здесь я, увы, ещё не познал дзен :)
собственно об этом и рассуждали - как познать дзен без боли. Имхо самый приемлимый способ - инджектить ресолвером перед обращением к связаным сущностям. Либо проксировать, но отказываемся от final в угоду реализации другого слоя, что не оч. корректно.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
array_diff типа. Доктрина из коробки это делает.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
array_diff типа. Доктрина из коробки это делает.
Что бы не городить свой велосипед, стоит использовать Доктрину?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):
Melodic писал(а):Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
array_diff типа. Доктрина из коробки это делает.
Что бы не городить свой велосипед, стоит использовать Доктрину?
я бы не стал - слишком тяжела.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):
zelenin писал(а): array_diff типа. Доктрина из коробки это делает.
Что бы не городить свой велосипед, стоит использовать Доктрину?
я бы не стал - слишком тяжела.
Т.е. альтернатив никаких нет? Придётся свой велосипед делать?)

И что бы сделать array_diff, нужно будет ещё раз загрузить Order?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):
Melodic писал(а):
Что бы не городить свой велосипед, стоит использовать Доктрину?
я бы не стал - слишком тяжела.
Т.е. альтернатив никаких нет? Придётся свой велосипед делать?)
ну а что там делать? в методе save делаете проверку. Как? Либо напрямую запросами, либо с помощью какого-то реестра, в котором будут храниться начальные сущности. Опять же вы не сможете создать сущность, не сгенерировав id в приложении (мы общались об этом в личке).
Мое мнение насчет доктрины: лучше сделать в пять раз менее функциональное решение, но в пять раз более быстрое.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):И что бы сделать array_diff, нужно будет ещё раз загрузить Order?
existById/existByIds - собственно можно что-то и другое придумать. Реализация ваша.
Закрыто