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
© Amir Salihefendic. Powered by Skeletonz.