Обновить
4
0

Пользователь

Отправить сообщение

И оба этих кейса подразумевают, то интерфейс относится к МНУ.

Оба этих кейса подразумевают обратное. МВУ становится тот модуль, который вызывает интерфейс. МНУ тот модуль, который реализует интерфейс.

Пример кода МНУ:

public class ModuleB : IContract {
  ...
}

Пример кода МВУ:

public class ModuleA {
  public ModuleA(IContract dependency){
    ...
  }
}

При этом конечно Module A , сам может быть МНУ для Module C:

public class ModuleA : IAnotherContract {
  public ModuleA(IContract dependency){
    ...
  }
}

Таким образом формируется граф зависимостей.

Давая волю рассуждениям, по идее, где-то в самом верху будет МВУ (точка входа в граф), который не является МНУ ни для кого. Но это уже что-то про уровень ядра операционных систем или глубже 🤔 А на уровне приложения такой точкой выступает Composition Root или же DI контейнер, который является частным случаем Composition Root.

Обратите внимание на мой комментарий https://habr.com/ru/articles/872078/#comment_27755320. Описываемая вами ситуация подходит под тип 2 на основе контрактов. Автопроизводитель это модуль А. ГОСТ это Контракт / Реестр. Производитель болтов это модуль B.

Тем не менее в вашем утверждении заложена неточность:

Задача: Производитель автомобилей определяет стандарты (требования к болтам) и использует эти стандарты для взаимодействия с болтами.

Стандарты и требования к болтам определяет ГОСТ (Контракт / Реестр). Автопроизводитель определяет, какие болты из реестра ГОСТ он будет использовать. Автопроизводитель не определяет, что собой представляет болт формата М6. Производитель болтов определяет, какие болты в соответствии со стандартами ГОСТ он будет производить.

Для соответствия типу 1, необходимо, чтобы автопроизводитель разработал свой собственный стандарт болта. Далее он создаёт свой собственный реестр болтов и вносит туда этот стандарт. Далее от открывает доступ заводам к этому реестру. С этого момента происходит инверсия зависимости: автопроизводитель берет на себя роль МВУ, а фабрика берет на себя роль МНУ.

P.S. В моем случае Service Registry подразумевает реестр ваших собственных служб, но это не обязательно. В реестр могут быть внесены так же более общие контракты, например IEmailSender. Было бы замечательно, если бы был общий реестр таких интерфейсов встроенный напрямую в стандартную библиотеку классов. И как мне кажется, такое уже постепенно происходит. Это можно наблюдать на примере интерфейсов Microsoft.Extensions.Configuration.IConfiguration, Microsoft.Extensions.Logging.ILogger. Это позволяет управляющему коду (производитель авто) использовать разные логгеры (serilog, log4net) опираясь на ГОСТ (интерфейс ILogger).

@liarЯ изложил свою точку зрения в комментарии по ссылке: https://habr.com/ru/articles/872078/#comment_27755320. Он получился довольно объемным, поэтому я решил не вставлять его в ваш диалог. :)

При анализе своего кода на предмет использования принципа инверсии зависимостей (DIP) я выявил несколько способов его применения.

Тип 1: Интерфейс находится на том же уровне, что и "верхний модуль".

Примером может служить следующая строка кода для конфигурации ASP.NET Core.

services.AddSingleton<IControllerFactory, MyCustomControllerFactory>();

Интерфейс объявлен на уровне фреймворка: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/IControllerFactory.cs.

Такой подход позволяет разработчику влиять на поведение фреймворка (важно помнить о соблюдении post conditions и pre conditions из LISP). Постулаты, описанные в статье соблюдены.

Так же данный пример может служить расширенной версией описанного в статье, так как разработчиками фреймворка так же предусмотрена стандартная реализация этого интерфейса https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/DefaultControllerFactory.cs.

Тип 2: Интерфейс вынесен на уровень общей библиотеки.

Например, в проекте может быть библиотека Service Registry, в которой описаны контракты всех модулей, сервисов и микросервисов. Эти контракты строго соблюдаются на уровне реализации служб (например, в других репозиториях). Когда Service A необходимо обратиться к Service B, он использует контракт ServiceRegistry.ServiceB.Execute(request).

