Event Bus em Delphi: construindo uma arquitetura orientada a eventos com Callbacks, Fluent Interface e SOLID

Event Bus em Delphi - DevSpace
Event Bus em Delphi - DevSpace

Última atualização em 3 de junho de 2026 por Willian Tuttoilmondo

Event Bus em Delphi é uma abordagem arquitetural que permite que diferentes partes de uma aplicação se comuniquem sem depender diretamente umas das outras. Em vez de um componente chamar outro componente explicitamente, eventos são publicados em um barramento central e entregues automaticamente aos interessados, promovendo baixo acoplamento e maior flexibilidade na evolução do sistema. Aprender a implementar este recurso em Delphi pode parecer complexo, mas é mais simples do que se imagina.

À medida que aplicações crescem, é comum encontrar rotinas responsáveis por executar múltiplas ações ao mesmo tempo: registrar logs, enviar notificações, atualizar dashboards, auditar operações e persistir informações. Embora funcionem inicialmente, essas implementações criam dependências rígidas entre componentes e tornam a manutenção cada vez mais complexa. O resultado é uma arquitetura difícil de testar, reutilizar e expandir.

Neste artigo construiremos um Event Bus em Delphi utilizando interfaces, callbacks, Fluent Interface e princípios SOLID. Além de implementar um mecanismo completo de publicação e assinatura de eventos, veremos como criar uma arquitetura orientada a eventos, desacoplada e extensível, capaz de servir como base para aplicações modernas, APIs RESTful e sistemas corporativos de grande porte.

Neste artigo você aprenderá:

  • O que é Event Bus em Delphi
  • Como implementar Publish/Subscribe
  • Como criar eventos tipados com Generics
  • Como executar assinantes assíncronos com TTask
  • Como aplicar SOLID em arquiteturas orientadas a eventos
  • Como integrar Event Bus a APIs RESTful

Sumário

O que é um Event Bus e quais problemas ele resolve

Um Event Bus é um mecanismo de comunicação que permite que diferentes partes de uma aplicação troquem informações sem depender diretamente umas das outras. Em vez de um componente chamar outro componente explicitamente, ele apenas publica um evento em um barramento central, ficando a cargo do Event Bus localizar e notificar todos os componentes interessados naquela informação.

Em uma aplicação tradicional, é comum encontrar serviços que conhecem detalhadamente todas as ações que devem ser executadas após uma determinada operação. Ao criar um pedido, por exemplo, a mesma rotina pode ser responsável por registrar logs, enviar notificações, atualizar indicadores, persistir informações de auditoria e acionar integrações externas. Embora essa abordagem funcione em sistemas pequenos, ela cria um forte acoplamento entre os componentes da aplicação.

Considere o exemplo de um sistema de vendas. Sempre que um pedido é criado, diversas ações podem precisar ser executadas. Uma implementação tradicional poderia ser semelhante a esta:

Neste cenário, o serviço responsável pela criação do pedido precisa conhecer todos os demais serviços envolvidos no processo. Sempre que uma nova funcionalidade for adicionada, como o envio de uma mensagem para um sistema externo ou a atualização de um relatório, o código da operação original precisará ser modificado. Isso viola princípios importantes de arquitetura, como o Open/Closed Principle, e torna a manutenção progressivamente mais complexa.

Com um Event Bus em Delphi, a responsabilidade muda completamente. Após concluir a operação, o serviço apenas publica um evento representando o que aconteceu:

A partir desse momento, todos os componentes interessados no evento poderão reagir a ele de forma independente. O serviço que criou o pedido não precisa conhecer os consumidores do evento, nem saber quantos são ou quais ações executam. O único compromisso existente é a publicação do evento.

Essa abordagem reduz significativamente o acoplamento entre componentes, melhora a reutilização de código, simplifica testes unitários e torna a aplicação mais flexível para evoluir ao longo do tempo. Novas funcionalidades podem ser adicionadas através da criação de novos assinantes, sem que seja necessário alterar o código responsável pela publicação dos eventos.

Em outras palavras, um Event Bus transforma a comunicação direta entre componentes em uma comunicação orientada a eventos, permitindo que sistemas complexos sejam construídos de forma mais organizada, escalável e alinhada com os princípios modernos de arquitetura de software.

Diferenças entre Event Bus, Observer e Callback Pattern

Ao estudar Event Bus em Delphi, é comum encontrar comparações com outros mecanismos de comunicação amplamente utilizados no desenvolvimento de software, especialmente os padrões Observer e Callback Pattern. Embora todos tenham como objetivo permitir que um componente reaja a eventos ou mudanças de estado, cada abordagem possui características próprias e atende a necessidades diferentes.

O Callback Pattern é a forma mais simples de comunicação entre componentes. Nele, um objeto recebe uma rotina ou método que será executado quando uma determinada operação for concluída. O componente responsável pela execução conhece explicitamente o callback que deverá ser chamado, estabelecendo uma relação direta entre quem produz e quem consome a informação.

Um exemplo simples de callback pode ser observado em uma operação matemática:

Nesse caso, existe apenas um produtor e um consumidor da informação. Quando a operação termina, o callback informado é executado diretamente pelo objeto responsável pelo processamento.

O padrão Observer amplia esse conceito permitindo que vários consumidores sejam notificados quando algo acontece. Um objeto, chamado de sujeito (Subject), mantém uma lista de observadores (Observers) e os notifica sempre que seu estado é alterado. Essa abordagem reduz parte do acoplamento existente nos callbacks tradicionais, mas ainda mantém uma dependência direta entre o sujeito e seus observadores.

Em aplicações Delphi, esse comportamento é bastante comum em componentes visuais. Um controle pode notificar diversos observadores quando seu estado muda, mas continua sendo responsável por gerenciar o ciclo de vida e o registro desses observadores.

O Event Bus, por sua vez, adiciona uma camada intermediária entre quem publica e quem consome eventos. Em vez de registrar observadores diretamente em um objeto específico, os eventos são publicados em um barramento central. Os assinantes registram interesse em determinados tipos de eventos e passam a ser notificados automaticamente quando esses eventos são publicados.

