Введение: Архитектурное дежавю

Вы когда-нибудь замечали, как цифровой мир движется по спирали? В 2018 году я, размахивая Dockerfile и Helm-чартами, внедрял микросервисы на C# с RabbitMQ — всё ради священной цели «низкой связанности». А через три года, переключившись на Angular, с ужасом осознал: фронтенд-компоненты общались через цепочки Input/Output, словно это 2005-й, а мы пишем WinForms.

Это как собрать космический корабль, но управлять им через телеграф. На бэкенде мы гордо декларируем event-driven architecture, а на фронтенде компоненты перешёптываются через пропсы, будто подростки на школьной дискотеке. Ирония? Чем сложнее становились наши системы, тем больше они напоминали те самые монолиты, от которых мы бежали в мире backend.

Мой момент истины наступил, когда пришлось вносить изменения в какое-то диалоговое окно и чтобы добавить кнопку «Отмена», мне пришлось:

  • Обновить родительский компонент

  • Переписать сервис-посредник

  • Подправить три дочерних модуля

Всего 12 файлов для одной кнопки! В мире микросервисов это выглядело бы как правка пяти разных репозиториев, чтобы поменять цвет лейбла. И тогда я задался вопросом: «Почему фронтенд, при всей своей прогрессивности, до сих пор не усвоил уроки распределённых систем?»

Мы освоили WebAssembly, внедрили GraphQL, разорвали CSS на модули, но коммуникация между компонентами застряла в эпохе jQuery. Как будто архитекторы забыли, что React, Angular и Vue — это не просто про рендеринг, а про взаимодействие независимых агентов.

Но что, если я скажу, что решение давно существует? Что паттерн, который десятилетиями работает в RabbitMQ, Kafka и AWS SQS, может стать спасением для фронтенда? И что для этого не нужны тяжеловесные библиотеки — достаточно 15 КБ кода и принципиально иного взгляда на компоненты.

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

Часть 1: Исторический экскурс — 40 лет эволюции Pub/Sub

В 1983 году, когда программисты вручную переключали джамперы на материнских платах, Адель Голдберг из Xerox PARC писала код для Smalltalk-80. В её лаборатории родилась идея, которая переживет персональные компьютеры, веб-революцию и мобильную эпоху: «Объекты должны общаться через сообщения, а не вызовы методов». Это был первый вздох Pub/Sub — паттерна, который стал цифровым эквивалентом эсперанто для независимых модулей.

Эволюция в трёх эпохах

1. Эра племён (1980–2000) Smalltalk-объекты обменивались сообщениями как первобытные люди — напрямую, без посредников. Проблема? Чтобы отправить «письмо», нужно было знать точное местоположение «племени»-получателя.

"Племя А отправляет сообщение племени Б"
Б примитив: 'Огонь погас!'

Эра империй (2000–2010) CORBA и Enterprise Service Bus (ESB) превратили сообщения в бюрократию. Нужно было:

  • Знать WSDL-контракты

  • Регистрировать конечные точки

  • Выстраивать XML-схемы

На проекте 2010 года мы три недели интегрировали SAP с .NET через ESB. Когда спросили архитектора: «Почему нельзя проще?», он ответил: «Это enterprise — здесь так принято».

Эра глобализации (2010–н.в.) Kafka, RabbitMQ и облачные очереди превратили Pub/Sub в лингва-франка микросервисов. Правила упростились:

  • Формат сообщения = единственный контракт

  • Издатель не знает подписчиков

  • Брокер гарантирует доставку

Философский поворот: От приказов к договорам

Pub/Sub — это цифровая версия социального договора Руссо. Когда модуль публикует PriceChangedEvent, он как бы заявляет:

  • «Я не знаю, кому это нужно»

  • «Но если хотите — слушайте»

  • «Обещаю формат: {itemId: string, newPrice: number}»

Это напоминает TCP/IP для людей: как пакеты данных не заботятся о том, браузер вы или почтовый клиент, так и сообщениям всё равно, Angular вы или React.

Почему Pub/Sub пережил 40 лет технологических революций?

  • Антихрупкость: Системы учатся жить с ошибками (вспомним принцип Dead Letter Queues)

  • Языковая агностичность: Сообщениям всё равно, на чём вы пишете — они как эсперанто для микросервисов

  • Эволюционность: Можно начинать с простой шины и расти до распределённого стриминга

Как-то раз на митапе я услышал фразу: «Kafka — это Smalltalk для больших данных». Возможно, в этом есть доля правды — оба подхода учат системы вежливому общению без лишних вопросов.

В следующей части мы возьмём этот 40-летний опыт и посмотрим, как применить его к Angular-компонентам — чтобы они перестали тыкать друг друга локтями через Input/Output и заговорили на языке независимых сообщений.

Фронтенд: Застрявший в прошлом?

Пока бэкенд в 2010-х переходил от SOAP к событиям, фронтенд изобретал… @Output(). В Angular-компонентах застряли пережитки эры империй:

  • Жёсткая иерархия вызовов

  • Сервисы как ESB-монстры

  • События через 5 уровней — как бюрократическая почта

Однажды, чтобы добавить аналитику для кнопки в дочернем компоненте, нам пришлось:

  • Добавить @Output() analyticsEvent в компонент D

  • Пробросить его через B → C → A

  • Подписаться в корневом компоненте

Вагон работы ради строки analytics.track('click'). Это как доставлять письмо соседу через три почтовых отделения.

Уроки, которые фронтенд пропустил

1. События ≠ цепочки вызовов

Бэкенд давно понял: если микросервис A вызывает B, а B вызывает C — это антипаттерн. Но во фронтенде @Output() → сервис → @Input() считается нормой.

2. Брокер ≠ точка отказа

RabbitMQ выдерживает миллионы сообщений. А типичный Angular-сервис с Subject-ами падает при 1000 событий в секунду.

3. Формат > реализация

