Splitting architecture apart via signals

Signals can be used to create code that's not coupled. It's a really nice solution to split code apart and makes things much more readable.

Let's take an example: user creation. When a user is created we might want to do following things:

  • send off a welcome email
  • notify analytics
  • notify some other service that the user is created

Generally you solve this in following manner:

import analytics
import other_service
import notifications

def create_user(...):
    ...
    analytics.user_created(user)
    other_service.user_created(user)
    notifications.send_welcome_email(user)

The main problem with this approach is that you'll create a high coupling between your modules and eventually you'll meet the glory of circular imports.

A better solution, using signals, is to simply fire off a user_created signal - firing off a signal will call all the listeners of the signal. E.g.:

import signals

def create_user(...):
    ...
    signals.send_signal('user_created', user=user)

This approach uncouples code, solves the problem with circular imports and improves readability. It's nize :-)

The complex implementations of signals

In Python the signals implementations are fairly complex (for me it seems like people have made the problem too hard), here are some signal implementations:

Both these use weak references and their API is fairly complex (they can do lots of stuff - that's unneeded, at least in my applications).

I have made my own little signals implementation, which solves my problems:

SIGNALS = {}

def connect(signal_key, callback):
    """Connect `callback` to signal's callback sequence.
    """
    if signal_key in SIGNALS:
        SIGNALS[signal_key].append( callback )
    else:
        SIGNALS[signal_key] = [callback]


def send_signal(signal_key, *args, **kwargs):
    """Sending a signal will iterate over a signal's callback.
    """
    if not signal_key in SIGNALS:
        raise Exception('No handlers for signal: %s' % signal_key)

    for callback in SIGNALS[signal_key]:
        callback(*args, **kwargs)

Example:

def reciver_1(x): print "hello"
def reciver_2(x): print "world"

signals_new.connect("hello", reciver_1)
signals_new.connect("hello", reciver_2)

signals_new.send_signal("hello", x=None)

I only add signals at the bootstrap, I don't need to remove signals and I don't need to have error handling. This makes the problem domain _very_ easy to solve as you can see.

Code · Code improvement · Code rewrite · Design · Python · Tips 25. Jan 2009
1 comment so far

Zope is a pretty complex system, but is implements events nearly as simple as you. The basic implementation (zope.event) is just:

subscribers = []

def notify(event):
for subscriber in subscribers:
subscriber(event)

subscribers is a list of dispatch functions. The dispatch functions can be as simple as you need them. Usually they look up an adapter for an interface (two core concepts in Zope3), which is a 4 liner. But there could be a simple key->receiver dispatcher.

With this implementation it's easy to write an dispatcher for debugging or logging, e.g. in doctests. But as your signals seem to be for a special purpose, keeping it in a more compact implementation seems to be a good choice.

Post a comment
Commenting on this post has expired.
© 2000-2009 amix. Powered by Skeletonz.