Essa arquitetura elimina o conhecimento mútuo entre produtores e consumidores. O componente responsável pela publicação de um evento não sabe quem irá recebê-lo, quantos assinantes existem ou quais ações serão executadas em resposta. Da mesma forma, os assinantes não precisam conhecer quem gerou o evento.

A principal diferença entre essas abordagens pode ser resumida da seguinte forma:

  • Callback Pattern: comunicação direta entre produtor e consumidor.
  • Observer: um produtor notifica múltiplos consumidores registrados diretamente nele.
  • Event Bus: produtores e consumidores comunicam-se através de um barramento central, sem qualquer dependência direta entre si.

Por esse motivo, o Event Bus em Delphi é frequentemente utilizado em arquiteturas orientadas a eventos, sistemas corporativos e aplicações distribuídas, onde baixo acoplamento, extensibilidade e facilidade de manutenção são requisitos fundamentais. Embora callbacks e observadores continuem sendo extremamente úteis em diversos cenários, o Event Bus oferece uma solução mais flexível quando a comunicação entre múltiplos componentes precisa ocorrer de forma transparente e desacoplada.

Reduzindo o acoplamento entre componentes utilizando eventos

Um dos principais objetivos de uma arquitetura moderna é reduzir o acoplamento entre componentes. Quanto mais dependências diretas existem entre as partes de uma aplicação, maior é o impacto de qualquer alteração realizada no sistema. Mudanças simples podem exigir modificações em diversos módulos, aumentando a complexidade da manutenção e elevando o risco de introdução de novos defeitos.

Em aplicações tradicionais, é comum que uma classe conheça explicitamente todos os serviços necessários para concluir uma operação. Considere o processo de criação de um pedido em um sistema comercial. Após persistir os dados, a aplicação pode precisar enviar um e-mail de confirmação, registrar informações de auditoria, atualizar indicadores gerenciais e notificar sistemas externos. Uma implementação convencional poderia ser semelhante a esta:

Embora funcional, essa abordagem cria uma forte dependência entre o serviço responsável pela criação do pedido e todos os demais componentes envolvidos no processo. Cada nova funcionalidade adicionada exigirá alterações nessa rotina, fazendo com que a classe acumule responsabilidades que não pertencem ao seu domínio principal.

O problema se torna ainda mais evidente conforme a aplicação cresce. O componente responsável pela operação passa a conhecer detalhes de infraestrutura, integração, monitoramento e auditoria, tornando-se um ponto central de dependências. Além de dificultar a manutenção, essa situação viola princípios importantes de projeto, como o Single Responsibility Principle (SRP) e o Open/Closed Principle (OCP).

Uma forma eficiente de reduzir esse acoplamento é utilizar eventos para comunicar o que aconteceu dentro da aplicação. Em vez de executar diretamente todas as ações necessárias, o componente responsável pela operação publica um evento representando o fato ocorrido.

Nesse cenário, a responsabilidade do serviço passa a ser apenas criar o pedido e informar ao sistema que um novo pedido foi criado. Todas as demais ações deixam de ser uma preocupação direta dessa classe.

Os componentes interessados passam a atuar como assinantes do evento. Um serviço pode ser responsável pelo envio de e-mails, outro pela auditoria, outro pela atualização de indicadores e assim por diante. Cada consumidor registra interesse no evento e reage a ele de forma independente.

A grande vantagem dessa abordagem é que novos comportamentos podem ser adicionados sem qualquer alteração no código responsável pela publicação do evento. Para incluir uma nova funcionalidade, basta criar um novo assinante e registrá-lo no barramento de eventos.

Além de reduzir o acoplamento, uma arquitetura baseada em eventos favorece a reutilização de componentes, simplifica testes unitários e melhora significativamente a escalabilidade da aplicação. Como produtores e consumidores não possuem dependências diretas entre si, cada parte do sistema pode evoluir de forma independente, tornando a manutenção mais simples e previsível.

Ao utilizar um Event Bus em Delphi, a comunicação deixa de ser baseada em chamadas diretas entre objetos e passa a ser orientada a eventos. O resultado é uma arquitetura mais flexível, extensível e alinhada às práticas modernas de desenvolvimento de software.

Modelando eventos através de interfaces

Para construir um Event Bus em Delphi de forma flexível e extensível, o primeiro passo é definir como os eventos serão representados dentro da aplicação. Um evento deve descrever algo que já aconteceu no sistema, como um pedido criado, um pagamento aprovado, um usuário autenticado ou uma falha registrada durante uma integração.

Em vez de representar eventos apenas como classes soltas, podemos modelá-los através de interfaces. Essa abordagem permite definir um contrato comum para todos os eventos da aplicação, facilitando o registro, a publicação e o consumo desses objetos pelo barramento de eventos. Além disso, o uso de interfaces em Delphi permite aproveitar a contagem automática de referências, reduzindo a necessidade de controle manual do ciclo de vida dos objetos.

A interface base para qualquer evento pode ser bastante simples:

Nessa estrutura, todo evento terá um identificador, uma data de criação e um nome. Esses três elementos serão úteis mais adiante para auditoria, logs, rastreamento e depuração da aplicação.

Para evitar repetição de código em todos os eventos concretos, podemos criar uma classe base abstrata responsável por implementar esse comportamento comum.

Com essa base definida, cada evento concreto pode se concentrar apenas nos dados que realmente fazem sentido para o domínio. Em um sistema comercial, por exemplo, podemos modelar um evento para representar a criação de um pedido.

Também podemos criar outros eventos seguindo exatamente a mesma estrutura. Um evento de pagamento aprovado, por exemplo, poderia ser modelado desta forma:

Observe que nenhum desses eventos conhece o Event Bus, os assinantes ou os serviços que irão reagir a eles. Um evento deve ser apenas uma descrição imutável de algo que aconteceu no sistema. Essa separação é importante para manter o baixo acoplamento e preservar a clareza da arquitetura.