На бэкенде OpenAPI/Swagger описывают контракты. Во фронтенде до сих пор работают с any в событиях.

Заря надежды: Web Components

По иронии, будущее событийного фронтенда началось в 2011 году с идеи Web Components. Их Custom Events ближе к духу Pub/Sub, чем Angular-подход:

// Компонент А
dispatchEvent(new CustomEvent('price-changed', {
   detail: {itemId: '45', price: 20}
}));

// Компонент Б
window.addEventListener('price-changed', (e) => {
   console.log(e.detail.price);
});

Это напоминает ранний Smalltalk, но с HTML5-синтаксисом. Жаль, что фреймворки не пошли этим путём дальше.

Исторический парадокс: Технологии фронтенда обновляются каждые 2 года, но архитектурные паттерны застряли в 2000-х. Возможно, пора перестать изобретать велосипеды и подсмотреть решения у… 40-летнего Smalltalk.

В следующей части разберём, как эти принципы применяются в современных Angular-приложениях — и почему @Input/@Output иногда опаснее, чем кажется. Спойлер: это как строить замковую стену, на которую забраться изнутри сложнее, чем снаружи.

Часть 2: Фронтенд-дилемма — Когда компоненты начинают болтать

Вы знаете этот момент, когда открываете код коллеги и видите компонент, который знает слишком много? Как тот сосед, который следит за всеми через камеры видеонаблюдения. В Angular-мире это часто начинается с невинного @Input() и @Output(), но быстро превращается в паутину зависимостей. Давайте разберемся, почему традиционные подходы иногда напоминают игру в «испорченный телефон».

Проблема 1: Input/Output как цепные реакции

Представьте компонент ProductCard, который должен показывать модальное окно при клике. Классический подход:

// product-card.component.ts
@Output() openModal = new EventEmitter<string>();

onClick() {
  this.openModal.emit('product-details');
}

// parent.component.html
<product-card (openModal)="handleModal($event)"></product-card>
<modal [type]="modalType"></modal>

Что не так:

  • ProductCard знает, что где-то есть модалка

  • Родительский компонент становится курьером между несвязанными частями

  • Изменение типа модалки требует правки нескольких файлов

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

Проблема 2: Сервисы-посредники как новый монолит

Когда Output-ов становится много, мы создаем ModalService:

// modal.service.ts
private modalSubject = new Subject<string>();
modal$ = this.modalSubject.asObservable();

open(type: string) {
  this.modalSubject.next(type);
}

// product-card.component.ts
constructor(private modal: ModalService) {}

onClick() {
  this.modal.open('product-details');
}

Кажется лучше, но:

  • Сервис превращается в «божественный объект», знающий обо всех модалках

  • Компоненты жестко привязаны к API сервиса

  • Тестирование требует мокинга целого сервиса для одной кнопки

На одном из проектов наш SharedService разросся до 1200 строк — он управлял модалками, тултипами, уведомлениями и анимациями. Мы шутили, что это он теперь управляет проектом, а не мы, но смех был нервным.

Кейс: Модальное окно-шпион

Несколько лет назад нам нужно было добавить аналитику для модалки обратной связи. Проблема — она открывалась из 17 мест в приложении. По старой схеме пришлось:

  • Добавить @Output() registerClick в 5 компонентов

  • Пропихнуть событие через 3 уровня родительских компонентов

  • Обновить AnalyticsService, добавив отслеживание

  • Написать 23 теста для проверки проброса событий

На это ушло 2 рабочих дня. С postboy решение заняло 20 минут:

// Открытие модалки с аналитикой
postboy.fire(new OpenModalEvent({
  type: 'signup',
  source: 'navbar' // Контекст для аналитики
}));

// Глобальная подписка на все открытия модалок
postboy.sub(OpenModalEvent).subscribe(event => {
  analytics.track('modal-open', event.type, event.source);
});

Никаких правок в компонентах — просто добавили подписку в корневом модуле.

Почему это архитектурная ловушка?

  • Хрупкость: Изменение одного компонента вызывает волну правок в других

  • Тестируемость: Чтобы проверить кнопку, нужно мокать цепочку сервисов

  • Масштабируемость: Новые фичи увеличивают сложность экспоненциально

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

А ещё до четверти обсуждений код-ревью приходится на обсуждение «как правильно пробросить событие через компонент C».

Промежуточный итог: Angular-компоненты похожи на жителей мегаполиса — они могут быть независимы, но им нужна «центральная почта» для общения. В следующей части разберём, как реализовать такую почту: через самописное решение, NgRx или легкие библиотеки. Спойлер: иногда лучший фреймворк — это несколько десятков строк кода.

Часть 3: Бэкенд-уроки — Что фронтенд может украсть у RabbitMQ

Если бы компоненты умели разговаривать, они должны были бы попросить у бэкенда совета. RabbitMQ, Kafka и другие брокеры десятилетиями решают те же проблемы, что терзают фронтенд. Давайте «позаимствуем» четыре принципа, чтобы перестать изобретать велосипеды.

Принцип 1: Издатели не знают подписчиков

Как это работает в RabbitMQ:

Продавец (издатель) кладёт товар на склад (брокер). Ему всё равно, кто заберёт товар — курьер, клиент или вор (шутка).

Фронтенд-аналог:

Компонент публикует событие «Пользователь вошёл», не зная:

  • Кто обновит хедер

  • Кто отправит аналитику

  • Кто покажет приветственный тултип

// плохо
this.authService.login().pipe(
    tap(() => {
        this.header.refresh();
        this.analytics.trackLogin();
        this.tourService.start();
    })
);

// Как должно быть
this.authService.login().subscribe(() => {
    eventBus.publish(new UserLoggedInEvent());
});

Принцип 2: Сообщения — документация системы

Бэкенд-практика:

В RabbitMQ схемы сообщений (например, через Avro) — это живая документация API.

Фронтенд-реализация:

Каждое событие — класс с типизацией:

