Kennisbank

EventBus pattern

Introductie

Tijdens de ontwikkeling van Codolex merkten we dat het verbinden van verschillende formulieren en componenten via callback events zorgde voor sterk gekoppelde code. Een voorbeeld: wanneer een gebruiker een project opslaat, moeten meerdere onderdelen van de applicatie reageren. De lijst met recente items moet worden bijgewerkt, de backup manager moet in actie komen en de analytics tracker moet de gebeurtenis vastleggen. Met overal callback events werd dit al snel moeilijk onderhoudbaar. Om dit probleem op te lossen, hebben we het EventBus pattern geïmplementeerd.

De EventBus biedt een centrale plek om events te distribueren. Wanneer een proces een event naar de EventBus stuurt, distribueert deze het automatisch naar alle componenten die zich hebben geabonneerd op dat eventtype. Dit is een implementatie van het Observer pattern gecombineerd met het Mediator pattern om code te ontkoppelen.

Implementation

Bij het versturen van events moeten we kunnen identificeren om wat voor soort event het gaat. We gebruiken hiervoor een enumeratie om het eventtype te herkennen. De waarden zijn afhankelijk van je specifieke implementatie.

{$SCOPEDENUMS ON}
// Indicates which event is fired
TEventType = (New, Update, Open, Close, Cancel, Validation);
TEventTypes = set of TEventType;
{$SCOPEDENUMS OFF}

Vervolgens definiëren we de EventBus. We definiëren deze als generic, zodat we bij het gebruik direct met de juiste types werken. De EventBus heeft een procedure nodig waarmee observers zich kunnen abonneren en een procedure waarmee een event kan worden afgevuurd.

// Procedure definition to receive events
TEventAction<I: ISerializableObject> = 
       reference to procedure(const EventType: TEventType; const Data: I);

// Procedure to unsubscribe
TUnsubscribeEvent = reference to procedure;

// Base (generic) event bus interface
IEventBus<I: ISerializableObject> = interface

  function Subscribe(const OnEvent: TEventAction<I>): TUnsubscribeEvent; overload;
  // Overloaded variant to register filtered subscriptions
  function Subscribe(const OnEvent: TEventAction<I>; 
                     const Filter: TEventTypes): TUnsubscribeEvent; overload;

  procedure Post(const EventType: TEventType; const Data: I);
end;

Met de basis van de EventBus gedefinieerd, kunnen we event buses maken voor specifieke types:

  // Specific interfaces for different types of event buses
  IProjectEventBus = interface(IEventBus<IProject>)
  end;

  IDataEntityEventBus = interface(IEventBus<IDataEntity>)
  end;

  IFlowEventBus = interface(IEventBus<IFlow>)
  end;

De master event bus

We willen een eenvoudige en transparante manier om de event bus structuur te gebruiken. Om dit te bereiken, gebruiken we een master event bus die een collectie van event buses bevat. Door deze master bus een singleton te maken in je applicatie, is er een uniforme manier om toegang te krijgen tot alle onderliggende event buses.

  // Master event bus to expose the available types of event buses (should be a singleton)
  IMasterEventBus = interface
    function Project: IProjectEventBus;
    function Flow: IFlowEventBus;
    function DataEntity: IDataEntityEventBus;
  end;

Abonnementen beheren

Wanneer iemand zich abonneert op een event bus, geeft de procedure een TUnsubscribeEvent terug die gebruikt kan worden om het abonnement op te zeggen. Als je je abonneert op meerdere event buses, wil je deze allemaal in één keer kunnen opruimen. Hiervoor definiëren we een aparte interface die de unsubscribe events bijhoudt en opruimt via een enkele functie (of wanneer het object automatisch wordt vernietigd).

  // Interface to store unsubscribe events and unsubscribe all at once
  IUnsubscriber = interface(IList<TUnsubscribeEvent>)
    procedure Unsubscribe;
  end;

Voorbeeld gebruik