Com os eventos modelados através de interfaces, teremos uma base sólida para implementar o mecanismo de publicação e assinatura. Nas próximas etapas, esses eventos serão publicados no barramento e consumidos por callbacks tipados, permitindo que diferentes partes da aplicação reajam a acontecimentos do domínio sem dependências diretas entre si.

Criando callbacks tipados com Generics

Depois de modelarmos os eventos através de interfaces, o próximo passo para construir um Event Bus em Delphi é definir como os assinantes serão notificados. Para isso, utilizaremos callbacks tipados com Generics, permitindo que cada evento possua um método de tratamento fortemente tipado e seguro em tempo de compilação.

No artigo sobre Callback Pattern em Delphi, vimos que callbacks podem ser definidos usando reference to procedure. Essa mesma abordagem será utilizada aqui, mas agora combinada com Generics para que o tipo do evento seja preservado durante a assinatura e a publicação. Assim, um assinante de IOrderCreated receberá exatamente um evento do tipo IOrderCreated, sem necessidade de casts espalhados pelo código consumidor.

A primeira estrutura que precisamos criar é o tipo genérico do callback. Ele será responsável por representar qualquer rotina capaz de tratar um evento específico.

Com essa definição, podemos criar callbacks diferentes para cada tipo de evento da aplicação. Um evento de pedido criado poderá ser tratado por um callback específico para IOrderCreated, enquanto um evento de pagamento aprovado poderá ser tratado por outro callback específico para IPaymentApproved.

A principal vantagem dessa abordagem é a segurança de tipo. O compilador passa a impedir que um callback preparado para tratar um tipo de evento seja usado indevidamente para outro. Isso reduz erros em tempo de execução e torna o código mais claro para quem consome o Event Bus.

Um exemplo simples de utilização pode ser visto abaixo. Nesse cenário, criamos uma variável capaz de armazenar um callback para eventos do tipo IOrderCreated.

Embora esse exemplo ainda não publique eventos, ele já demonstra a base do mecanismo que utilizaremos no barramento. O callback recebe uma interface de evento concreta, acessa suas propriedades e executa alguma ação em resposta ao acontecimento ocorrido.

À medida que avançarmos na implementação do Event Bus, precisaremos armazenar esses callbacks internamente para que possam ser executados quando um evento for publicado. Para isso, vamos criar uma estrutura responsável por representar uma inscrição (subscription) dentro do barramento.

O primeiro passo é definir uma interface que represente uma assinatura de evento.

Essa interface será utilizada futuramente para controlar o ciclo de vida das inscrições, permitindo inclusive que um assinante seja removido do Event Bus quando não desejar mais receber notificações.

Agora podemos criar uma implementação concreta para essa assinatura. Ela armazenará um identificador único e o nome do evento associado ao registro.

Como estamos construindo um projeto funcional, também precisaremos de uma estrutura capaz de armazenar o callback associado à assinatura. Para isso, vamos criar uma versão genérica da inscrição, responsável por manter a referência do callback que será executado quando o evento for publicado.

A interface IEventSubscriptionInternal será usada internamente pelo Event Bus. Ela permitirá verificar se determinada assinatura é compatível com um evento publicado e, em caso positivo, despachar esse evento para o callback correspondente.

Essa classe será uma peça fundamental na implementação do padrão Publish/Subscribe, pois permitirá que o Event Bus mantenha uma coleção de assinaturas registradas e execute automaticamente os callbacks corretos quando um evento for publicado.

Neste ponto, já temos quatro elementos importantes para o nosso projeto:

  • Uma interface base para eventos (IEvent);
  • Eventos concretos modelados através de interfaces;
  • Callbacks tipados utilizando Generics;
  • Estruturas de assinatura para armazenar os callbacks registrados.

Esses componentes serão utilizados na próxima etapa para implementar o núcleo do Event Bus, responsável por registrar assinantes, publicar eventos e distribuir notificações para todos os consumidores interessados.

A combinação de callbacks com Generics torna o Event Bus mais expressivo, seguro e alinhado com uma arquitetura moderna em Delphi. Em vez de trabalhar com estruturas genéricas demais, como TObject, Pointer ou casts manuais, passamos a criar um fluxo de comunicação fortemente tipado, legível e muito mais fácil de manter.

Implementando o padrão Publish/Subscribe (Pub/Sub)

Depois de criarmos os eventos, os callbacks tipados e as estruturas de assinatura, podemos finalmente implementar o núcleo do nosso Event Bus em Delphi. É nesta etapa que o padrão Publish/Subscribe, também conhecido como Pub/Sub, começa a acontecer de fato.

A ideia central do Pub/Sub é simples: um componente publica um evento e todos os assinantes interessados naquele tipo de evento são notificados automaticamente. Quem publica não conhece quem consome. Quem consome não precisa conhecer quem publicou. Toda a comunicação passa a ser intermediada pelo Event Bus.

Para começar, definiremos a interface principal do barramento de eventos. Essa interface terá três responsabilidades básicas: registrar assinantes, remover assinaturas e publicar eventos.

A assinatura do método Subscribe utiliza Generics para garantir que o callback registrado seja compatível com o tipo do evento. Isso significa que, ao assinar um evento do tipo IOrderCreated, o callback receberá exatamente um IOrderCreated.

O método Publish, por sua vez, recebe um evento concreto e distribui esse evento para todos os assinantes registrados para aquele tipo. Já o método Unsubscribe permite remover uma assinatura previamente criada, evitando que o callback continue sendo executado.

Com todas as estruturas auxiliares já definidas, podemos implementar o Event Bus propriamente dito. Para armazenar os assinantes, utilizaremos uma lista de IEventSubscriptionInternal. Dessa forma, o barramento consegue manipular todas as assinaturas de maneira uniforme, independentemente do tipo específico do evento.

Agora já temos um Event Bus funcional com suporte ao padrão Publish/Subscribe. A assinatura é feita com Subscribe, a publicação é feita com Publish e a remoção de assinantes pode ser realizada com Unsubscribe.

Um uso inicial do barramento ficaria assim:

