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 any Callable 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 a MessageBus 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?')