Signal/Slot design pattern¶
Introduction¶
Signal/Slot is a pattern that allows loose coupling various components of a software without having to introduce boilerplate code. Loose coupling of components allows better modularity in software code which has the nice side effect of making it easier to test because less dependencies means less mocking and monkey patching.
Signal/Slot is a widely used pattern, many frameworks have it built-in
including Django, Qt and probably many others. If you have a standalone project
then you probably don’t want to add a big dependency like PyQt or Django just
for a Signal/Slot framework. There are a couple of standalone libraries which
allow to acheive a similar result, like Circuits or PyPubSub, which has way
more features than signalslots
, like messaging over the network and is a
quite complicated and has weird (non-PyPi hosted) dependencies and is not PEP8
compliant ...
signalslot
has the vocation of being a light and simple implementation of
the well known Signal/Slot design pattern provided as a classic quality Python
package.
Tight coupling¶
Consider such a code in your_client.py
:
import your_service
import your_dirty_hack # WTH is that doing here ? huh ?
class YourClient(object):
def something_happens(self, some_argument):
your_service.something_happens(some_argument)
your_dirty_hack.something_happens(some_argument)
The problem with that code is that it ties your_client
with
your_service
and your_dirty_hack
which you really didn’t want to put
there, but had to, “until you find a better place for it”.
Tight coupling makes code harder to test because it takes more mocking and harder to maintain because it has more dependencies.
An improvement would be to acheive the same while keeping components loosely coupled.
Observer pattern¶
You could implement an Observer pattern in YourClient
by adding
boilerplate code:
class YourClient(object):
def __init__(self):
self.observers = []
def register_observer(self, observer):
self.observers.append(observer)
def something_happens(self, some_argument):
for observer in self.observers:
observer.something_happens(some_argument)
This implementation is a bit dumb, it doesn’t check the compatibility of observers for example, also it’s additionnal code you’d have to test, and it’s “boilerplate”.
This would work if you have control on instanciation of YourClient
, ie.:
your_client = YourClient()
your_client.register_observer(your_service)
your_client.register_observer(your_dirty_hack)
If YourClient
is used by a framework with IoC then it might become
harder:
service = some_framework.Service.create(
client='your_client.YourClient')
service._client.register_observer(your_service)
service._client.register_observer(your_dirty_hack)
In this example, we’re accessing a private python variable _client
and
that’s never very good because it’s not safe against forward compatibility.
With Signal/Slot¶
Using the Signal/Slot pattern, the same result could be achieved with total component decoupling. It would organise as such:
YourClient
defines asomething_happens
signal,your_service
connects its own callback to thesomething_happens
,- so does
your_dirty_hack
, YourClient.something_happens()
“emits” a signal, which in turn calls all connected callbacks.
Note that a connected callback is called a “slot” in the “Signal/Slot” pattern.
See Usage for example code.