Também podemos registrar múltiplos assinantes para o mesmo evento. Essa é uma das principais vantagens do padrão Pub/Sub: uma única publicação pode disparar diversas ações independentes.

Com essa estrutura, o serviço responsável por criar um pedido não precisa conhecer os serviços de e-mail, auditoria ou dashboard. Ele apenas publica o evento IOrderCreated, e o Event Bus se encarrega de entregar esse evento a todos os interessados.

Esse é o ponto em que o Event Bus em Delphi começa a mostrar seu real valor arquitetural. O código deixa de ser baseado em chamadas diretas entre componentes e passa a ser organizado em torno de eventos. Isso reduz acoplamento, facilita a manutenção e permite que novas funcionalidades sejam adicionadas através de novos assinantes, sem modificar o fluxo principal da aplicação.

Construindo um Event Bus utilizando Fluent Interface

Até este ponto já construímos um Event Bus em Delphi totalmente funcional. Os eventos podem ser publicados através do método Publish, os consumidores podem se registrar utilizando Subscribe e as inscrições podem ser removidas através de Unsubscribe.

Embora essa implementação já resolva o problema da comunicação desacoplada entre componentes, ainda podemos melhorar significativamente sua experiência de uso. É aqui que entra o padrão Fluent Interface, responsável por transformar sequências de chamadas em uma API mais legível, intuitiva e agradável de utilizar.

Como todos os métodos principais do nosso barramento já retornam IEventBus, não precisamos criar uma classe auxiliar ou um builder adicional. O próprio Event Bus já possui a estrutura necessária para se comportar como uma API fluida.

Sem uma interface fluida, o registro de vários assinantes para um mesmo evento poderia ficar assim:

O código funciona, mas repete a chamada a TEventBus.Instance diversas vezes. Em um sistema real, com múltiplos eventos e vários assinantes, essa repetição rapidamente compromete a legibilidade.

Como o método Subscribe retorna a própria interface do barramento, podemos encadear todas as inscrições em um único fluxo.

A leitura desse código é bastante natural: obtemos a instância do Event Bus e registramos, em sequência, todos os assinantes interessados no evento IOrderCreated. Cada bloco representa uma reação diferente ao mesmo acontecimento de domínio.

A publicação de eventos também pode se beneficiar da mesma fluidez. Podemos publicar um evento isolado:

Ou, se fizer sentido para determinado fluxo da aplicação, publicar mais de um evento em sequência:

Embora publicar vários eventos em sequência não seja obrigatório, a possibilidade de encadear chamadas torna a API mais flexível e consistente. O ponto mais importante é que o consumidor passa a interagir com o Event Bus de maneira limpa, previsível e com baixa verbosidade.

Em termos arquiteturais, a Fluent Interface não altera o funcionamento interno do barramento. O que ela melhora é a experiência de consumo. O Event Bus continua responsável por registrar assinantes, publicar eventos e distribuir notificações, mas agora essa interação passa a ser escrita de forma mais próxima da linguagem natural.

Com isso, nosso Event Bus em Delphi passa a reunir três características importantes: comunicação desacoplada através de eventos, callbacks tipados para segurança em tempo de compilação e uma API fluida capaz de tornar o uso do barramento mais claro e elegante. Na próxima etapa, evoluiremos essa implementação para permitir o registro e a remoção dinâmica de assinantes.

Aplicando os princípios SOLID em uma arquitetura orientada a eventos

Ao construir um Event Bus em Delphi, não estamos apenas criando uma estrutura para publicar e assinar eventos. Estamos também organizando a comunicação entre componentes de forma mais alinhada com princípios modernos de arquitetura de software. Nesse contexto, os princípios SOLID ajudam a entender por que uma arquitetura orientada a eventos tende a ser mais flexível, extensível e fácil de manter.

O objetivo do SOLID não é adicionar complexidade ao código, mas reduzir dependências desnecessárias, separar responsabilidades e permitir que novas funcionalidades sejam adicionadas com menor impacto sobre o restante da aplicação. O Event Bus contribui diretamente para isso, pois faz com que publicadores e assinantes se comuniquem através de abstrações, sem dependências diretas entre si.

Single Responsibility Principle

O Single Responsibility Principle, ou princípio da responsabilidade única, afirma que uma classe deve ter apenas um motivo para mudar. Em uma implementação tradicional, é comum que um serviço de domínio acabe acumulando responsabilidades que não pertencem diretamente a ele.

Nesse exemplo, o serviço responsável pela criação do pedido também conhece regras de e-mail, auditoria, dashboard e notificação. Embora o código seja simples, ele concentra responsabilidades demais em uma única rotina.

Ao utilizar eventos, o serviço passa a ter uma responsabilidade mais clara: criar o pedido e publicar o fato de que ele foi criado.

Agora, cada ação derivada da criação do pedido pode ser tratada por um assinante específico. O envio de e-mail, o registro de auditoria e a atualização do dashboard deixam de ser responsabilidades do serviço de pedidos.

Open/Closed Principle

O Open/Closed Principle determina que entidades de software devem estar abertas para extensão, mas fechadas para modificação. Esse princípio fica bastante evidente em uma arquitetura baseada em eventos.

Se amanhã precisarmos enviar uma mensagem para um sistema externo sempre que um pedido for criado, não será necessário alterar o método CreateOrder. Basta adicionar um novo assinante para o evento IOrderCreated.

O comportamento da aplicação foi estendido sem modificar o código responsável pela criação do pedido. Esse é um dos maiores benefícios do Event Bus: novas reações a eventos podem ser adicionadas sem alterar o fluxo principal da regra de negócio.

Liskov Substitution Principle

O Liskov Substitution Principle afirma que implementações concretas devem poder substituir suas abstrações sem quebrar o comportamento esperado pelo sistema. No nosso Event Bus, esse princípio aparece principalmente no uso de interfaces para representar eventos.

Como o barramento trabalha com a interface IEvent, qualquer evento concreto que implemente esse contrato pode ser publicado e tratado pelo mecanismo de distribuição.