class PasswordChangedEvent {
    constructor(
        public readonly userId: string,
        public readonly method: 'email' | 'sms'
    ) {
    }
}

Теперь любой разработчик видит:

  • Какие данные содержит событие

  • Возможные значения полей

  • Где используется (через поиск по проекту)

Принцип 3: Очереди как буфер против хаоса

Паттерн бэкенда:

Если сервис-потребитель упал, RabbitMQ сохранит сообщения в очереди, пока он не оживёт.

Фронтенд-адаптация:

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

class AnalyticsService {
    private failedEvents: AnalyticEvent[] = [];

    constructor() {
        eventBus.subscribe(AnalyticEvent).subscribe(event => {
            try {
                this.sendToServer(event);
            } catch {
                this.failedEvents.push(event); // Сохраняем для повтора
            }
        });
    }
}

Принцип 4: Типизация как контракт

Бэкенд-пример:

В Kafka схемы регистрируются в Confluent Schema Registry. Несовместимые версии блокируются.

Фронтенд-реализация:

Используем TypeScript для защиты от ошибок:

// V1: Устаревшая версия
class ProductAddedEvent {
    constructor(public productId: string) {
    }
}

// V2: Новая версия
class ProductAddedEventV2 {
    constructor(
        public productId: string,
        public categoryId: string
    ) {
    }
}

// Подписчик ловит только свою версию
eventBus.subscribe(ProductAddedEventV2).subscribe(/* ... */);

Как это выглядит в идеальном мире

Представьте компоненты как независимые микросервисы:

  • Модуль A публикует CartUpdatedEvent с типом { cartId: string, items: CartItem[] }

  • Модуль B подписывается и обновляет бейдж корзины

  • Модуль C слушает то же событие для расчёта доставки

  • Модуль D пишет в LocalStorage

Никто не знает о существовании других. Изменили формат корзины? Просто создайте CartUpdatedEventV2 — старые подписчики останутся работать с V1.

Почему фронтенд отстаёт?

Синхронность мышления: «Нажали кнопку → вызвали метод → получили результат» — это legacy-подход.

Страх асинхронности: Разработчики боятся «плавающих» событий, хотя в бэкенде это норма.

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

Истории из жизни:

Когда мы внедрили типизированные события, новый разработчик смог за день подключить фичу «история заказов», просто изучив существующие классы сообщений. Раньше это требовало недели согласований.

Итог:

Бэкенд-архитекторы десятилетиями шлифовали event-driven подходы. Фронтенду пора перестать вариться в собственном соку и начать «красть» проверенные решения. В следующей части реализуем эти принципы на практике — от самописной шины до готовых решений.

Часть 4: Реализация на практике — От самописных решений до библиотек

Ответ на вопрос «Писать свой EventBus или использовать готовое решение?» зависит от масштаба. Давайте реализуем три варианта и разберём, когда какой подходит.

Вариант 1: Самописная шина на RxJS за 15 минут

Простейший EventBus на RxJS — это 20 строк кода:

import {filter, map, Subject} from 'rxjs';

type EventPayload<T> = { type: string; data?: T };

class EventBus {
   private _events$ = new Subject<EventPayload<unknown>>();

   publish<T>(type: string, data?: T): void {
      this._events$.next({type, data});
   }

   subscribe<T>(type: string) {
      return this._events$.pipe(
              filter(event => event.type === type),
              map(event => event.data as T)
      );
   }
}

// Использование
const bus = new EventBus();
bus.subscribe<string>('GREETING').subscribe(msg => console.log(msg));
bus.publish('GREETING', 'Hello from 1983!');

Плюсы:

  • Полный контроль

  • 0 зависимостей

  • Подходит для малых проектов

Минусы:

  • Нет типизации данных

  • Ручное управление подписками

  • Нет поддержки жизненного цикла Angular

Когда использовать:

  • Прототипы

  • Мини-приложения (<15 компонентов)

  • Обучение принципам Pub/Sub

Вариант 2: NgRx как хранилище событий

NgRx — это «тяжёлая артиллерия» с полным набором инструментов:

// actions/events.actions.ts
export const showModal = createAction(
    '[UI] Show Modal',
    props<{ type: string; context?: unknown }>()
);

// effects/events.effects.ts
showModal$ = createEffect(() =>
    this.actions$.pipe(
        ofType(showModal),
        tap(({type}) => console.log(`Modal ${type} opened`))
    ), {dispatch: false}
);

// component.ts
this.store.dispatch(showModal({type: 'confirm'}));

Плюсы:

  • DevTools с историей событий

  • Интеграция с состоянием приложения

  • Поддержка сложных сценариев (CQRS, саги)

Минусы:

  • Оверкилл для простых задач

  • Кривая обучения

  • Размер бандла

  • Проблемы типизации

Когда использовать:

  • Enterprise-приложения

  • Когда уже используется NgRx

  • Сложная бизнес-логика с отслеживанием состояния

Вариант 3: Специализированные библиотеки

Пример postboy:

// Определение события
class ApiErrorEvent extends PostboyGenericMessage {
    constructor(public readonly error: Error) {
        super();
    }
}

// Публикация
postboy.fire(new ApiErrorEvent(err));

// Подписка
postboy.sub(ApiErrorEvent).subscribe(event => {
    alert(`Ошибка ${event.error.message}`);
});

Когда использовать:

  • Средние проекты и Enterprise-приложения

  • Микрофронтенды

  • Постепенная миграция с legacy-кода

Когда события вредны: 3 опасных кейса

Как выбрать инструмент?

1. Карта решений:

  • < 15 компонентов: RxJS Subject

  • 15+ компонентов (до бесконечности): postboy или аналог

  • 50+ компонентов (до бесконечности): NgRx + дополнительные брокеры

2. Правило 48 часов:

Если за два дня не смогли внедрить Pub/Sub — ваш подход слишком сложен.

3. Тест на масштабируемость:

