Messaging Framework¶
The frequent.messaging module provides a skeleton for implementing your own messaging framework. Useful for applications which implement the Repository and/or the Unit of Work patterns as a command/command-handler system. The advantages of using this kind of system are:
It decouples
Message
objects from the business-logic which handles them (MessageHandler
or anyCallable
taking a message object as its first argument).It allows chaining of handlers together in order to call subsequent handlers on a message in a sequential way via a simple
chain
function.It allows broadcasting a single message to multiple handlers (different from sequential chaining).
It uses a centralized
MessageBus
to shuttle messages about (you may want to create your own instance with the Singleton module to make it a singleton object).Each
MessageHandler
has a handle to aMessageBus
instance it can use to send any subsequent messages (potentially to different buses than the bus which transmitted the original message to the handler).
Usage¶
The frequent.messaging
module provides all the pieces needed to create
custom Message
and
MessageHandler
classes as well as
the components needed to facilitate message passing via the
MessageBus
. In the examples below
we’ll create a very basic messaging framework to deliver messages to the
appropriate user’s mailbox to show some of these features.
Creating Message Classes¶
To start you’ll need to create your own Message
classes, which can be done
using the @message
decorator:
from frequent.messaging import message
@message
class MyMessage:
sender: str
recipient: str
text: str
The decorator will automatically add the Message
class to the base
classes (__bases__
) of the MyMessage
class and cast the class as a
dataclass via the new
(as of Python 3.7) standard library.
Note
Each instance of Message
has an auto-generated id
attribute (a
UUID
) generated using uuid.uuid1()
from the standard library.
Creating Message Handlers¶
Now let’s create a message handler for sending messages by subclassing the
MessageHandler
abstract base class:
from frequent.messaging import MessageHandler
class MyMessageHandler(MessageHandler):
def __init__(self, bus, mailboxes):
self._mailboxes = mailboxes
return super().__init__(bus)
def handle(self, msg, successor=None):
self._mailboxes[msg.recipient].append(msg)
return
We can create the instance now with:
>>> bus = MessageBus()
>>> mailboxes = []
>>> my_message_handler = MyMessageHandler(bus, mailboxes)
Note
Handlers can also be functions which take the first argument as the message
object and an (optional) keyword-argument successor
for the next
handler to call (if chaining handlers together). The advantage of the
MessageHandler
object is it’s reference to a MessageBus
which
it can use to transmit additional messages (if needed).
Chaining Handlers Together¶
Suppose we want to first log a message prior to handling it, we can create a function to do that which will then call the next function in the chain:
from frequent.messaging import chain
def log_message_handler(msg, successor):
print(f"{msg.sender}->{msg.recipient}: '{msg.text}'")
return successor(msg)
Now we chain this one together with the previous MyMessageHandler
:
>>> chained_handler = chain(log_message_handler, my_message_handler)
>>> chained_handler(MyMessage('Doug', 'Liz', 'Hello!'))
Doug->Liz: 'Hello!'
Configuring the MessageBus¶
We can now create and configure the MessageBus
and send messages to the
appropriate handler(s). First let’s setup a helper object to store messages a
user has received (the mailboxes
object - a simple dict
which stores
list`s of ``MyMessage`
objects using the message recipient’s name as the
key).
>>> mailboxes = defaultdict(list)
Then we can create the MessageBus
and the MyMessageHandler
and lastly, map the MyMessage
Message
type to it in the
MessageBus
’s registry (an instance of HandlerRegistry
):
>>> msg_bus = MessageBus()
>>> msg_handler = SendMessageHandler(message_bus, mailboxes)
>>> message_bus.registry.add(MyMessage, mymsg_handler)
Using the New Framework¶
Now that the MyMessage
class is mapped to our instance of the
MyMessageHandler
we can pass messages to the msg_bus
instance to have
them stored in the appropriate user’s mailbox (via the msg_handler
):
>>> msg_a = MyMessage('Doug', 'Liz', 'How are you?')
>>> msg_bus(msg_a)
>>> rcvd = mailboxes['Liz'].pop()
>>> rcvd
MyMessage(sender='Doug', recipient='Liz', text='How are you?')
>>> msg_b = MyMessage(rcvd.recipient, rcvd.sender, "I'm great, how are you?")
>>> msg_bus(msg_b)
>>> mailboxes['Doug'].pop()
MyMessage(sender='Liz', recipient='Doug', text='I'm great, how are you?')