Обновить
30
Пётр Грибанов@ghost404

Symfony professional developer

19
Подписчики
Отправить сообщение

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


Интересно былоб почитать именно про эволюцию проекта. Например разобрать штук 5 итераций, чтоб было понятней как это вообще происходит и почему выбираются те или иные решения, а почему какие-то решения отклоняются. Почитать про архитектурные ошибки и способы их решения. И я не говорю о конечных итерациях переработки кода. Многие решения не уходят дальше головы или бумаги. Их просто отбрасывают за ненадобностью или проскакивают, но для обучения и понимания они важны ИМХО.

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


DDD это не про реализацию, а про проектирование. Качественно продуманная и сформулированная предметная область легко реализуется на любом языке программирования. А вот проработать эту самую предметную область и есть основная проблема.


И не подумайте что я вас критикую. Ваше стремление похвально.


PS: Рекомендую к прочтению книгу Вон Вернона — Implementing Domain-Driven Design.

У вас тут целая куча проблем.


  1. Уберите раздел Предыстория из статьи.
    Вы в начале конфигурируете проект под Symfony, Docker, VueJs и прочее, но в статье они ни как не фигурируют больше.
    Ваща статья исключительно про DDD. VueJs только пару раз упомянулся, но на практике не использовался.
    Возможно вы будете их использовать в следующих статьях.
    Вот тогда и напишете про Docker и VueJs.


  2. Сходу проблемы с DDD.


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

    Вы Фонд потеряли в своем проекте. Все финансовые транзакции делаются через Фонд накопления средств, а не через сущность Желание.
    Я бы их вообще разделил на 2 отдельных контекста (Bounded Context).


  3. Как уже сказали, AbstractId::next() лучше вынести в сервис генерации id.


    interface WishIdGenerator
    {
       public function next(): WishId;
    }

  4. Я бы не привязывался так явно к UUID.
    Вдруг захотите сменить генератор id.
    Я сейчас готовлю статью по использованию более оптимального id чем UUID.


  5. Статические фабричные методы AbstractId::fromString() и Expense::fromCurrencyAndScalars вам вообще не нужны.
    Вы все должны деалть через конструктор и передавать в него явные значения, а не генерировать VO внутри.


  6. Использование getter-ов и setter-ов это известный DDD антипаттерн.
    Лучше переименовать методы:


    • AbstractId::getId() в AbstractId::id()
    • WishName::getValue() в WishName::name()
    • Expense::getCurrency() в Expense::сurrency()
    • Wish::getFund() в Wish::fund()
      и т.д.
      Кто-то может со мной не согласится, но по мне так префикс get тут лишний.

  7. publish/unpublish


    например, вы можете отложить его до лучших времен

    Вы явно описали действие: отложить
    Тоесть действия у вас будут:


    • отложить до лучших времен — postpone
    • возобновить накопление — resume

    В некоторых местах вы говорите:


    Публиковать и убирать в черновики

    Что немного противоречит. Черновики это отдельная история и тоже делается не через unpublish.


  8. Вангуем дату исполнения.
    Логика расчитана на то, что вы вклад делаете каждый день, но этого нет в условии.
    В нашей стране распространеное двух этапная выплата зарплаты — аванс и зарплата.
    Соответсвенно, делать вклады в таком случае чаще 2 раз в месяц затруднительно.
    Вообще это все сильно зависит от возможостей делать вклады.
    Я бы закладывал ежемесечные вклады, но это сильно зависит от бизнеса.


  9. С вычетами и удалениями депозитов у вас тоже не все впорядке.


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

    Вклад это фиксированная величина. Вы не можете удалить вклад после внесения его в фонд, так как он растворяется в нем.
    В фонде у вас хранится общая сумма и история вкладов (если она вам нужна).
    Внесение вклада это внесение средств. Вы делаете вклад в копилку и вносите в нее деньги и теперь денег в копилке стало больше.
    Вклада в ней нет. В ней только деньги. По сути вклад обертка над Money.
    Если вы по ошибке сделаи вклад не на то желание, то вы можете сделать транзакцию по переводу средств из одного фонда в другой на размер последнего вклада или любую другую величину.
    Если вы внесли в фонд больше денег чем хотели, то вы можете изьять сумму из конкретного фонда.
    Вы не обязаны извлекать из фонда сумму равную какому-то вкладу.
    Например, вы внесли 50 рублей, а потом поняли что 24 рубля 74 копейки из них были лишними (я утрирую но мысль я думаю вы поняли) и хотите извлеч из вклада конкретную сумму денег.
    Да и вообще, вам по жизни может потребоваться извлечь произвольную суммму из вклада.


  10. Вы уверены что накопленные средства = исполнению желания?


    По мере накопления достаточного количества средств желание становится исполненным.

    Я бы не ставил между ними равно.
    Исполнение желания это одно, а накопление достаточной суммы для исполнения желания это савсем другое.
    И не ограничивайте явно потолок накопления суммы. Вспоминаем Kickstarter.


  11. Вклад не должен знать о Желании.
    Вы сделали рекурсивную ссылку, а это плохо.
    Правильней так:
    • Есть Желание и Фонд накопления средств на конкретное Желание.
    • Если Фонд выносить в отдельный контекст, то лучше делать связь от Фонда к Желанию и тогда:
    • У Желания есть цена.
    • Желание не знает о Фонде.
    • Фонд знает о Желании и как следствие о его цену.
    • Фонд накапливает сумму на исполнение Желания.
    • Вклад не знает ничего ни о Фонде, ни о Желании. Это просто деньги.
    • Мы сами определяем в какой Фонд внести Вклад.
    • Вклад это VO.
    • После внесения Вклада в Фонд он превращается (если это нам нужно) в Транзакцию в Истории транзакций фонда.
    • Транзакции нельзя удалять или изменять. Это уже история.