Da mesma forma, assinantes especializados podem trabalhar com contratos mais específicos, como IOrderCreated ou IPaymentApproved, desde que esses contratos preservem o comportamento esperado de um evento.

Essa substituição é segura porque todos os eventos concretos respeitam o contrato definido pelas interfaces. O Event Bus não precisa conhecer a classe concreta do evento, apenas o contrato que ele implementa.

Interface Segregation Principle

O Interface Segregation Principle recomenda que clientes não sejam obrigados a depender de métodos que não utilizam. No Event Bus, esse princípio aparece na separação entre interfaces públicas e interfaces internas.

A aplicação consumidora precisa conhecer apenas a interface pública da assinatura.

Já o Event Bus precisa de operações adicionais, como verificar se uma inscrição suporta determinado evento e despachar a notificação. Essas operações ficam em uma interface interna separada.

Com essa separação, o consumidor da API não é exposto a detalhes internos do barramento. Ele recebe uma assinatura, mas não precisa conhecer o processo de despacho dos eventos. Isso mantém a API pública mais simples e reduz o acoplamento com a implementação interna.

Dependency Inversion Principle

O Dependency Inversion Principle afirma que módulos de alto nível não devem depender de módulos de baixo nível, mas ambos devem depender de abstrações. Em uma arquitetura orientada a eventos, esse princípio é aplicado quando os componentes passam a depender de contratos, e não de implementações concretas.

O serviço responsável por criar pedidos não precisa conhecer TEmailService, TAuditService ou TDashboardService. Ele depende apenas da abstração representada pelo Event Bus e pelo contrato do evento publicado.

Essa pequena alteração torna o serviço mais fácil de testar, pois podemos fornecer uma implementação falsa de IEventBus durante os testes. Além disso, o serviço deixa de depender diretamente do Singleton TEventBus.Instance, ficando mais flexível para cenários reais de aplicação.

Com isso, o Event Bus deixa de ser apenas um utilitário global e passa a fazer parte de uma arquitetura orientada a abstrações. O resultado é um código mais testável, extensível e preparado para evoluir com menor impacto sobre o restante da aplicação.

Ao aplicar os princípios SOLID em uma arquitetura orientada a eventos, percebemos que o Event Bus não é apenas uma forma elegante de disparar notificações. Ele se torna uma ferramenta arquitetural para separar responsabilidades, reduzir acoplamento, favorecer extensões e permitir que diferentes partes da aplicação evoluam de maneira independente.

Registrando e removendo assinantes dinamicamente

Depois de implementar o padrão Publish/Subscribe e tornar a API do nosso Event Bus em Delphi fluida, o próximo passo é permitir que assinantes sejam registrados e removidos dinamicamente durante a execução da aplicação. Esse recurso é importante em sistemas reais, especialmente quando determinados componentes existem apenas temporariamente, como formulários, telas, módulos carregados sob demanda, rotinas de integração ou processos em segundo plano.

Até aqui, o método Subscribe registra um callback no barramento e o método Unsubscribe remove uma assinatura previamente registrada. Para que isso funcione de maneira segura, precisamos guardar a referência da assinatura retornada no momento do registro.

Considere um formulário que deseja ser notificado sempre que um pedido for criado. Ao abrir a tela, ele registra um assinante no Event Bus. Ao fechar a tela, ele remove essa assinatura para evitar que o callback continue sendo executado quando o formulário já não estiver mais disponível.

O método Open será responsável por registrar o assinante e guardar a assinatura retornada pelo Event Bus.

Já o método Close utilizará essa mesma assinatura para remover o assinante do barramento.

Essa abordagem é simples, mas poderosa. A aplicação passa a ter controle explícito sobre o ciclo de vida dos assinantes. Isso evita notificações indesejadas, reduz riscos de acessar objetos que já deveriam ter sido descartados e torna o comportamento do sistema mais previsível.

Para que esse fluxo funcione corretamente, o método Subscribe precisa retornar a assinatura criada. Na versão fluida apresentada anteriormente, fizemos Subscribe retornar IEventBus. Essa decisão melhora o encadeamento das chamadas, mas dificulta o armazenamento da assinatura retornada.

Uma forma elegante de resolver isso é separar duas intenções diferentes: registrar assinantes de forma fluida e registrar assinantes quando precisamos controlar explicitamente a assinatura criada. Para isso, adicionaremos um novo método chamado SubscribeFor.

Agora temos duas formas de registrar assinantes. O método Subscribe mantém a API fluida, ideal para configurações encadeadas. Já SubscribeFor retorna a assinatura criada, permitindo que o consumidor remova esse assinante posteriormente.

A implementação do Event Bus pode reaproveitar a mesma lógica nos dois métodos. O método SubscribeFor cria a assinatura, adiciona à lista interna e retorna a referência criada.

Com SubscribeFor implementado, o método fluido Subscribe pode simplesmente chamar esse novo método e retornar a própria instância do barramento.

A remoção continua sendo feita pelo método Unsubscribe, que percorre a lista de assinaturas procurando pelo identificador da assinatura informada.

Com essa alteração, podemos reescrever o exemplo do dashboard de forma mais apropriada.

Também podemos demonstrar o comportamento publicando um evento antes e depois da remoção da assinatura.

Nesse exemplo, o primeiro evento será recebido pelo dashboard, pois a assinatura ainda está ativa. Após a chamada a Close, a assinatura é removida do Event Bus. O segundo evento continuará sendo publicado normalmente, mas o dashboard não será mais notificado.

Essa capacidade de registrar e remover assinantes dinamicamente é essencial para aplicações reais. Ela permite que componentes participem do fluxo de eventos apenas enquanto fizer sentido, sem criar dependências permanentes ou callbacks órfãos. Em aplicações com formulários, módulos plugáveis, telas temporárias ou processos de longa duração, esse controle evita efeitos colaterais difíceis de rastrear.

Com SubscribeFor, mantemos o melhor dos dois mundos: uma API fluida para registros simples e uma API controlável para cenários em que o ciclo de vida da assinatura precisa ser gerenciado explicitamente. Na próxima seção, veremos como publicar eventos para múltiplos consumidores simultaneamente e como organizar esses assinantes de forma mais próxima de uma aplicação real.

