When developing Codolex, we noticed that connecting different forms and components with callback events was creating tightly coupled code. For example, when a user saves a project, multiple parts of the application need to respond: the recent items list updates, the backup manager kicks in, and the analytics tracker logs the event. With callback events everywhere, this quickly became difficult to maintain. To solve this problem, we implemented the EventBus pattern.
The EventBus provides a central place to distribute events. When a process sends an event to the EventBus, it automatically distributes it to all components that have subscribed to that event type. This is an implementation of the Observer pattern combined with the Mediator pattern to decouple code.
When sending events, we need to identify what kind of event it is. We use an enumeration to recognize the event type. The values depend on your specific implementation.
{$SCOPEDENUMS ON} // Indicates which event is fired TEventType = (New, Update, Open, Close, Cancel, Validation); TEventTypes = set of TEventType; {$SCOPEDENUMS OFF}
Next, we define the EventBus. We define it as generic, so that when we use it, we’re working directly with the right types. The EventBus needs a procedure that allows observers to subscribe and a procedure that allows an event to be fired.
// 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;
With the basic EventBus defined, we can define event buses for specific types:
// Specific interfaces for different types of event buses IProjectEventBus = interface(IEventBus<IProject>) end; IDataEntityEventBus = interface(IEventBus<IDataEntity>) end; IFlowEventBus = interface(IEventBus<IFlow>) end;
We want a simple and transparent way to use the event bus structure. To achieve this, we use a master event bus that contains a collection of event buses. By making this master bus a singleton in your application, there is a unified way to access all the underlying 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;
When someone subscribes to an event bus, the procedure returns a TUnsubscribeEvent that can be used to handle unsubscribing. If you subscribe to multiple event buses, you want to be able to clean them all up at once. For that, we define a separate interface that keeps track of the unsubscribe events and cleans them up via a single function (or when the object is automatically destroyed).
// Interface to store unsubscribe events and unsubscribe all at once IUnsubscriber = interface(IList<TUnsubscribeEvent>) procedure Unsubscribe; end;
Now that we have everything in place, here’s how this works in practice:
// 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;
The filter parameter uses Delphi’s set syntax to specify which event types you want to receive. In the example above, we only subscribe to Open and Close events for flows, while subscribing to all event types for projects.
The EventBus pattern has helped us keep Codolex maintainable as it grows. Before implementing this pattern, we had callback events scattered throughout the codebase. Adding a new feature that needed to respond to project changes meant modifying multiple files and keeping track of all the connections manually.
With the EventBus, adding new functionality is straightforward. For example, when we added analytics tracking, we simply subscribed to the relevant events without touching any existing code. The same applied when we implemented the backup manager and recent items list. Each component registers its interest in specific events and handles them independently.
This approach also makes testing easier. We can test components in isolation and verify that they post the correct events, without needing the entire application structure in place.
While the EventBus pattern is powerful, there are a few things to keep in mind:
IUnsubscriber interface with interface reference counting handles this automatically.At GDK Software, we apply modern design patterns like the EventBus across all our Delphi projects. This helps us maintain code quality and makes it easier to add new features as applications grow. Want to learn more about writing clean, maintainable Delphi code? Check out our articles on SOLID principles in Delphi and Dependency Injection in Delphi.
Are you working with legacy Delphi applications that have become difficult to maintain? We can help. Contact us to discuss how we can modernize your Delphi codebase.
Contact
GDK Software NL
(+31) 78 750 1661GDK Software UK
(+44) 20 3355 4470