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
Messageobjects from the business-logic which handles them (MessageHandleror anyCallabletaking 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
chainfunction.It allows broadcasting a single message to multiple handlers (different from sequential chaining).
It uses a centralized
MessageBusto shuttle messages about (you may want to create your own instance with the Singleton module to make it a singleton object).Each
MessageHandlerhas a handle to aMessageBusinstance 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?')