Предположим, что Service B реализует этот интерфейс на уровне двух протоколов: gRPC и JSON-RPC (например, Yandex Cloud API). Также существуют две клиентские библиотеки: ServiceB.JsonRPCClient и ServiceB.GRPCClient. На уровне инфраструктуры Service A происходит выбор клиентской библиотеки. Тем не менее, нельзя утверждать, что Service A теперь независим от реализации Service B. Единственное, от чего он не зависит, — это от транспорта или способа коммуникации между ними. Однако он зависит от контракта и поведения. Возможно, такой подход также подпадает под DIP, но в данном случае происходит абстрагирование не от логики, выполняемой Service B, к которой Service A становится привязан, а от способа коммуникации.

Если рассматривать пример с репозиторием, я бы отнес его к Типу 1. Интерфейс репозитория определяется на уровне, где требуется получить данные о сущности, например, в сценариях использования (use cases). Это позволяет не беспокоиться о том, откуда загружается эта сущность (будь то SQL база данных, NoSQL база данных, кеш или файловая система), однако создает зависимость от конкретной сущности, и это именно то, чего мы хотим.

Для написания работающего кода невозможно абстрагироваться от всего, поэтому, как мне кажется, DIP позволяет абстрагироваться только от тех аспектов, которые не важны для реализации конкретного пользовательского сценария. Если пользовательскому сценарию необходимо перевести сущность из состояния А в состояние Б, он сосредотачивается на этом, снимая с себя "ответственность" за способ записи в базу данных. Если сценарий Service A жестко зависит от поведения Service B, то от поведения Service B абстрагироваться не удастся (и не нужно), но можно, по крайней мере, абстрагироваться от используемого транспортного протокола.

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

  1. Polling — периодическое опрашивание таблиц transactional outbox с помощью отдельных потоков или инстансов для публикации сообщений в брокер. Имплементация будет зависеть от вашей инфраструктуры (serverless / serverfull), single instance / multu instance и тд.

  1. Pushing — использование встроенных в базу данных механизмов Change Data Capture (CDC). Когда в таблицу transactional outbox вносится запись, коннектор, используя CDC, обрабатывает это как изменение в базе и пытается опубликовать сообщение в брокер.

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

Что касается реализации очередей для tenants, я до конца не понимаю, почему вы не нашли на рынке решения для этой проблемы. Kafka прекрасно подходит для этого — используйте tenantId в качестве ключа партиционирования и создайте столько партиций, сколько готовы обрабатывать параллельно.

Возможно, речь идет о in-process / in-memory очередях, тогда действительно пришлось бы эмулировать поведение Kafka внутри процесса.

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

На данный момент я пришел к следующим подходам:

  1. Каждая команда работает только с одним агрегатом. При его изменении слой ORM осуществляет преобразование из VO в обычный объект, выполняет функции UnitOfWork и рассчитывает различия состояния для последующей записи в базу. Все агрегаты имеют проверку оптимистичной согласованности.

  2. Изменение агрегата генерирует доменное событие (синхронно). Доменное событие существует только в памяти и включает value objects. Я не использую Event Sourcing, поэтому UoW не хранит события.

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

  4. Интеграционное событие обеспечивает синхронизацию контекстов в режиме eventual consistency.

  5. Создается поток "Команда - Событие".

  6. Доступ к агрегату из репозитория осуществляется только по id.

  7. Команда ничего не возвращает.

  8. Команда всегда включает все необходимые данные для выполнения. При создании агрегата id генерируется вызывающей стороной. Например, команда обновления агрегата выглядит как new UpdateRatingCommand(locationId, rating).

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

Таким образом реализуется подход CQS.

Cторона команды моделирует все необходимое для записи: бизнес-инварианты и транзакционную целостность.

Запросы всегда обеспечивают согласованность read after write. То есть в реальности запрос может вернуть устаревшие данные только в случае, если запрашиваются данные из различных контекстов, которые по дизайну синхронизируются асинхронно. Например, если рейтинг локации изменился в контексте управления рейтингом, при запросе этого рейтинга вы получите актуальные данные (если не было других изменений). Однако уведомление об изменении рейтинга могло еще не пройти обработку в контексте аналитики, так что при запросе данных в аналитике изменения могут еще не отразиться.