Publicando eventos para múltiplos consumidores simultaneamente

Uma das principais vantagens do padrão Publish/Subscribe é permitir que um único evento seja consumido por diversos componentes ao mesmo tempo. Em uma arquitetura orientada a eventos, um acontecimento de domínio raramente interessa apenas a um único módulo da aplicação. Pelo contrário, é comum que diferentes partes do sistema reajam de maneiras distintas ao mesmo fato.

Considere novamente o evento IOrderCreated. Quando um pedido é criado, diversas ações podem precisar ocorrer simultaneamente: envio de e-mail, auditoria, atualização de dashboards, integração com sistemas externos e geração de métricas.

Quando o evento for publicado, todos os assinantes compatíveis serão executados automaticamente.

Essa característica transforma o Event Bus em uma poderosa ferramenta de extensão arquitetural. Novos comportamentos podem ser adicionados simplesmente registrando novos assinantes, sem modificar o fluxo principal da aplicação.


Criando eventos de domínio (Domain Events)

Nem todo evento representa apenas uma mensagem técnica. Em aplicações corporativas, é comum utilizar o conceito de Domain Events, que representam acontecimentos relevantes do domínio de negócio.

Um Domain Event descreve algo que já aconteceu e possui significado para a organização. Exemplos incluem:

  • Pedido criado;
  • Pagamento aprovado;
  • Cliente bloqueado;
  • Produto esgotado;
  • Contrato cancelado.

Em vez de utilizar eventos genéricos, podemos criar contratos específicos para cada acontecimento de negócio.

Uma implementação concreta poderia ser construída da seguinte forma:

Ao trabalhar com Domain Events, a aplicação passa a comunicar acontecimentos de negócio de forma explícita, melhorando a legibilidade da arquitetura e aproximando o código da linguagem utilizada pelos especialistas do domínio.

Integrando Event Bus a APIs RESTful

Uma API RESTful também pode se beneficiar significativamente de uma arquitetura baseada em eventos. Em vez de executar todas as ações dentro do endpoint, a API pode publicar eventos e delegar o restante do processamento aos assinantes registrados.

Considere um endpoint responsável pela criação de pedidos.

Nesse cenário, a API responde rapidamente ao cliente e o restante do processamento ocorre através dos assinantes registrados.

Essa abordagem favorece:

  • Baixo acoplamento;
  • Separação entre domínio e infraestrutura;
  • Maior organização das regras de negócio;
  • Facilidade para adicionar novos comportamentos futuramente.

Em APIs maiores, essa estratégia se aproxima bastante dos conceitos utilizados em arquiteturas orientadas a eventos e até mesmo em soluções baseadas em CQRS.

Tornando um Event Bus thread-safe

Até este ponto, nosso Event Bus funciona perfeitamente em aplicações monothread. Entretanto, quando múltiplas threads começam a registrar assinantes ou publicar eventos simultaneamente, surge a necessidade de proteger as estruturas internas do barramento.

O principal ponto crítico é a coleção FSubscriptions, que pode sofrer acessos concorrentes.

Uma solução simples consiste em utilizar TCriticalSection.

A criação e destruição do lock podem ser feitas no construtor e destrutor.

Durante o registro de assinantes:

O mesmo procedimento deve ser aplicado em Publish e Unsubscribe.

Com isso, evitamos condições de corrida e garantimos que a coleção permaneça consistente mesmo em ambientes concorrentes.

Executando assinantes de forma assíncrona com TTask

Em muitos cenários, não é desejável que o publicador espere a conclusão de todos os assinantes. Imagine um pedido criado que dispara envio de e-mails, auditoria e integrações externas. O usuário não precisa aguardar a conclusão de todas essas operações.

Uma solução elegante consiste em executar cada assinante em uma TTask.

Agora a publicação retorna imediatamente, enquanto os consumidores executam em paralelo.

Essa abordagem é extremamente útil para:

  • Envio de e-mails;
  • Integrações externas;
  • Logs;
  • Auditoria;
  • Atualização de dashboards;
  • Geração de métricas.

Naturalmente, os assinantes precisam ser thread-safe para operar corretamente nesse modelo.

Utilizando Event Bus em Delphi para logs, auditoria, notificações e dashboards

Uma das aplicações mais comuns de um Event Bus é a centralização de comportamentos transversais da aplicação.

Em vez de espalhar chamadas para logs e auditoria por todo o sistema, podemos registrar consumidores especializados.

Log:

Auditoria:

Dashboard:

Notificações:

Todos esses comportamentos são adicionados sem alterar uma única linha da regra de negócio responsável pela criação do pedido.

Construindo uma arquitetura desacoplada, extensível e de fácil manutenção

Ao longo deste artigo construímos um Event Bus em Delphi completo utilizando interfaces, callbacks tipados, Generics, Fluent Interface e princípios SOLID.

O resultado é uma arquitetura na qual os componentes deixam de depender diretamente uns dos outros e passam a se comunicar através de eventos. O serviço responsável pela criação de pedidos não precisa conhecer dashboards, serviços de e-mail, auditorias ou integrações externas. Ele apenas informa ao sistema que um determinado acontecimento ocorreu.

Essa separação traz benefícios importantes:

  • Redução do acoplamento;
  • Maior reutilização de código;
  • Facilidade de testes;
  • Extensibilidade sem alteração do código existente;
  • Melhor organização das responsabilidades;
  • Arquitetura preparada para crescimento.

Mais importante ainda, a implementação construída ao longo deste artigo não depende de bibliotecas externas nem de frameworks específicos. Todos os conceitos foram desenvolvidos utilizando recursos nativos do Delphi, demonstrando como é possível criar soluções modernas e alinhadas às melhores práticas de arquitetura utilizando apenas a linguagem e sua biblioteca padrão.