Попробуйте добавить реакцию на событие из совершенно нового модуля. Если это требует изменений в 3+ местах — архитектура не event-driven.

История из практики:

На проекте с 70 компонентами мы начали с самописного решения, а через полгода перешли на postboy. Это было как менять двигатель на летящем самолёте, но событийная архитектура позволила делать миграцию постепенно.

Итог:

Pub/Sub — не серебряная пуля. Это как молоток: им можно забить гвоздь, а можно разбить экран. Выбирайте инструмент под размер «гвоздя» и помните: лучшая архитектура та, которая позволяет спать по ночам, а не хвастаться на митапах.

Часть 5: Когда события вредны — Опасные сценарии Pub/Sub

Pub/Sub — это как огонь: греет, когда под контролем, и сжигает всё, когда вырывается наружу. Давайте разберём три сценария, когда событийная модель превращается из лекарства в яд.

1. Циклические зависимости: Бесконечный круговорот

Проблема:

Событие A вызывает BB вызывает C, а C снова вызывает A.

// Компонент А
postboy.subscribe(EventC).subscribe(() => {
    postboy.publish(new EventA()); // Зацикливание
});

// Компонент B
postboy.subscribe(EventA).subscribe(() => {
    postboy.publish(new EventB());
});

// Компонент C
postboy.subscribe(EventB).subscribe(() => {
    postboy.publish(new EventC());
});

Чем опасно:

  • Бесконечный цикл событий → 100% загрузка CPU

  • Невозможность отладки через DevTools

Решение:

  • Добавить debounce (RxJS-оператор):

postboy.subscribe(EventA).pipe(
    debounceTime(100)
).subscribe(/* ... */);
  • Использовать флаги-ингибиторы:

let isProcessing = false;

postboy.subscribe(EventA).subscribe(() => {
    if (!isProcessing) {
        isProcessing = true;
        // Логика...
        isProcessing = false;
    }
});
  • Навести порядок в логике вызовов, ибо это вообще не нормально

2. Утечки памяти: Призрачные подписки

Проблема:

Неотписанные подписки в сервисах накапливаются при горячем обновлении модулей.

@Injectable()
export class AnalyticsService {
    constructor() {
        // Подписка никогда не отписывается!
        eventBus.subscribe(TrackingEvent).subscribe(/* ... */);
    }
}

Чем опасно:

  • Утечка памяти → падение производительности

  • «Зомби-обработчики» реагируют на события после уничтожения компонента

Решение для Angular:

  • Использовать takeUntilDestroyed:

private destroyRef = inject(DestroyRef);

eventBus.subscribe(Event)
    .pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe(/* ... */);
  • Для сервисов — ручное управление:

private sub?: Subscription;

ngOnInit()
{
    this.sub = eventBus.subscribe(Event).subscribe(/* ... */);
}

ngOnDestroy()
{
    this.sub?.unsubscribe();
}

3. Слепые зоны типизации: Ошибки в темноте

Проблема:

Нетипизированные события превращаются в мины замедленного действия.

// Плохо: данные без контракта
eventBus.publish('user_updated', {id: 123, name: 'Alice'});

// Где-то в другом модуле
eventBus.subscribe('user_updated').subscribe(data => {
    console.log((data as any).age); // undefined → падение в рантайме
});

Чем опасно:

  • Ошибки обнаруживаются только в рантайме

  • Рефакторинг становится игрой в рулетку

Решение:

  • Использовать классы-сообщения:

class UserModel {id: string; name: string}

class UserUpdatedEvent extends PostboyGenericMessage {
    constructor(data: UserModel) {
        super();
    }
}

// Подписка с гарантией типов
postboy.subscribe(UserUpdatedEvent).subscribe(data => {
    console.log(data.name); // Тип string известен
});

Когда НЕ использовать Pub/Sub

Правило безопасности:

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

  • Есть ли подписчики кроме меня?

  • Может ли это событие вызвать неожиданные эффекты?

  • Можно ли решить задачу проще через Input/Output?

Итог:

Pub/Sub требует дисциплины. Это как ядерная энергия: при правильном обращении даёт свет, при ошибках — разрушение.

Часть 6: Заключение — Выращивая архитектуру

Архитектура программных систем — это не чертёж, высеченный в камне. Это живой сад, где компоненты, как растения, растут, переплетаются и иногда требуют обрезки. Pub/Sub — не волшебный посох, а инструмент садовника, который помогает управлять этим хаосом, не подавляя его.

Три правила эволюции

1. Начинайте с малого

Не пытайтесь внедрить событийную модель сразу во всём приложении. Начните с одного модуля, где связи уже напоминают паутину. Замените самый проблемный @Output() на событие — и наблюдайте, как архитектура начнёт меняться сама.

2. Рефакторите только при возникновении проблем

Если компоненты общаются через два уровня инициализации — не трогайте их. Как говорил Кент Бек: «Не решайте проблемы, которых у вас нет». Pub/Sub — лекарство от сложности, а не витамин для профилактики.

3. Выбирайте инструмент под масштаб

Самописная шина из 20 строк кода может быть лучше NgRx или postboy для проекта на 15 компонентов. Но когда система разрастается до 200+ акторов — ищите решения с типизацией.

Как начать завтра

1. Найдите «подозрительный» компонент

Тот, который знает о пяти других модулях. Замените один вызов метода на событие.

2. Документируйте контракты

Создайте папку events с классами сообщений. Даже если используете строковые типы — опишите их в JSDoc.

3. Устройте «день тишины»

Запретите команде использовать @Output() и сервисы-посредники неделю. Вы удивитесь, как быстро найдут event-driven альтернативы.

Эпилог для скептиков

«Но ведь события усложняют отладку!» — скажете вы. Отвечу историей: когда в нашем проекте внедрили событийную модель, новый разработчик за день подключил фичу, которую раньше делали бы неделю. Он просто нашёл нужное событие в документации и подписался.

