-
Notifications
You must be signed in to change notification settings - Fork 1
EventBus
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
sequenceDiagram
activate EventConsumer
EventConsumer->>+EventBus: unregister_callback(handle)
EventBus-->>-EventConsumer:
deactivate EventConsumer
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
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
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
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
This version addresses the issue that EventBus
and EventDistributor
are more or less the same. The Logger
should be also an event consumer. The log
method of EventBus
stands for all methods the Logger
currently has.
classDiagram
class Component {
}
class EventBus {
+callbacks: list[tuple[callable[[Event], None], EventType]]
+register_callback(callback: callable[[Event], None], event_type: EventType): UUID
+unregister_callback(handle: UUID)
+log(*args, **kwargs)
}
class EventConsumer {
+event_handle: UUID
+on_event(event: Event)
}
class Event {
+arguments: dict[str, any]
}
class EventType {
<<enum>>
}
class Logger {
}
Component "" *-- "1" EventBus: event_bus
EventBus --> Event: creates
Event "" *-- "1" EventType
EventBus --> EventConsumer: notifies
EventConsumer --> EventBus: (un)registers callbacks
Logger --> EventConsumer: is
sequenceDiagram
activate Component
Component->>+EventBus: log(*args, **kwargs)
EventBus->>+Event: __init__(*args, **kwargs)
Event-->>-EventBus:
EventBus->>+EventConsumer: on_event(event)
EventConsumer-->-EventBus:
EventBus-->-Component:
deactivate Component
Emitting of events is simple and replaces calling logger methods. So instead of for example calling self.logger.spawn_train(tick, train_id)
you would now call self.event_bus.spawn_train(tick, train_id)
. All classes inheriting from Component
therefore hold a reference to event_bus
instead of a logger
reference. When calling methods of event_bus
they are automatically to the logger
if the logger
has subscribed to them.
To subscribe to events you have to register a callback and an associated EventType
at the EventBus
. You therefore call in for example __init__
of your Component
the method self.event_bus.register_callback(callback, EventType.TRAIN_SPAWN)
. callback
here is a callable taking an Event
as argument. The event has the attributes event_type
and arguments
where arguments
holds a dictionary containing all arguments given to the method call to the EventBus
that emitted the event.
When registering a callback you get back a handle
. You can use this handle to unregister the callback by calling self.event_bus.unregister(handle)
.
To define a new event you have to add a new entry to EventBus.EVENT_METHODS
in src.event_bus.event_bus.py
. The key is the name of the method one has to call on the EventBus
to emit the new event. The value is a tuple containing a list of strings and the EventType associated with the event. The list contains the names of all arguments one needs to pass to the mentioned method call.
All Components that need to react to the new Event (for example the Logger
) have to subscribe to it as described above.