I'm looking for Wikpedia-style references to designs and implementations of lightweight publish-subscribe mechanisms that actually work. I will update the question according to the answers and comments, and to my own research.
I researched my books and the Web for work done in Python and Delphi for publish/subscribe, and wasn't happy with the results. The designs relied on function signatures or bitmaps or slots for filtering messages or deciding what should be delivered to who, and were either too restrictive (bound to a messaging server), or too promiscuous (everyone can subscribe to anything).
I don't want to write my own. I want to find something that's already well-designed, debated, and field-proven.
Today I implemented a design in Delphi Pascal (because Delphi was what I needed first). Dispatching on argument type, as this API does, is not an original idea (it is explained the Design Patterns Visitor
pattern), and I think I have seen something like this before (but I don't remember where; Taligent?). The core of it is that subscription, filtering, and dispatching is over the type system.
unit JalSignals;
// A publish/subscribe mechanism.
// 1. Signal payloads are objects, and their class is their signal type.
// 2. Free is called on the payloads after they have been delivered.
// 3. Members subscribe by providing a callback method (of object).
// 4. Members may subscribe with the same method to different types of signals.
// 5. A member subscribes to a type, which means that all signals
// with payloads of that class or any of its subclasses will be delivered
// to the callback, with one important exception
// 6. A forum breaks the general class hierarchy into independent branches.
// A signal will not be delivered to members subscribed to classes that
// are not in the branch.
// 7. This is a GPL v3 design.
interface
uses
SysUtils;
type
TSignal = TObject;
TSignalType = TClass;
TSignalAction = (soGo, soStop);
TCallback = function(signal :TSignal) :TSignalAction of object;
procedure signal(payload: TSignal);
procedure subscribe( callback :TCallback; atype :TSignalType);
procedure unsubscribe(callback :TCallback; atype :TSignalType = nil); overload;
procedure unsubscribe(obj :TObject; atype :TSignalType = nil); overload;
procedure openForum( atype :TSignalType);
procedure closeForum(atype :TSignalType);
The "callbacks" in the above are like bound methods in Python.
The complete source code for the Delphi implementation is here:
This is the implementation in Python. I changed the key names because signal and message are already too overloaded. Unlike in the Delphi implementation, the results, including exceptions, are collected and returned to the signaler in a list.
"""
A publish/subscribe mechanism.
1. Signal payloads are objects, and their class is their signal type.
2. Free is called on the payloads after they have been delivered.
3. Members subscribe by providing a callback method (of object).
4. Members may subscribe with the same method to different types of signals.
5. A member subscribes to a type, which means that all signals
with payloads of that class or any of its subclasses will be delivered
to the callback, with one important exception:
6. A forum breaks the general class hierarchy into independent branches.
A signal will not be delivered to members subscribed to classes that
are not in the branch.
"""
__all__ = ['open_forum', 'close_forum', 'announce',
'subscribe', 'unsubscribe'
]
def _is_type(atype):
return issubclass(atype, object)
class Sub(object):
def __init__(self, callback, atype):
assert callable(callback)
assert issubclass(atype, object)
self.atype = atype
self.callback = callback
__forums = set()
__subscriptions = []
def open_forum(forum):
assert issubclass(forum, object)
__forums.add(forum)
def close_forum(forum):
__forums.remove(forum)
def subscribe(callback, atype):
__subscriptions.append(Sub(callback, atype))
def unsubscribe(callback, atype=None):
for i, sub in enumerate(__subscriptions):
if sub.callback is not callback:
continue
if atype is None or issubclass(sub.atype, atype):
del __subscriptions[i]
def _boundary(atype):
assert _is_type(atype)
lower = object
for f in __forums:
if (issubclass(atype, f)
and issubclass(f, lower)):
lower = f
return lower
def _receivers(news):
bound = _boundary(type(news))
for sub in __subscriptions:
if not isinstance(news, sub.atype):
continue
if not issubclass(sub.atype, bound):
continue
yield sub
def announce(news):
replies = []
for sub in _receivers(news):
try:
reply = sub.callback(news)
replies.append(reply)
except Exception as e:
replies.append(e)
return replies
if __name__ == '__main__':
i = 0
class A(object):
def __init__(self):
global i
self.msg = type(self).__name__ + str(i)
i += 1
class B(A): pass
class C(B): pass
assert _is_type(A)
assert _is_type(B)
assert _is_type(C)
assert issubclass(B, A)
assert issubclass(C, B)
def makeHandler(atype):
def handler(s):
assert isinstance(s, atype)
return 'handler' + atype.__name__ + ' got ' + s.msg
return handler
handleA = makeHandler(A)
handleB = makeHandler(B)
handleC = makeHandler(C)
def failer(s):
raise Exception, 'failed on' + s.msg
assert callable(handleA) and callable(handleB) and callable(handleC)
subscribe(handleA, A)
subscribe(handleB, B)
subscribe(handleC, C)
subscribe(failer, A)
assert _boundary(A) is object
assert _boundary(B) is object
assert _boundary(C) is object
print announce(A())
print announce(B())
print announce(C())
print
open_forum(B)
assert _boundary(A) is object
assert _boundary(B) is B
assert _boundary(C) is B
assert issubclass(B, B)
print announce(A())
print announce(B())
print announce(C())
print
close_forum(B)
print announce(A())
print announce(B())
print announce(C())
These are the reasons for my search:
- I've been going through several thousand lines of Delphi code that I have to maintain. They use the Observer pattern for MVC decoupling, but everything is still very coupled because the dependencies between observers and subjects are too explicit.
- I have been learning PyQt4, and it would kill me if I have to click-click-click in Qt4Designer for every event I want arriving to a meaningful destination.
- Yet on another personal-data application, I need to abstract event passing and handling because persistence and UI will vary by platform, and must be totally independent.
References
Found by self and others should go here
- PybubSub uses strings for topycs, and method signatures (the first signal defines the signature).
- An article in the blog of FinalBuilder reports that they have successfully used a system with integers structures as payloads, as messages, and integer masks for filtering.
- PyDispatcher has minimal documentation.
- D-Bus has been adopted by the Gnome and KDE projects, among others. A Python binding is available.