Там еще целая гора мелких недочетов с реализацией финансовой части. Я не буду тут вдаваться в подробности.

инджектишь НЕ Entity Manager а Guzzle\Client например.

Точно. Что-то я не так прочитал. Тогда все логично)

не совсем так

То есть, вы признаете что сходство ролей очевидно?
Я не предлагаю что-то менять. Хотите, называйте Repository, хотите Catalog. Это ваше право.


мой репозиторий выглядит так

Мне сложно представить зачем вам может быть нужен одновременно и ApiClient и EntityManager в одном таком репозитории.


Разве что у вас разделены read и write хранилища и вы в одном репозитории читаете из одного хранилища, а пишете в другое. Хотя это явно неправильно.


Всё что мне приходит в голову делается через доменные события.
Расскажите, зачем вам ApiClient и EntityManager в одном репозитории? Мне просто любопытно.

Мне не нравится хотя бы вот это:


  • У вас 2 класса называются одинаково — repository (назови вы второй manager вопросов бы не было);
  • Они оба находятся на одном слое — инфраструктурном;
  • У них одинаковая роль — управление сущностью в хранилище (интерфейсы разные, но роль одна).

Одного этого достаточно чтоб задуматься, что что-то не так.
Потому я и предложил не разделять функции на 2 класса.


Также вы можете переименовать ваш репозиторий в manager, gateway или что-то более близкое к его роли или вы можете перенести этот репозиторий в другой слой.


И да. Я проверил. С Symfony 3.3 нам не нужно тегетируем сервис репозитория, нам не нужен компилятор для меток, нам не нужна своя фабрика репозиториев. Достаточно просто создать сервис и прописать его в аннотациях к сущности.


Свой репозиторий в Symfony 3.3

Прописываем репозиторий в аннотациях сущность


/**
 * @ORM\Table(name="article")
 * @ORM\Entity(repositoryClass=DoctrineArticleRepository")
 */
final class Article
{
    // ...
}

Реализация репозитория


class DoctrineArticleRepository
    extends EntityRepositoryDummy
    implements ArticleRepository
{
    private $em;

    private $client;

    public function __construct(
        EntityManagerInterface $em,
        ApiClient $client
    ) {
        $this->em = $em;
        $this->client = $client;
    }

    // ...
}

Для соблюдения контракта нам придется сделать заглушку


abstract class EntityRepositoryDummy implements ObjectRepository
{
    final public function find($id)
    {
        throw new \RuntimeException('This method is not implemented.');
    }

    // ...
}

И мы можем получить наш сервис репозиторий двумя способами в любом месте.


class SomeService
{
    public function __construct(
        EntityManagerInterface $em,
        ArticleRepository $repository
    ) {
        // true
        $em->getRepository(Article::class) === $repository;
    }
}