Да, события — это ответственность. Да, они требуют дисциплины. Но они же дают свободу, сравнимую с переходом от монархии к демократии. Вы перестаётся быть богом-архитектором и становитесь садовником, который лишь задаёт правила роста.

postboy — один из инструментов в вашем сарае. Вы можете выбрать NgRx, самописную шину или что-то ещё. Суть не в библиотеке, а в смене парадигмы: перестать связывать компоненты и начать описывать их взаимодействие как договор равных.

Последний совет. Когда в следующий раз увидите цепочку из трёх @Output() — представьте, что это сорняк. Выдерните его, посадите событие и наблюдайте, как архитектура расцветёт.

P.S. Документация Postboy — здесь. Но если предпочитаете свой EventBus, я надеюсь, что этот опус сподвигнет вас на этот подвиг. Happy coding!

Комментарии (86)


  1. php7
    10.05.2025 09:14

    Фронтенд всегда был event-driven.
    В jQuery можно было пулять события. Пуляй - не хочу.


    1. artptr86
      10.05.2025 09:14

      Фреймворк Backbone в принципе весь был построен вокруг эвентов и подписок на них.


    1. artstesh Автор
      10.05.2025 09:14

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

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

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

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

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

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


      1. nicolas_d
        10.05.2025 09:14

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


        1. vanxant
          10.05.2025 09:14

          Менеджеры очередей на бэке нужны для общения разнородных сервисов на физически или виртуально разных устройствах

          Нет, главная фишка это снижение связанности компонентов. Сервис авторизации фигачит сообщения USER_LOGGED_IN и USER_LOGGED_OUT и ничего не знает про десятки других сервисов, которые на это реагируют. А эти "другие сервисы" знают только про структуру сообщений и всё, т.е. заменить сервис авторизации например моком не стоит вообще ничего.


          1. 1755
            10.05.2025 09:14

            А в чем отличие от redux?


            1. ruimage
              10.05.2025 09:14

              Отсутствием центрального состояния которое хранит это все. ИМХО тут чистый Event Emmiter получается. Надо оно нам, да вроде нет. Сейчас Flux решает все проблемы, т.к. совмещает в себе и функцию хранения и функцию подписки и отправки сообщений.


            1. vanxant
              10.05.2025 09:14

              В моём сценарии есть всего две функции pub и sub.

              Redux это такой комбайн на стероидах.

              При этом, если у вас в скажем мобильном приложении уже есть redux, можно использовать и его, почему нет.


              1. Chamie
                10.05.2025 09:14

                Redux это такой комбайн на стероидах.

                Redux — это буквально 6 файлов, суммарно на сотню строк кода, не считая комментарии.


          1. SergeyGershkovich
            10.05.2025 09:14

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


          1. viacheslav_ustinov
            10.05.2025 09:14

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


      1. Fenzales
        10.05.2025 09:14

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

        Ключевое слово - мы, т.е. конкретно вы, лично. Не обобщайте свой опыт на всю индустрию.

        Потому что на pub/sub в виде либо кастомных DOM-eventов, либо самописных eventbus-ов жили почти все крупные проекты со сколь-нибудь продуманной архитектурой. Ну и про backbone выше уже тоже сказали. И это мы еще Flash и, прости господи, Silverlight не вспоминаем.


        1. vanxant
          10.05.2025 09:14

          Просто замечу, что backbone, flash и, прости господи, Silverlight давно и надёжно на кладбище.

          На фронте же у нас по факту на первом месте jQuery, на второем реакт и на третьем vue


      1. Femistoklov
        10.05.2025 09:14

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

        Про observer не забывайте.

        Однако, соглашусь, что это всё не было шибко распространено и стандартизировано.


  1. Vindicar
    10.05.2025 09:14

    Вот насчёт типизации событий. Как бы вы посоветовали описывать схему событий так, чтобы её мог использовать любой проект-микросервис. Хотя бы на этапе разработки.

    Второй вопрос: имеет ли смысл навешивать на классы событий метаданные, описывающие, в какие очереди эти события отправляются? Или это неудачное разделение обязанностей?


    1. artstesh Автор
      10.05.2025 09:14

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

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

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

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

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

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

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

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

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

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

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


      1. kmatveev
        10.05.2025 09:14

        Использовать слово "антипаттерн" - это антипаттерн. А уж настолько облениться, что прогонять ответ на комментарий через LLM - это супер-антипаттрн.


      1. Vindicar
        10.05.2025 09:14

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

        1. Каков смысл определённого события (это не внести в код, только документировать)

        2. Каков состав определённого события (это легко решается описанием DTO-классов для содержимого событий)

        3. В каком топике должно быть помещено определённое событие

        Соответственно, по вопросу 2 возникает проблема: DTO должны быть объявлены в каждом сервисе, который их использует. Иными словами, значительная часть DTO будет требоваться более чем одному сервису. Дублировать это описание в каждый сервис = создавать себе головную боль в дальнейшем, когда потребуется поддерживать. Хочется иметь один источник истины в проекте. Встречал советы использовать git submodules, но это выглядит довольно громоздко. Хотя, зато случайно внести изменения в DTO будет сложнее.

        По вопросу 3 схожая проблема: разные топики несут сообщения с разным смыслом. Если одно и то же событие идёт в два разных топика (не очереди) - это, скорее всего, два семантически разных события, хотя они и имеют идентичное содержимое в настоящий момент. В этом случае я нахожу куда более вероятным, что со временем одно из них изменится, и нам всё равно потребуется два разных класса. И разные сервисы должны соглашаться относительно того, какой тип событий куда идёт.

        Ну и наконец, у меня зреет подозрение, что broker-agnostic подход - это такой же миф, как database-agnostic. В теории звучит круто, на практике миграция всё равно потребует полного рефакторинга системы - если миграция вообще потребуется. Стоит ли огород городить?


        1. Fenzales
          10.05.2025 09:14

          Дублировать это описание в каждый сервис = создавать себе головную боль в дальнейшем, когда потребуется поддерживать. Хочется иметь один источник истины в проекте. Встречал советы использовать git submodules, но это выглядит довольно громоздко.

          А что мешает их сделать зависимостью?


          1. Vindicar
            10.05.2025 09:14

            Хммм. Ну если речь про шарп, то да, это идея - библиотека классов, и все дела.

            А вот как быть с языками вроде питона... Поднимать свой репозиторий и делать приватный pip-пакет?


            1. Fenzales
              10.05.2025 09:14

              Ну насчет питона не подскажу (хотя и не вижу, что мешает), но на фронте мы так и делаем.


            1. Razor0077x
              10.05.2025 09:14

              Да

              Так же как и приватные пакеты для composer в php


    1. vanxant
      10.05.2025 09:14

      имеет ли смысл навешивать на классы событий метаданные, описывающие, в какие очереди эти события отправляются

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


  1. kmatveev
    10.05.2025 09:14

    Где вы набираетесь этой манеры излагать: с драматизацией, с громкими словами, с пафосом? Это звучит как клоунство. На конференциях что ли? По тону сразу чувствуется, что тебе будут что-то продавать, нахваливая достоинства, скрывая проблемы и недостатки.

    «Но ведь события усложняют отладку!» — скажете вы. Отвечу историей: когда в нашем проекте внедрили событийную модель, новый разработчик за день подключил фичу, которую раньше делали бы неделю. Он просто нашёл нужное событие в документации и подписался.

    Уж ответил так ответил, просто размазал аргументацией.

    В целом статья вроде норм, но было бы лучше, если бы часть "pub/sub решит все ваши проблемы" была поменьше.


    1. dopusteam
      10.05.2025 09:14

      Забавно, что в приведённой цитате нет ответа на аргумент про сложность отладки)

      Т.е. мало того, что аргументация слабая, так она, как будто, и не по делу, вообще


    1. shai_hulud
      10.05.2025 09:14

      Он просто нашёл нужное событие в документации и подписался.

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


  1. nihil-pro
    10.05.2025 09:14

    Самописная шина из 20 строк кода может быть лучше NgRx или postboy для проекта на 15 компонентов. Но когда система разрастается до 200+ акторов — ищите решения с типизацией.

    Если отбросить в сторону проблемы event-driven, то всю статью можно сократить до нескольких строк:

    Ребята, у нас есть «кафка*» в браузере. Вот как она работает:

    Можно использовать CustomEvent как есть, но получится «многословно» и с подсказками в IDE будет не очень, поэтому формируем реестр событий с единым API и храним их в одном месте. Api, например, такой:

    interface PriceChangedEventPayload {
      productId: string;
      newPrice: number;
    }
    
    export class PriceChangedEvent extends CustomEvent<PriceChangedEventPayload> {
      
      init: CustomEventInit<PriceChangedEventPayload>;
    
      // избавляемся от бойлерплейта при создании событий
      constructor(detail: PriceChangedEventPayload) {
        const init = { detail }
        
        super('onPriceChanged', init);
        
        this.init = init;
        
        // избавляемся от бойлерплейта при отправке событий
        dispatchEvent(this);
      }
      
    }
    
    // Добавляем подсказки для IDE и Typescript-а
    declare global {
      interface WindowEventMap {
        onPriceChange: PriceChangedEvent;
      }
    }

    Ну и пользуемся:

    // где-то в одном месте
    addEventListener('onPriceChange', (event) => {
      // ...
    });
    
    
    
    // где-то в другом месте
    import { PriceChangedEvent } from 'registry'
    
    if (priceWasChanged) {
      // Создание экземпляра сразу диспатчит событие
      new PriceChangedEvent({ productId, newPrice });
      
    }

    *Конечно это не кафка, но:

    • Формат сообщения = единственный контракт

    • Издатель не знает подписчиков

    • Брокер гарантирует доставку


  1. isumix
    10.05.2025 09:14

    15 КБ кода на Pub/Sub

    10 строчек всего, вот рабочий пример. Если 10 файлов вам пришлось править, то может вам досталось плохо-спроектированное приложение?

    export class Observable {
      #callbacks = new Set();
      notify() {
        for (const fn of this.#callbacks) fn();
      }
      subscribe(callback) {
        this.#callbacks.add(callback);
        return () => this.#callbacks.delete(callback); // unsubscribe
      }
    }


  1. dom1n1k
    10.05.2025 09:14

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


    1. Opaspap
      10.05.2025 09:14

      к счастью это больше не нужно уметь - ллм прекрасно верстают и знают все трюки css.


      1. dom1n1k
        10.05.2025 09:14

        ллм в этой ситуации - дополнительная положительная обратная связь


    1. evgeniyPP
      10.05.2025 09:14

      А какое решение? Отказаться от TS? Попробуйте, потом расскажите, как прошло)


      1. dom1n1k
        10.05.2025 09:14

        Простого решения тут нет, иначе бы его уже кто-то придумал.
        Но всё-таки кажется неправильным, когда фронты поголовно не умеют верстать.


        1. cupraer
          10.05.2025 09:14

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


  1. artptr86
    10.05.2025 09:14

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

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

    this.authService.login().subscribe(() => {
        eventBus.publish(new UserLoggedInEvent());
    });

    Плохо. Гораздо лучше так:

    class AuthService {
        @Observable
        user: User;
    
        login() {
            ...
            this.user = user;
        }
    }

    Когда поле user обновится, все заинтересованные сами узнают об этом через механизм реактивности. Даже Angular начал внедрять сигналы.

    Паттерн бэкенда:

    Если сервис-потребитель упал, RabbitMQ сохранит сообщения в очереди, пока он не оживёт.

    Фронтенд-адаптация:

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

    Выглядит неэквивалентно. У вас получается, что сервис аналитики должен сам кешировать сообщения из шины. И это даже не очередь, если сервис аналитики лежал или упал, то заново он это сообщение не получит. Если вы хотите воплощать Кафку на фронтенде, то собирайте односвязный список сообщений, пусть у каждого подписчика свой курсор будет, вводите retention policy. Тогда перезапущенный или вновь поднятый сервис аналитики перечитает недополученные сообщения и всё переделает.

    они же дают свободу, сравнимую с переходом от монархии к демократии

    Не, это, скорее, охлократия — власть толпы

    начать описывать их взаимодействие как договор равных

    Это нарушает принцип единственной ответственности и снижает связность (cohesion): любой модуль может отослать в шину любое сообщение, даже если отправитель не владеет этой информацией.

    Пример: как и выше есть сервис Auth, который логинит пользователя и рассылает сообщение UserLoggedInEvent. Есть некий сервис Foo, который выполняет какие-то действия при получении UserLoggedInEvent. Какой-то разработчик обнаружил, что ему нужно снова запускать какие-то действия в сервисе Foo уже после логина. Обычно такая история случается во время фикса бага. Он находит самое простое и тупое решение: из сервиса Bar начинает самостоятельно рассылать сообщение UserLoggedInEvent, чтобы инициировать действия в сервисе Foo. На самом же деле проблема в том, что сервис Foo неявно зависит и от каких-то других данных, но эту зависимость не оформили явно.

    Реактивная же модель, в отличие от модели pub/sub, самостоятельно обновляет данные, если обновился их источник. Фактически в ней тоже происходит рассылка сообщений, но более точечная, и только тем, кто в этом заинтересован, а сервисы-источники данных остаются единственными владельцами этих данных (source of truth).


  1. Opaspap
    10.05.2025 09:14

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

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


  1. FlyingDutchman2
    10.05.2025 09:14

    фронтенд-компоненты общались через цепочки Input/Output, словно это 2005-й, а мы пишем WinForms.

    А что такое "цепочки Input/Output" в WinForms? Я долгое время работаю с WinForms, но этого не знаю.

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

    А можно объяснить, что такое "проопсы" (для старпёров типа меня, которые не знают современного молодежного жаргона)?


    1. yakimchuk-ry
      10.05.2025 09:14

      Видимо, про JSX и подобные вещи речь, когда атрибуты являются значениями свойств объекта, передаваемого в компонент. Свойства = пропсы.

      <Component some={1} />

      some это проп (от property, свойство) со значением 1


    1. paamayim
      10.05.2025 09:14

      Это типа свойства компонентов (properties) или props сокращенно. Всякие данные для передачи вниз по дереву. От цвета кнопчек вплоть до всяких коллбеков и самих компонентов.Не то чтобы это современный жаргон а скорее наоборот дремучий.


  1. paamayim
    10.05.2025 09:14

    А разве angular не умер ещё ? Я помню ещё лет 10 назад делали на нем проект и после него даже vue уже казался пушка фоеймворк. Потом несколько лет юзали react и вот из всех веб фреймворков он зашёл больше всего. И уже тогда лет 7-8 назад он считался самым тяжёлым и тормозным а потом как мне казалось и вовсе канул в лету. С тех пор я мигрировал в мобилки пару лет с react native и вот уже года 4 как работаю с флаттером. А тут вот оказывается кто то ещё пишет на angular. Сорри за оффтоп просто удивило это. Ну а что касается event driven то мне кажется это больше про бекенд даже скорее полезно когда нужно объединить какие то независимые системы. К примеру у вас есть код или какая то часть общей системы и нужно его подружить с другой системой, возможно даже написанной на другом языке. Через события это удобно и у меня был опыт внедрения scala kafka в один такой проект. Полагаю есть ещё много областей для применения этих технологий но строить архитектуру на event bus и подобных техниках я бы крайне не рекомендовал.


    1. yar3333
      10.05.2025 09:14

      Писал на Vue, сейчас на Angular. Разница видится непринципиальной - скорее дело вкуса.

      По поводу шины полностью с вами согласен, далеко не везде оно нужно.


    1. Tomheten
      10.05.2025 09:14

      Не совсем понятно, какая версия имеется ввиду. AngularJs(первая версия, без typescript) - практически не используется, Angular 2+(и выше, текущая - 19 версия) - вполне себе используется.


  1. yakimchuk-ry
    10.05.2025 09:14

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

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

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

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

    Поэтому дело совсем не в пропсах или событиях, а скорее в уровне кадров, который сейчас довольно низкий, в основном ребята которые могут написать всё, но не смогут дописать и развить это всё через пару лет потому что шаг влево-вправо и оно развалится )


  1. Luzinov
    10.05.2025 09:14

    Минусы Pub/Sub:

    1. Если по событию выполняется команда, вы не знаете она была выполнена успешно или нет.

    2. Если подписчика больще одного, то непонятно в каком порядке они выполняются. Если говорят, что в порядке добавления, то это ненадежно.

    Promise лишены этих недостатков.

    P.S. Не люблю, когда приходят бекендеры, которые думают что они умнее всех и начинают рассказывать фронтам, как им надо жить.


    1. Arioch
      10.05.2025 09:14

      а это точно недостатки? само название PubSub показывает, что это не про команды, а про оповещения. А вопрос "в каком порядке они выполняются" вообще не имеет смысла, у нас в самых нищебродских процессорах сегодня больше 10 ядер, они все должны выполняться, а не "в порядке".


      1. mvv-rus
        10.05.2025 09:14

        у нас в самых нищебродских процессорах сегодня больше 10 ядер, они все должны выполняться, а не "в порядке".

        ЕМНИП в браузере Event Loop идет в одном потоке. И даже если использовать Worker'ы, до DOM им недоступен ("you can't directly manipulate the DOM from inside a worker", [из MDN](you can't directly manipulate the DOM from inside a worker,) ). В десктопном мире всё устроено примерно так же: WinForms и WPF в C# имеют однопоточный SynchronizationContext, и ко всем элементом GUI надо обращаться только из него (теоретически, в Windows оконные процедуры разных окон могут работать в разных потоках, но этой возможностью редко кто пользуется).


        1. Arioch
          10.05.2025 09:14

          в браузере Event Loop идет в одном потоке

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

          в Windows оконные процедуры разных окон могут работать в разных потоках

          WinForms же на основе Delphi VCL создавали. Так что скорее всего дело в синхронизации глобального кэша C#-объектов: шрифтов, кистей и т.д. Чтобы хэндлы не плодить до бесконечности при каждом изменении типа Font1.Size += 2. И чтобы не париться в отдельном кэше на каждый отдельный поток. Не то, чтобы это было сложно сделать, а просто не нужно. Новичков запутает, а понимающие и сами сделают, если в конкретной задаче надо.

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

          Но почему сразу ограничиваться десктопом (DOM или WPF - не важно)? Если речь про разные микросервисы с кроликами, если их взяли за идеальный образец для репликации, то тогда у отдельных "клиентов" шины просто нет разделяемого состояния, нет синхронизации, нет проблем одновременного исполнения.

          Даже в статье список: "Модуль C слушает то же событие для расчёта доставки, Модуль D пишет в LocalStorage" - это не про "рисовать окошки" (хотя C вероятно после расчёта и запросит такое изменение, но запросить можно тоже асинхронно). Тут у нас типовые расчетно-загрузочные worker thread, который GUI не трогают. Проблемы тяжеловесности kernel threads тоже можно избежать, поскольку код JS, а не нативный. Хотя спор о целесообразности green threads в JVM/CLR так вроде однозначно не решился.

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


          1. mvv-rus
            10.05.2025 09:14

            а не только о какой-то единственной реализации, типа V8.

            А вы уверены, что именно о единственнной реализации, а не всей экосистемы в целом, начиная с ECMAScript?

            WinForms же на основе Delphi VCL создавали.

            Вообще-то - делали с нуля: код VCL принадлежал не MS, а другому владелцьцу. Оптяь же VCL тоже была однопоточной, и не без причины: первую версию Delphi и VCL делали под Win3.x, где один поток был не то что на приложение, а на все графические приложения сразу, и если он стопорился, то всё что мог делать пользователь - это смотреть на песочные часы. Паэтому многопоточного доступа изначально никто не закладывал - а дльше уже начала играть роль совместимость.

            PS А на остальное я отвечать не буду: я от фронта далёк (и это к счастью). И хотя сама идея тащить микросервисный бардак ещё и на веб-страницу мне не нравится, но возразить аргументированно по ее поводу у меня не хватает знаний.


      1. Luzinov
        10.05.2025 09:14

        Я говорю не про название, а про то как используется паттерн в реальном мире.

        Что вы запускаете получив "оповещение"? Например запускаете транзакцию, которая может откатится?


  1. Arioch
    10.05.2025 09:14

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

    А куда ему ещё двигаться, если там сплошной цикл "лижем там, где больно" и при этом "надо написать что-то новое и назвать новым словом"

    Это обычный осциллятор, как пробки на дорогах:

    • Пробка на улице Иванова

    • Все посмотрели в навигатор, и потому что все умные - все поехали на улицу Петрова

    • Пробка на улице Петрова, улица Иванова пустая

    • Все посмотрели в навигатор...

    Очевидно (должно быть), что у любого инструмента есть плюса и минусы. Свалка отходов, в каком-то смысле. Чем чаще используется Самый Модный Сегодня Инструмент, тем больше в мире накапливается его отходов.

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

    Но про это пока никто не думает, у всех эйфория, "лижем где больно". Кроме того, про это говорить вообще вредно для продаж Инструмента2.

    Проходит какое-то время, старую свалку в самом деле разгребли, но накапливается другая свалка, и происходит Событие: чтобы разгрести отходы Инструмента2 придумывают революционный Инструмент3... почему-то очень похожий на старый и проклятый Инструмент1. Хотя цикл может быть и длиннее, чем два инструмента.

    ...когда-то конфигурации 1С скорее всего были в самом деле конфигурациями. Но complexity has to live somehwere и пришлось разрешить в конфигурации добавить скрипты, как исключение конечно. А потом ещё. И наконец "конфигурацией" стали называть программу на спец-языке. И у них, как у большинства программ, появились конфигурации конфигураций, пока ещё простые наверное.

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


  1. sldo_ru
    10.05.2025 09:14

    Интересный подход. Мне интересно еще вот что: судя под всему, проект, на основе которого вы сделали такие выводы

    Всего 12 файлов для одной кнопки! В мире микросервисов это выглядело бы как правка пяти разных репозиториев, чтобы поменять цвет лейбла. И тогда я задался вопросом: «Почему фронтенд, при всей своей прогрессивности, до сих пор не усвоил уроки распределённых систем?»

    Это не что иное, как просто криво написанный код, без учета того, что в будущем он может меняться, адаптироваться и быть готовым к модификации (это уже вопрос к принципам ООП, которые не были соблюдены). И тут тогда проблема не в том, что фронт не учел уроки, а что проект был изначально написан криво.

    В Angular-компонентах застряли пережитки эры империй:

    • Жёсткая иерархия вызовов

    • Сервисы как ESB-монстры

    • События через 5 уровней — как бюрократическая почта

    Понимаю, что речь про Angular, но в том же React можно было бы обойтись как Redux (как поминали выше), так и Context.

    Мои слова не отменяют смысл статьи и ее подход, просто как будто аргументация проблемы выстроена слабовато.


    1. sunnybear
      10.05.2025 09:14

      В реальном мире примерно 99,9%+ проектов изначально написаны криво :)


    1. yar3333
      10.05.2025 09:14

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


  1. Chamie
    10.05.2025 09:14

    Что только люди ни придумают, лишь бы Redux не использовать…