На вопрос о том, как создать команду, если ее данные зависят от нескольких агрегатов, но в обработчике команд мы работаем только с одним агрегатом, ответ таков: мы выполняем query (учитывая, какие данные будут актуальными, а какие могут быть устаревшими, что в свою очередь завсит от контекста) и на основе его результатов формируем команды.

На вопрос о том, кто формирует команды, ответ — это зависит; речь идет об оркестрации. В некоторых случаях роль оркестратора может выполнять контроллер. Но если команда является частью процесса, эту роль возьмет на себя Message Router / Saga или Process Manager (мое предпочтение).

На вопрос о том, как оптимизировать базы и проекции для интенсивного чтения, ответ прост: применяются хорошо известные всем решения — такие как ElasticSearch, Mongo или DynamoDB. В контексте DDD создается отдельный ограниченный контекст с собственными агрегатами, инвариантами (более мягкими), командами и запросами. Роль хранения данных выполняет одна из упомянутых технологий. Поскольку в рамках DDD мы понимаем, что разные контексты синхронизируются с задержкой, для нас не станет неожиданностью, что данные в этих системах обновляются не мгновенно.

В этом контексте возникает вопрос: какова роль доменных событий, если одна команда отвечает за обновление одного агрегата и публикацию одного интеграционного события? Действительно, если обработать доменное событие в памяти и синхронно обновить другой агрегат в рамках того же контекста, это приведет к обновлению двух (или более) агрегатов в одной транзакции и публикации нескольких событий, что нарушает этот подход и увеличивает вероятность ошибок, связанных с оптимистичной проверкой согласованности. Я вижу для доменных событий два основных применения:

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

  2. Event Sourcing — мы храним эти события в базе данных и восстанавливаем агрегат на их основе.

Я полагаю, что призывы использовать доменные события вместо интеграционных связаны с тем, что раньше не было такого акцента на расширяемость. Ранее существовала одна служба и одна база данных, и было вполне приемлемо сохранять несколько агрегатов в рамках одной транзакции. В настоящее время мы имеем дело с архитектурой, основанной на службах, микрослужбах и даже (извините за каламбур) "ламбда-службах", где вычислительные ресурсы и базы данных выделяются под агрегат.

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

Принимайте эти компромиссы и не беспокойтесь о том, как это скажется на вашем "будущем", особенно если оно неопределенно, что часто встречается в мире Agile.

Поправьте меня, если я неправ, но похоже в этой секции нарушены границы транзакционности:

    boxRepo.SaveChanges(aggregate.Boxes);
    osgBoxRepo.SaveChanges(aggregate.OsgBoxes);
    linkRepo.UpdateLinks(aggregate.GetIds());

Первое. Что произойдет в случае, если

boxRepo.SaveChanges(aggregate.Boxes);

выполнится успешно, а последующие инструкции провалятся (недоступность сети, системные прерывания и тд)?

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

В противном случае вы не можете гарантировать целостность агрегата, что в свою очередь ведет к тому, что вы не можете гарантировать корректность и целостность данных в системе.

Что думаете по этому поводу?

Я правильно понимаю, что вы недовольны как прямыми вызовами fetch и axios, так и их обвёртками?

И ваше решение это заменить «неправильные» обертки других разработчиков своей «правильной» обвёрткой?

Ну и вас не устраивает то, что взаимодействие с api идёт через store. А что если я вам скажу, что я могу взять ваш Request Manager и использовать внутри store?

Я без какого либо негатива задаю все эти вопросы. Пример, который вы привели в качестве плохого кода действительно плох. Но это не значит, ваше утверждение верно для всех случаев. Если нам нужно подгрузить глобальную настройку и обновить стейт всего приложения - ваше решение потребует больше костылей, чем принесёт пользы. А если речь идёт о единообразии и абстракциях, то компонентам вообще не стоит знать, что они идут в какой-то REST API. Одно цепляется за другое, ну вы поняли…

12 ...
8

Информация

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