Skip to content

EventBus

Christian R edited this page May 9, 2023 · 13 revisions

Here I present two alternatives for an Event Bus system. The first called "Hijacking the Logger" is easier to integrate into the existing architecture but it is not a architecturally clean solution. The second alternative called "More Indirection" is cleaner but more work.

No matter the alternative all event consuming objects should be able to register callbacks for specific event types. These callbacks should then be called when such an event is logged. The registration returns a handle that can be used for unregistration later.

sequenceDiagram
    activate EventConsumer
    EventConsumer->>+EventBus: register_callback(callback, event_type)
    EventBus-->>-EventConsumer: handle
    deactivate EventConsumer
Loading
sequenceDiagram
    activate EventConsumer
    EventConsumer->>+EventBus: unregister_callback(handle)
    EventBus-->>-EventConsumer:  
    deactivate EventConsumer
Loading

Hijacking the Logger

This solution requires minimal change in already existing code and is therefore very simple to integrate into the current architecture. The idea is to override the save method of LogEntry and to notify the EventBus there. The EventBus takes the LogEntry as argument and then looks for all registered callbacks associated with the entrys type and calls them passing the entry as argument again.

The downside is that it violates the single responsibility principle. The Logger is not responsible for managing an event system.

classDiagram
    class BaseModel {
        <<abstract>>
    }
    class LogEntry {
        <<abstract>>
        +save()
    }
    class Logger {
        +log(*args, **kwargs)
    }
    class Component {
    }
    class EventBus {
        <<singleton>>
        +callbacks: list[tuple[callable[[LogEntry], None], Type[LogEntry]]]
        +register_callback(callback: callable[[LogEntry], None], entry_type: Type[LogEntry]): UUID
        +unregister_callback(handle: UUID)
        +notify(entry: LogEntry)
    }
    class EventConsumer {
        +event_handle: UUID
        +on_event(entry: LogEntry)
    }

    Logger <-- Component: logs event
    LogEntry <-- Logger: creates
    BaseModel <|-- LogEntry
    EventBus <-- LogEntry: notifies
    EventBus <-- EventConsumer: (un)registers callbacks
    EventConsumer <-- EventBus: notifies
Loading
sequenceDiagram
    activate Component
    Component->>+Logger: log(*args, **kwargs)
    Logger->>+LogEntry: create(*args, **kwargs)
    LogEntry->>+LogEntry: save()
    LogEntry->>+EventBus: notify(self)
    EventBus->>+EventConsumer: on_event(entry)
    EventConsumer-->>-EventBus:      
    EventBus-->>-LogEntry:  
    LogEntry-->>-Logger:  
    deactivate LogEntry 
    Logger-->>-Component:  
    deactivate Component
Loading

More Indirection

This alternative abstracts the logger interface into ILogger and enables the usage of arbitrary many objects using this interface. Two of these objects are the already existing logger and also the EventBus. Objects inheriting from Component now communicate with the EventDistributor instead of the Logger using the same interface. The EventDistributor then forwards the call to Logger and EventBus. The EventBus then uses an Event having an EventType instead of the LogEntry. The Event holds all the data the LogEntry would hold. The rest of the mechanism works same as the first alternative presented above.

The disadvantage is that it is more work and one has to change more existing code. Especially the code concerning the interaction with the logger interface.

classDiagram
    class ILogger {
        <<abstract>>
        +log(*args, **kwargs)
    }
    class Logger {
    }
    class EventBus {
        <<singleton>>
        +callbacks: list[tuple[callable[[Event], None], EventType]]
        +register_callback(callback: callable[[Event], None], event_type: EventType): UUID
        +unregister_callback(handle: UUID)
    }
    class EventType {
        <<enum>>
    }
    class Event {
    }
    class EventConsumer {
        +event_handle: UUID
        +on_event(event: Event)
    }
    class EventDistributor {
    }
    class Component {
    }

    ILogger <|-- Logger
    ILogger <|-- EventBus
    ILogger <|-- EventDistributor
    EventDistributor "" o-- "n" ILogger
    Component "" *-- "1" EventDistributor
    Event "" *-- "1" EventType
    Event <-- EventBus: creates
    EventBus <-- EventConsumer: (un)registers callbacks
    EventConsumer <-- EventBus: notifies
Loading
sequenceDiagram
    activate Component
    Component->>+EventDistributor: log(*args, **kwargs)
    EventDistributor->>+Logger: log(*args, **kwargs)
    Logger-->>-EventDistributor:  
    EventDistributor->>+EventBus: log(*args, **kwargs)
    EventBus->>+Event: __init__(*args, **kwargs)
    Event-->>-EventBus: self
    EventBus->>+EventConsumer: on_event(event)
    EventConsumer-->>-EventBus:  
    EventBus-->>-EventDistributor:  
    EventDistributor-->>-Component:  
    deactivate Component
Loading
Clone this wiki locally