Search
Write a publication
Pull to refresh
18
0
Send message

Я рассуждал в контексте статьи, а она о взаимодействии элементов внутри приложения.
Ок, но тогда возникает множество вопросов в духе
- А как выглядит архитектура взаимодействия компонентов системы?
- Как устроена шина передачи сообщений? Какова её ответственность?
И далее по тексту... В вакууме, я бы предложил идти либо по пути npm, либо развить описанный мной п.4 по следующему пути: определяем стабильные модели на стороне бэка, но не как DTO, а как значимые объекты с содержимым, пишем какой-нибудь веселый скрипт в духе того же openapi-gen для автогенерации моделей; так получаем единый источник типизированных моделей, и даже обладающих фактическим знанием (id сообщений, например); отсутствие версионности здесь одновременно и плюс и минус, но танцев с бубном в процессе использования явно сильно меньше, чем с самостоятельными библиотеками. Путь довольно скользкий, но, как полёт фантазии и экспромт "на тему", выглядит интересно.
P.S. Можете и в личку писать, - больше шанс, что отвечу более оперативно.

Вариантов тут масса, и все не удобные:

  1. В случае с monorepository, мы можем выделить локальную библиотеку; в вакууме это наиболее чистое и быстрое решение, которое позволяет не размазывать код по десятку репозиториев. Из минусов: необходимость отдельных build'ов этой библиотеки, что на фоне прочих вариантов - мелочь. Но, как я понимаю, это не ваш сценарий.

  2. npm'ка. Никаких проблем поднять на рабочем серваке какой-нибудь Verdaccio сегодня, imho, нет. Получаем типизированный набор событий, с возможностью хранения некой меты, а не просто DTO. Минусы:

    • на коленке тут ничего не поправишь - ради каждого чиха нужно вносить изменения в репозиторий библиотеки и поднимать версию

    • портирование изменений во множестве модулей превращается в жуткую рутину

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

  3. submodules. То же самое, что и второй пункт, но, imho, в два раза болезненнее из-за танцев с git'ом. Я бы вообще не смотрел в эту сторону, но это моя личная боль и детская травма, возможно.

  4. Из странноватых решений есть еще некое переосмысление Module Federation (оригинал даже не предлагаю, ибо это вообще дичь, на мой вкус; но, если не в курсе - почитайте и про него). Если нас устроят исключительно DTO, чистые от каких-либо данных, то можно описать их в бэке и прокинуть через swagger, подтягивая в проект каким-нибудь автоматическим генератором моделей на основе api (openapi-gen). Тема интересная, но тут стоит пятьдесят раз подумать, потому что ограничения у этой штуки понятные - никаких id событий и прочего туда уже не пихнешь - канонические DTO. Зато получаем чистый источник единственной истины во всех проектах.

Но, в целом, мне кажется, что сама постановка у вас какая-то странная. События принадлежат конкретному приложению, зачем приложению А обладать такими же событиями, что и у В? Они способны ими обмениваться через какой-нибудь SignalR? Тогда это уже точно вовлечение бэка и решать это надо там.

Если речь о простой унификации в духе "везде должно быть одинаково"... Это ведет нас к армейскому "круглое тащить, квадратное катить", столько боли с поддержкой cross-моделей не стоят этой красоты однообразия, imho.
---

По части broker-agnostic не понимаю ваших сомнений. Если речь идет о чистом брокере сообщений (и о корректном его использовании). То клиенты пользуются им в очень ограниченных сценариях в духе .subscribe()/publish(). Создайте вокруг брокера самописный слой абстракции и пользуйтесь только им, при смене брокера все изменения будут лежать внутри нашего кастомного решения. Этот подход не сработает, если вы завязываетесь на какие-то сложные механизмы взаимодействия с оригинальным брокером, но тут уже проблема не в порочности brocker-agnostic, а в неправильном использовании самого паттерна Message.

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

По второму вопросу:

Добавлять метаданные об очередях напрямую в классы событий — это архитектурный антипаттерн:

  1. Нарушение инкапсуляции
    Событие — это констатация факта («Пользователь зарегистрировался»), а не инструкция по его обработке. Когда событие знает, куда его отправить, оно берёт на себя чужую ответственность — как если бы письмо диктовало почте, в какой ящик его положить.

  2. Смешение слоёв
    Информация об очередях — это инфраструктурная деталь. Зашивая её в доменный объект (событие), вы связываете бизнес-логику с технической реализацией.

  3. Риск vendor lock-in
    Если события привязаны к конкретным очередям, переход на другую систему потребует правки всех событий. Это превращает миграцию в ад, где «просто поменять брокера» уже не получится.

  4. Усложнение тестирования
    События с метаданными сложнее тестировать: теперь вам нужно имитировать не только данные, но и инфраструктурный контекст.

Альтернатива:
Вынесите маршрутизацию в отдельный Routing Layer — сервис или конфиг, который знает:

  • Какие события в какие очереди направлять;

  • Как реагировать на изменения инфраструктуры.

Это сохранит события «чистыми». Например, если завтра вы захотите дублировать события в две очереди вместо одной, достаточно обновить конфиг, а не переписывать тысячу классов.