Embora simples, essa implementação já fornece uma base sólida para aplicações desktop, APIs RESTful, serviços Windows e sistemas corporativos. A partir dela, é possível evoluir para cenários mais avançados, como filas persistentes, integração com brokers de mensagens, CQRS, Event Sourcing e arquiteturas distribuídas.

O mais importante é perceber que o Event Bus não é apenas um mecanismo de notificação. Ele é uma ferramenta arquitetural capaz de transformar a forma como os componentes de uma aplicação se relacionam, promovendo baixo acoplamento, alta coesão e uma estrutura muito mais preparada para evoluir ao longo do tempo.

Exemplo prático: Event Bus em Delphi aplicado à Arquitetura Limpa

Até aqui construímos um Event Bus em Delphi funcional, com suporte a callbacks tipados, Fluent Interface, múltiplos assinantes, remoção dinâmica, execução assíncrona com TTask e proteção para ambientes concorrentes. Agora vamos organizar essa implementação dentro de uma estrutura mais próxima de uma aplicação real baseada em Arquitetura Limpa.

A principal mudança em relação aos exemplos anteriores está na separação das responsabilidades. Em vez de expor diretamente um Singleton global através de TEventBus.Instance, passaremos a trabalhar com abstrações e injeção de dependência. Dessa forma, os casos de uso dependerão apenas da interface IEventBus, enquanto a implementação concreta ficará isolada na camada de infraestrutura.

Uma estrutura possível para o projeto seria a seguinte:

Na camada Application ficarão os contratos do Event Bus. Essa camada define o que a aplicação precisa, mas não como o barramento será implementado. Isso permite substituir a implementação concreta futuramente sem alterar os casos de uso.

Application/EventBus/Devspace.Application.EventBus.Event.pas

Application/EventBus/Devspace.Application.EventBus.Callback.pas

Application/EventBus/Devspace.Application.EventBus.Subscription.pas

Application/EventBus/Devspace.Application.EventBus.Intf.pas

Application/EventBus/Devspace.Application.EventBus.Facade.pas

A partir desse ponto, qualquer camada que precise publicar eventos dependerá apenas de IEventBus. O domínio não precisa conhecer a implementação concreta, nem a aplicação precisa saber se o Event Bus utiliza lista em memória, fila, broker externo ou qualquer outro mecanismo.

Na camada Domain, criaremos os eventos de negócio. Eles representam fatos relevantes que já aconteceram dentro da aplicação.

Domain/Events/Devspace.Domain.Events.OrderCreated.pas

Domain/Events/Devspace.Domain.Events.PaymentApproved.pas

As interfaces dos eventos ficam no domínio porque fazem parte da linguagem da aplicação. Já as implementações concretas desses eventos podem ficar na infraestrutura, pois envolvem detalhes de construção, identificação e data de criação.

Infrastructure/EventBus/Devspace.Infrastructure.EventBus.Event.Base.pas

Infrastructure/Events/Devspace.Infrastructure.Events.OrderCreated.pas

Infrastructure/Events/Devspace.Infrastructure.Events.PaymentApproved.pas

Agora podemos implementar o núcleo do barramento na camada de infraestrutura. Essa camada contém os detalhes técnicos, como lista de assinantes, sincronização com TCriticalSection e execução assíncrona com TTask.

Infrastructure/EventBus/Devspace.Infrastructure.EventBus.Subscription.Base.pas

Infrastructure/EventBus/Devspace.Infrastructure.EventBus.Subscription.Intf.pas

Infrastructure/EventBus/Devspace.Infrastructure.EventBus.Subscription.Typed.pas

Infrastructure/EventBus/Devspace.Infrastructure.EventBus.pas

Com o Event Bus implementado na infraestrutura, podemos criar um caso de uso na camada de aplicação. Esse caso de uso dependerá apenas da interface IEventBus, sem conhecer a classe concreta TEventBus.

Application/UseCases/Devspace.Application.UseCases.CreateOrder.pas

Em um projeto ainda mais rigoroso, a criação concreta dos eventos também poderia ser movida para fábricas ou serviços de domínio. Para fins didáticos, manteremos a criação no caso de uso para preservar a clareza do exemplo.

Agora registraremos os assinantes da aplicação. Esses assinantes representam comportamentos independentes que reagem aos eventos publicados pelo caso de uso.

Application/Subscribers/Devspace.Application.Subscribers.Order.pas

Por fim, a camada de apresentação será responsável apenas por montar as dependências e executar o caso de uso. Neste exemplo, usaremos uma aplicação console, mas a mesma estrutura poderia ser utilizada em uma aplicação VCL, FMX, serviço Windows ou API RESTful.

Presentation/Console/EventBusCleanArchitectureDemo.dpr

Observe que a aplicação console conhece a implementação concreta TEventBus, mas o caso de uso conhece apenas a interface IEventBus. Essa é uma separação importante: a camada externa monta as dependências, enquanto a camada de aplicação trabalha apenas com abstrações.

Com essa estrutura, o exemplo passa a respeitar melhor os princípios da Arquitetura Limpa. O domínio define os eventos relevantes para o negócio, a aplicação define os contratos e casos de uso, a infraestrutura implementa os detalhes técnicos do barramento e a apresentação apenas executa o fluxo.

O resultado é uma aplicação pequena, mas arquiteturalmente rica. Ela demonstra como um Event Bus em Delphi pode ser utilizado para reduzir acoplamento, organizar responsabilidades, executar múltiplos consumidores, trabalhar de forma assíncrona e manter a aplicação preparada para crescer sem transformar cada nova funcionalidade em uma alteração invasiva no código existente.

Quando NÃO utilizar Event Bus em Delphi

Embora um Event Bus em Delphi seja uma excelente solução para reduzir acoplamento e construir aplicações mais flexíveis, ele não deve ser encarado como uma resposta universal para todos os problemas arquiteturais. Como qualquer padrão de projeto, seu uso inadequado pode introduzir complexidade desnecessária, dificultar o entendimento do fluxo de execução e tornar a depuração mais trabalhosa.