Nu we alles op zijn plaats hebben, werkt het in de praktijk als volgt:

// Somewhere in the constructor: declare the unsubscriber
FUnsubscriber := GlobalContainer.Resolve<IUnsubscriber>;

// Declare and get the master event bus
var EventBus := GlobalContainer.Resolve<IMasterEventBus>;

// Subscribe for specific flow events
var UnsubscribeFlow := EventBus.Flow.Subscribe(
                        procedure(const EventType: TEventType; const Data: IFlow)
                        begin
                          // Handle the flow event
                        end,
                        [TEventType.Open, TEventType.Close]);

FUnsubscriber.Add(UnsubscribeFlow);

// Subscribe for all project events
FUnsubscriber.Add(
  EventBus.Project.Subscribe(
    procedure(const EventType: TEventType; const Data: IProject)
    begin
      // Handle the project event
    end));

// Post an event about a data entity
var MyDataEntity: IDataEntity;
EventBus.DataEntity.Post(TEventType.Update, MyDataEntity);

// Somewhere in the destructor: clean up all subscriptions
FUnsubscriber.Unsubscribe;

De filter parameter gebruikt de set-syntax van Delphi om aan te geven welke eventtypes je wilt ontvangen. In het bovenstaande voorbeeld abonneren we ons alleen op Open en Close events voor flows, terwijl we ons voor projecten abonneren op alle eventtypes.

Voordelen in de praktijk

Het EventBus pattern heeft ons geholpen om Codolex onderhoudbaar te houden naarmate het groeit. Voordat we dit pattern implementeerden, hadden we callback events verspreid door de hele codebase. Het toevoegen van een nieuwe feature die moest reageren op projectwijzigingen betekende dat we meerdere bestanden moesten aanpassen en alle verbindingen handmatig moesten bijhouden.

Met de EventBus is het toevoegen van nieuwe functionaliteit eenvoudig. Toen we bijvoorbeeld analytics tracking toevoegden, abonneerden we ons simpelweg op de relevante events zonder bestaande code aan te raken. Hetzelfde gold toen we de backup manager en de lijst met recente items implementeerden. Elk component registreert zijn interesse in specifieke events en behandelt deze onafhankelijk.

Deze aanpak maakt testen ook eenvoudiger. We kunnen componenten geïsoleerd testen en verifiëren dat ze de juiste events posten, zonder dat de hele applicatiestructuur aanwezig hoeft te zijn.

Belangrijke overwegingen

Hoewel het EventBus pattern krachtig is, zijn er een aantal zaken om rekening mee te houden:

  • Memory management: Zorg er altijd voor dat je het abonnement opzegt wanneer je component wordt vernietigd. Het gebruik van de IUnsubscriber interface met interface reference counting regelt dit automatisch.
  • Thread safety: Deze implementatie is ontworpen voor single-threaded gebruik. Als je events vanuit meerdere threads moet posten, moet je synchronisatiemechanismen toevoegen.
  • Error handling: Als een subscriber een exception genereert tijdens het verwerken van een event, kan dit voorkomen dat andere subscribers het event ontvangen. Overweeg try-except blokken toe te voegen in de EventBus implementatie als dit een zorg is voor je applicatie.

Meer leren over Delphi patterns

Bij GDK Software passen we moderne design patterns zoals de EventBus toe in al onze Delphi-projecten. Dit helpt ons de codekwaliteit te behouden en maakt het gemakkelijker om nieuwe features toe te voegen naarmate applicaties groeien. Wil je meer leren over het schrijven van schone, onderhoudbare Delphi-code? Bekijk onze artikelen over SOLID principles in Delphi en Dependency Injection in Delphi.

Werk je met legacy Delphi-applicaties die moeilijk te onderhouden zijn? Wij kunnen helpen. Contact ons om te bespreken hoe we je Delphi codebase kunnen moderniseren.

Contact

Laat ons helpen jouw ambities concreet te maken.