Вы правы, события во фронтенде действительно существуют со времён jQuery. Но есть нюанс: в те годы мы говорили о событиях DOM — кликах, ховерах, сабмитах. Это как общение через таблички «Нажми меня» — локально и прямолинейно.

Современные SPA с их компонентной архитектурой — это уже не кнопки на странице, а целые экосистемы.

  • В jQuery вы вешаете обработчик на элемент → жёсткая привязка к DOM

  • В Pub/Sub компоненты общаются через тематические каналы → нулевая связь между отправителем и получателем

Безусловно, событийность — не новинка. Но именно сейчас, когда фронтенд дорос до сложности микросервисных систем, нам, imho, нужны подходы уровня RabbitMQ, а не табличек «Продам гараж».

Как писал Дейкстра: «Инструменты влияют на образ наших мыслей». jQuery-события учили нас реагировать, Pub/Sub учит строить коммуникации.

В рамках этого цикла для новичков рассматривать такое будет не очень уместно, но отдельной статьей может и напишу, раз заявка есть) Интересует что-то конкретное?

1. Использование DI для разделения зависимостей Да, внедрение зависимостей (DI) действительно решает задачу разделения компонентов и сервисов на уровне инфраструктуры. Однако важно учитывать, что DI — это только способ предоставления зависимости, то есть кто-то должен связать потребителя и его зависимость. Решение "компонент → сервис" само по себе не избавляет от логической зависимости компонента от сервиса.

С помощью @artstesh/postboy достигается более высокая степень абстракции, так как компонент запрашивает данные, не зная о существовании конкретного сервиса вообще.

  • DI определяет, какой конкретный сервис использовать или подставить;

  • подход с запросами через postboy позволяет компоненту вообще не знать, где и как будут получены данные.

Иначе говоря, DI решает проблему на уровне внедрения, а postboy — на уровне взаимодействия, убирая прямые зависимости.

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

3. Зачем такого рода абстракция, если можно обойтись DI? Вот здесь ваш комментарий о "Мосте" подходит в части философского определения о "разрыве реализации и абстракции", но это опять про попытку создать конфликт с DI... Мост действительно абстрагирует интерфейсы от их реализаций. Разница в том, что с использованием единой централизованной точки взаимодействия вы уходите от вопроса "какой именно интерфейс использовать". Поведение устанавливается на уровне обработчика сообщений, а не конфигурируется в DI. Вместо этого:

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

  • Работа с центром сообщений (postboy) позволяет отделить транспорт запроса (как слать) от доставки результата (кто возвращает).

4. Обслуживающий код и его рост Конечно, накладные расходы в виде кода на recordSubject или sub в реальной практике могут быть заметны. Тем не менее такие механизмы предоставляют преимущества:

  • Инкапсуляция сложной логики в одном месте, а не в зависимости от компонентов.

  • Централизация процесса подписки, что снижает вероятность ошибок и упрощает масштабирование системы.

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

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

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

Если обобщить: это инструмент для создания архитектуры, которая стоит дополнительных затрат (наличие обработчиков, регистраций и т.д.), но выигрывает в поддержке при масштабировании. То есть это не альтернатива DI, а дополнение к нему — инструмент там, где DI уже недостаточно.

P.P.S. Боюсь, что в данном случае вы спорите не с библиотечным подходом, а с устоявшимися паттернами и подходами, я бы предложил вам ознакомиться с CQRS-подходом, Messaging pattern (pub/sub, как частная его реализация) и иже с ними. Логика брокеров сообщений вовсе не моя выдумка)

Приветствую,

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

Если рассматривать подход с использованием запросов (как в примере), то основная идея заключается в том, чтобы:

  1. Компонент запрашивал данные в абстрактной форме, а не напрямую обращался к конкретному сервису. Это позволяет компоненту не иметь привязки или зависимости от реализации сервиса.

    Например, если в будущем логика получения данных изменится (например, вместо текущего API будет использоваться другой источник или появится кэширование), нам не придётся вносить изменения в сам компонент — вся логика изменения будет изолирована в обработчике запроса.

  2. Существует единый, централизованный механизм коммуникации. Запросы и команды из библиотеки @artstesh/postboy помогают унифицировать взаимодействие между сервисами и компонентами. Это особенно ценно в сложных, модульных системах, где много областей ответственности.

Теперь о "ненужных зависимостях".

  • В описанной схеме компонент действительно знает о библиотеке postboy, так как он использует её для отправки запроса, но этот код (запрос через fireCallback) легко изолируется и инкапсулируется. Основная ценность этого подхода — то, что компонент вообще не знает, откуда приходят данные и какой сервис отвечает за их получение. Благодаря этому достигается гибкость архитектуры.

  • В обычном сценарии "компонент → сервис" любой значительный рефакторинг, изменение логики получения данных или, например, внедрение нового уровня обработки потребует изменений и в компоненте, и в вызываемой логике. Здесь же мы разделяем их полностью: поведение можно менять, не трогая компонент.

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