O principal benefício do Event Bus é permitir que produtores e consumidores de eventos sejam completamente independentes entre si. Entretanto, essa característica também pode se transformar em um problema quando existe uma dependência explícita entre as operações executadas. Em cenários onde uma ação depende diretamente do resultado de outra, uma chamada direta entre componentes costuma ser mais simples, mais clara e mais fácil de manter.

Outro aspecto importante é que arquiteturas orientadas a eventos tornam o fluxo da aplicação menos evidente. Enquanto uma chamada tradicional permite identificar facilmente quem está executando determinada operação, em um Event Bus diversos assinantes podem reagir ao mesmo evento simultaneamente. Em sistemas pequenos ou com poucas integrações, essa camada adicional de abstração pode gerar mais complexidade do que benefícios.

Processos que exigem resposta imediata

Um Event Bus não é a melhor escolha quando uma operação precisa retornar um resultado imediatamente ao chamador. Como o padrão é baseado em publicação e assinatura, o produtor do evento não possui conhecimento sobre quem irá processá-lo nem sobre quando o processamento será concluído.

Imagine uma rotina responsável por validar credenciais de acesso:

Nesse caso, a autenticação depende diretamente do resultado produzido pelo serviço responsável pela validação. Utilizar um Event Bus apenas adicionaria complexidade sem trazer benefícios reais.

Operações transacionais

Outro cenário onde o uso de Event Bus deve ser avaliado com cautela são operações transacionais. Quando múltiplas etapas precisam ser executadas dentro da mesma transação de banco de dados, a comunicação direta entre componentes costuma ser mais adequada.

Considere o processo de emissão de uma nota fiscal:

Todas as operações fazem parte de uma única unidade de trabalho. Caso qualquer etapa falhe, a transação inteira deverá ser revertida. Nesse contexto, uma arquitetura orientada a eventos pode dificultar o controle transacional e aumentar significativamente a complexidade da solução.

Regras de negócio fortemente encadeadas

Quando existe uma sequência rígida de execução entre diferentes regras de negócio, o Event Bus também tende a não ser a melhor alternativa.

Por exemplo:

Cada etapa depende do sucesso da etapa anterior. Embora seja possível modelar esse fluxo utilizando eventos, a solução normalmente se torna mais difícil de compreender do que uma implementação procedural ou baseada em casos de uso explícitos.

Aplicações pequenas e de baixa complexidade

Um erro comum é introduzir Event Bus em aplicações que possuem poucos componentes ou poucas integrações. Nesses casos, o custo de manutenção da infraestrutura de eventos pode superar os benefícios obtidos com o desacoplamento.

Se uma funcionalidade possui apenas um consumidor conhecido e não existe expectativa de expansão futura, uma simples chamada direta pode ser a alternativa mais adequada.

Utilize Event Bus quando o desacoplamento for um requisito

O Event Bus brilha em cenários onde múltiplos componentes precisam reagir ao mesmo acontecimento sem conhecer uns aos outros. Processos como auditoria, geração de logs, envio de notificações, atualização de dashboards, integrações externas e publicação de eventos de domínio são excelentes candidatos para esse padrão.

A melhor arquitetura não é necessariamente a mais sofisticada, mas aquela que resolve o problema com o menor nível de complexidade possível. Utilizar um Event Bus em Delphi quando realmente existe a necessidade de desacoplamento permite aproveitar todos os benefícios da arquitetura orientada a eventos sem transformar a solução em algo excessivamente complexo ou difícil de manter.

Concluindo…

Ao longo deste artigo construímos um Event Bus em Delphi completo, partindo dos conceitos fundamentais de arquitetura orientada a eventos até uma implementação robusta utilizando interfaces, Generics, Fluent Interface, callbacks tipados e execução assíncrona com TTask. Mais do que apenas implementar um mecanismo de publicação e assinatura de eventos, desenvolvemos uma infraestrutura capaz de servir como base para aplicações corporativas modernas.

Durante a construção da solução vimos como o padrão Publish/Subscribe permite desacoplar componentes, eliminando dependências diretas entre módulos e favorecendo a reutilização de código. Também exploramos a criação de Domain Events, a integração com APIs RESTful, a execução simultânea de múltiplos consumidores e a implementação de mecanismos thread-safe para cenários concorrentes.

A combinação entre Event Bus, Fluent Interface e os princípios SOLID resulta em uma arquitetura flexível, extensível e de fácil manutenção. Novos comportamentos podem ser adicionados através da simples criação de assinantes, sem a necessidade de modificar componentes já existentes, reduzindo riscos e aumentando a capacidade de evolução da aplicação.

Embora o exemplo desenvolvido tenha sido apresentado em um contexto didático, os conceitos demonstrados são amplamente utilizados em sistemas distribuídos, arquiteturas baseadas em microsserviços, aplicações orientadas a domínio (DDD) e plataformas que demandam alta escalabilidade. O mesmo mecanismo pode ser utilizado para implementar auditorias, monitoramento, notificações, integração entre módulos e diversas outras funcionalidades sem aumentar o acoplamento do sistema.

Dominar a implementação de um Event Bus em Delphi representa um passo importante na construção de aplicações modernas e arquiteturas resilientes. Quando utilizado corretamente, esse padrão transforma a forma como os componentes interagem, promovendo um código mais limpo, organizado e preparado para crescer de maneira sustentável ao longo do tempo.

Ah, o código fonte deste artigo pode ser encontrado aqui.

Até a próxima!

Sobre Willian Tuttoilmondo 16 Artigos
Arquiteto de software com mais de 25 anos de experiência em desenvolvimento de software, especialista em desenvolvimento multicamadas utilizando Embarcadero Delphi e NodeJS para o back-end e o próprio Delphi para o front-end. Usuário Linux desde 1998, é evangelista PostgreSQL, banco de dados no qual é especialista. Ocupa hoje a posição de arquiteto de software na TOTVS, a maior empresa de tecnologia do Brasil, além de ser sócio fundador da LT Digital Labs, empresa especializada em desenvolvimento e treinamentos.

Seja o primeiro a comentar

Faça um comentário

Seu e-mail não será publicado.


*


Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.