Я не предлагаю сейчас возвращаться к обсуждению целесообразности такого решения, но возможность такая есть и без единой строчки в конфиге не считая импорта.


То есть, для того чтоб перейти от вашего решения к моему, достаточно добавить extends и прописать репозиторий в аннотациях сущности.
И все. Больше ничего делать не надо.
И это гораздо проще чем настраивать deptrac.


И еще, ваши фразы


позволяет юзать не только доктрину но и скажем дергать внешние API, просто инджектишь не EM а Guzzle\Client например.

и


В этом случае у вас по сути "репозиторий" на каждую выборку (что мы выше и обсуждали) что в целом меня устраивает и я даже так иногда делаю.

наводят меня на мысли, что ваши репозитории делают больше чем должны.


PS: Если вам не нравятся лишние методы в ObjectRepository, то вы можете сделать PR или fork.

На мой взгляд Clean Architecture не очень подходит для больших и долгоиграющих проектов со сложной бизнес логики (читай DDD).


Классическая архитектура DDD приложений имеет слои:


  • Presentation layer
  • Application layer
  • Domain layer
  • Infrastructure layer
  • Persistence layer

И вот вся это катавасия из Clean Architecture:


  • Controller
  • Presenter
  • Reques modal
  • Rrsponse model
  • Interactor

Очень похожа на CQRS и находится на одном уровне — Application. Мы просто разделяем потоки и то что могло выполнятся в контроллере выполняется в Interactor или Command Heandlers в CQRS подходе.


А все самое интересное из DDD оказалось спрятано за неоднозначным понятием Entities.


Application layer оказался эквивалентен Infrastructure layer, а Presentation layer эквивалентен Persistence layer.


В общем, очень странная архитектура на мой взгляд. Хотя многие могут не согласиться со мной.

Вообще-то, по Эванса, сущность это слой бизнес лигики (domain), а контроллеры это слой приложения (application).
Репозитории это тоже слой бизнес логики, а реализация репозиторий под конкретное хранилище это вообще слой инфраструктуры которого нет в Clean Architecture

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


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

Всё просто.
Если вашей сущности не хватает данных для проверки бизнес ограничения вы можете:
А) передать ей эти данные в аргументе функции
Б) вынести проверку бизнес ограничения в сервис доменного слоя


У Вернона есть упоминание о сервисах доменного слоя. Рекомендую почитать.

Я видел уже эту схему, но она мне показалась странной. Я ее не понял и прошел мимо.
Теперь я понял эту архитектуру.
Спасибо вам за это.
Правда мне всё равно она кажется немного странной.
Мне больше по душе классическая архитектуру DDD приложений + CQRS.

Всё здорово, но этого ещё нет в LTS. Со следующего года можно внедрять.


Я все еще не понимаю почему ты считаешь такой подход "неправильным".

Я уже исправился. Я уже не считаю его "неправильным". Он просто мне не нравится.

и откуда берется ids?

И зачем ты дурачком прикидываешся? Прекраснож понимаешь откуда.


 Тегетируем наши сервисы репозитории и возвращаем в фабрике.

Естественно ids мы получаем из Compiler Passes.
Если тебе нужны зависимости в репозитории, то ты в любом случае должен объявить их как сервисы. А чтоб можно было их получить из EntityManager, мы просто добавляем им метку.

А зачем много фабрик? Одной достаточно. Кода то всего на 10 строчек.


public function getRepository(EntityManagerInterface $entity_manager, $entity_name)
{
    $class = $entity_manager->getClassMetadata($entity_name)->getName();

    if (isset($this->ids[$class])) {
        return $this->container->get($this->ids[$class]);
    }

    return $this->default->getRepository($entity_manager, $entity_name);
}

Эээ… Вы читать не умеете? Я уже тысячу раз сказал, что я не хочу давать пользователю интерфейс ObjectRepository. На каком ещё языке мне это сказать чтоб вы меня поняли? Может на белорусском?

А что тут сложного?
Берём и реализовывает свой RepositoryFactory для доктрины. Тегетируем наши сервисы репозитории и возвращаем в фабрике.


Вот простейший пример реализации фабрики репозиториев.

Хотя мы можем добавить интерфейс ObjectRepository к DoctrineArticleRepository и не добавлять его к ArticleRepository и таки образом не будем нарушать контракт, но смысла в этом особого нет, так как создаст ненужные, скрытые, пустые методы в репозитории.

