2

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:

  1. 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.
  2. 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.
  3. 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.
Community
  • 1
  • 1
Apalala
  • 9,017
  • 3
  • 30
  • 48
  • 2
    You haven't specified what the actual requirements of your PubSub system are. There are many pre-built ones - can you describe why none of them suit? – Nick Johnson Jan 18 '11 at 05:51
  • @Nick Johnson. I thought that the requirements were in. The number one was hard references to already published work. The complaints are also explicit: bureaucracy and/or promiscuity. It should be simpler to set up a conference center (hint for a new name) in which every one has access to everything he knows the directions to. – Apalala Jan 18 '11 at 06:34
  • 2
    "References to already published work" is not a requirement of a system - it's a requirement of an answer. What do you want your pubsub system to _do_? Why don't any of the existing solutions suit? – Nick Johnson Jan 18 '11 at 22:32
  • Nick. You're probably right about my question being vague, as there's not been much interest in it so far. I'll start to work with what's available, and edit the question with what I find wrong or missing. – Apalala Jan 19 '11 at 00:53
  • What doesn't the code included with the Final Builder article above do that you need it to do? – Warren P Jan 21 '11 at 20:21
  • The Final Builder code uses integers and integer bit-masks for topics and topic filtering, and untyped memory for messages, and that is a disaster waiting to happen. There's no way to keep certain messages within the context that knows their meaning, code can accidentally send a message with an unexpected payload, and several other simple mistakes can have large and difficult to debug repercusions. – Apalala Jan 21 '11 at 22:30

2 Answers2

2

You could also try DDS. Data Distribution Service is a full standard for communication patterns using Publish / Subscribe semantics.

Bert F.
  • 31
  • 1
2

You should try Enterprise Integration Patterns it gives publish-subscribe a very detailed treatment though it is centered on inter-process message passing.

Kenneth Cochran
  • 11,954
  • 3
  • 52
  • 117
  • Enterprise message buses are related to what I want, but much more complex, and usually much more complicated. For one, I don't care about guaranteed delivery, but do care about incorrect delivery. – Apalala Jan 21 '11 at 22:33