RxJS полезен там, где нужно работать с потоками данных — асинхронными, событийными, бесконечными. Если axios даёт возможность сделать одиночный запрос, а сокеты — получать события, то RxJS позволяет управлять их потоком: фильтровать, объединять, трансформировать, восстанавливать после ошибок и повторно использовать подписки.

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

Это инструмент для сложной работы с событиями и асинхронщиной, а не для единичных задач. Если потоков немного и они простые — справишься и без RxJS. Но как только появляется много асинхронности, сложная логика или перекрёстная работа потоков — RxJS начинает экономить силы и резко повышает читаемость кода.

Меня прямо-таки умиляют такие пассивно-агрессивные поздравления... Спасибо, конечно, но я всё же порекомендовал бы Вам внимательнее взглянуть на статью: речь тут идет о unit-тестировании. О каких unit-тестах вы говорите на проде? В runtime? Так это уже совершенно другое. Unit'ы используются для тестирования в процессе разработки и для проверки приложения перед деплоем. Любые тесты на живом приложении имеют дело с совершенно другим контекстом и, в большинстве случаев, не должны опираться на рандомные подделки.

Что касается теста... Так статья вроде не про написание самого красивого и умного теста, а просто пример того, как могут быть созданы некие рандомные объекты.

Благодарю, совершенно забыл про эту опцию, добавил 9й пункт

А вот теперь мы переходим к реальному конфликту подходов, без подколов "кто, что изобрел") И я, к сожалению, не могу согласиться с Вашим подходом в контексте обсуждения.
По факту, я вижу у Вас оценку библиотеки, направленной на unit-тестирование, через призму PBT. С "высоты" своего десятилетнего опыта, могу заметить, что, в рамках unit-тестов, мне прям очень важно быстрое создание рандомных объектов, которые я могу тюнить под конкретный тест. Вы считаете, что такой подход - not true? Может быть, но библиотека в том не виновата, она вообще не про PBT, и не способна следовать за вашими суждениями.

Во-первых, давайте спустимся с философских небес на землю и приведем пример создания объекта в рамках typescript-приложения. Далеко ходить не будем и возьмем мой же пример:

interface User {
  id: number;
  name: string;
  isActive: boolean;
}

const user = Forger.create<User>();

Можно ли пример того, как тот же fast-check из "десятка существующих" создаст поддельный объект в соответствии с описанием класса/интерфейса? Получится ли у нас это сделать в одну строчку, как в моем примере, или необходимо полностью описать модель объекта отдельным, по сути, классом, с параметризированием каждого поля?

В таком случае, я скорее переизобрел (хотя вообще не считаю, что изобрёл что-то) AutoFixture из C# под typescript, или эта библиотека с Вашей точки зрения тоже никчемная поделка из миллиона?

Идея же о том, что Mock - это исключительно про поведение, а не про данные... Ну ок, как вы охарактеризуете тот же AutoFixture? Мы подделываем именно данные согласно интерфейсу (читай контракту), это не подделка? Как это называется в Вашем словаре?

Лови тролля! А если серьезно, то за поздравления спасибо, но мне, к сожалению Вас, в ответ, поздравить не с чем: Вы не поленились написать едкий комментарий, но поленились разобраться чем mock'ирование обектов в целях тестирования отличается от функционального PB-тестирования; forger не имеет никакого отношения к PBT (подразумевающего автоматические проверки предикатов), а направлен совершенно в другую сторону - создание объектов по контракту.
P.S. Но за комментарий спасибо, учту, что стоит подсвечивать более явно.

Так... По порядку:
signals во многом направлены на замещение механизма detectChanges в целях оптимизации рендеринга. В этой части никакой конкуренции с представленным решением нет.
В части передачи состояний все гораздо прозаичнее: я не вижу никакого ясного выигрыша от signals в задаче передачи данных между компонентами.
Есть у нас таблица элементов и input, при вводе таблица фильтруется в соответствии со значением input. Каким образом передавать состояние между элементами? Примитивный (и не самый лучший, согласен) пример с @Input - ужасная затея, скорее мы пойдем в Subject/ReplaySubject. Но как передать эту подписку между компонентами? Нужен сервис, содержащий объект подписки, оба компонента ссылаются на этот сервис и работают с подпиской. Signals, в этом смысле не предлагает ничего нового - мы так же должны будем создать сервис для передачи нашего signal. Со временем конструкторы компонентов распухают от ссылок на всевозможные сервисы, создавая сонм весьма неприятных связей. В моих проектах большинство компонентов зависят только от сервиса PostboyService, подписываясь на любое интересующее событие, не зная ничего об обработчиках/отправителях сообщений внутри системы.
P.S. За комментарий спасибо, учту на будущее, что стоит подсветить)

Основа здесь - rxjs, а он, как писалось тут, 'никогда' не будет замещен signal'ами. Про относительность 'никогда' мы все все понимаем, но сейчас функционал разнится, особенно в части асинхронности. Та же логика executors из библиотеки, вероятно, в некоторой степени может быть замещена signal'ами, но либа больше про rxjs, чем про Angular, не у всех они есть) В общем, каждой задаче - свой инструмент, и это - ещё одна опция.

Information

Rating
259-th
Registered
Activity