что меня не устраивает от слова совсем.

Ну это ваши личные трудности. Меня тоже много чего не устраивает и что с того?


только через setter injection, что опять же меня не устраивает.

В смысле? Использовать constructor injection религия не позволяет?


не чувствую. Information hiding надо рассматривать исключительно с точки зрения клиента

Разницы для клиента между двумя реализациями нет. Они обе дают конкретный интерфейс ArticleRepository.


Или вы намекаете что "это чудаки могут где-то заинджектить EntityManager в обход Dependency Injection?

А почему в обход? Если EntityManager вы инжектите в репозиторий, то и в любой другой сервис его можно спокойно инжектить.


Ваш же способ просто позволяет сразу всегда и везде получать инстанс EntityRepository. Вообще никакой изоляции.

Как раз наоборот. Мой вариант позволяет полностью заблокировать возможность получения EntityRepository. Полная изоляция.


Хотя я должен признаться. Я забыл что метод EntityManagerInterface::getRepository() должен возвращать ObjectRepository. Он может возвращать и другие объекты, но это будет нарушением контракта.


Так что мой вариант нарушает контракт и ваше решение правильней, хотя я считаю его неприемлемым, от слова совсем.

А что тут представлять?
Если компания хочет выходить на мировой рынок, то ей нужно переводить на другие языки:


  • Документациию к своему оборудованию или ПО
  • UI программного обеспечения
  • UI сайта

Стандартом для Российских компаний является наличие документов на русском и английском языках. Следующим как правило идут немецкий, французский и итальянский.
Если компания хочет развивать свою деятельность на востоке, то к этому списку очень быстро добавляются такие специфические языки как японский, китайский, корейский и арабский.
Всё делается для пользователей.
Вам ведь легче читать инструкцию на русском языке чем на английском, по например, эксплуатации стиральной машины произведенной в Японии, японский компанией.

нас интересовать должно не это, а information hiding. Мы не добавлять должны методы а изолировать текущее. Все методы "инфраструктурные" должны быть приватными и изолированы красивым интерфейсом исключающим "неправильное использование".

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


А вы не думали что доктрина может возвращать вам репозиторий реализацующий интерфейс доменного слоя?


$em->getRepository(Article::class) instanceof ArticleRepository == true;

Вы можете все так же использовать зависимости в репозитории. Все также можете внедрять репозиторий как зависимость, но ещё вы можете получать его из EntityManager-а. И точка доступа к данным у вас в этом случае одна (если не брать в расчет прямые запросы к EntityManager и Connection).


use GuzzleHttp/Client;

class DoctrineArticleRepository implements ArticleRepository
{
    private $em;

    private $client;

    public function __construct(EntityManagerInterface $em, Client $client)
    {
        $this->em = $em;
        $this->client = $client;
    }

    public function get(ArticleId $id): Article
    {
        $article = $this->em->find(Article::class, $id);
        if (!$article instanceof Article) {
            throw new \RuntimeException();
        }
        return $article;
    }

    public function add(Article $article): bool
    {
        $response = $this->client->request(
            'put',
            sprintf('/article/%s/', $article->id()),
            ['body' => $article->text]
        );

        return $response->getStatusCode() == 201;
    }
}

Условный пример использования в зависимостях сервиса доменного слоя


class ArticleService
{
    private $rep;

    public function __construct(ArticleRepository $rep)
    {
        $this->rep = $rep;
    }

    public function createWithText(
        ArticleId $id,
        ArticleText $text,
        ArticleEditor $editor
    ): bool {
        return $this->rep->add(new Article($id, $text, $editor);
    }
}

Чувствуете разницу? Нет лишней прослойки. Вот это и есть information hiding, а не то что вы предлагаете.


И это решение, в отличии от вашего, ни сколько не нарушает использование устоявшегося и нормального способа получения репозитория из EntityManager.
Поди объясни новичку, что то, что он привык делать годами у вас делается через… иначе.


Да. Использовать EntityManager для получения репозитория не всегда хорошо, а иногда и плохо, но это лучше чем возможность получить через него репозиторий который вскрывает все кишки наружу, а не репозиторий реализующий интерфейс доменного слоя.


Сами сказали:


красивым интерфейсом исключающим "неправильное использование".

Информация

В рейтинге
Не участвует
Откуда
Россия
Зарегистрирован
Активность