Created
January 21, 2009 19:37
-
-
Save seanh/50140 to your computer and use it in GitHub Desktop.
My implementation of Panda3D's messager pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
My implementation of Panda3D's messager pattern | |
This is my implementation of the messager pattern used by Panda3D for event | |
handling. It's a really nice idea but I think Panda's version is implemented | |
weirdly, it uses the subscribe objects as keys in the dictionary. That means | |
that if you want more than one object to subscribe to the same message you have | |
to subclass DirectObject and use self.accept(...), can't just call the messager | |
directly or you'll get a different behaviour, it means that a single object | |
instance can't subscribe two different functions to the same message, and it | |
means that your objects have to be hashable (!) because the messager uses them | |
as keys in a dictionary. It seems to me that it would be much simpler if the | |
messager's dictionary just mapped message names to function objects, so that's | |
what I did. | |
Use this pattern when you have a lot of objects spread throughout the class | |
hierarchy that need to communicate with each other. It's a way of implementing | |
one-to-many or many-to-many communication, in which an object can send a message | |
to many receivers without needing to know who those receivers are, or how many | |
receivers there are, or even if there are any receivers (although an object can | |
potentially check all these things if it wants to). The singleton messager | |
object receives messages from broadcaster objects and forwards them to receiver | |
objects. Setup a single, simple, generic, system- wide messaging system, instead | |
of creating different systems for each different type of message or messaging | |
relation. | |
The disadvantage of just implementing this pattern once and using it everywhere | |
is that with lots of senders and receivers it might become difficult to | |
understand and debug message-based behaviour. Particularly if the order in which | |
messages are sent and received becomes important, when you send a message with | |
this pattern there's no way of knowing in what order the receiver objects will | |
receive the message, and therefore know way of knowing in what order their | |
responses will be executed, and those responses may include sending more | |
messages. You can implement some ordering relations by having an object receive | |
a message, deal with it, then send a different message to other recievers, but I | |
wouldn't want to overuse that. | |
Also you have to be careful to avoid clashes in message names. | |
The GoF Mediator and Observer patterns are similar in that they enable a mode of | |
communication between objects in which the sender object broadcasts a message | |
without needing to know all of the receiver objects, and the receiver objects | |
can receive messages without being tightly coupled to the sender object(s). | |
In the GoF Mediator pattern, the Mediator object is more than just a messager | |
that passes messages between other objects and leaves the behaviour up to the | |
others. The mediator actually encapsulates the interaction behaviour between | |
objects, deciding which methods to call on which objects each time it receives a | |
notification from an object. Mediator centralises control of how objects | |
cooperate in the mediator class, whereas the Messager pattern leaves this | |
control distributed throughout the objects themselves. | |
In the GoF observer pattern a one-to-many dependency is setup, there is no | |
separate Messager object but rather the sender object itself maintains the list | |
of reciever object and notifies them of new messages. With its separate messager | |
object the Messager pattern enables many-to-many dependencies as well as one-to- | |
many. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
messager.py -- a simple message-passing pattern for one-many or many-many | |
dependencies. Useful for event notifications, for example. | |
To send a message use the singleton messager instance: | |
from messager import messager | |
messager.send('message name',argument) | |
You can pass a single argument with a message, and this argument can be anything | |
you like. For example, event objects that simply hold a number of attributes can | |
be constrcuted and passed as arguments with messages. | |
Messager maintains a mapping of message names to lists of functions (and their | |
arguments). When a message is sent, all of the functions subscribed to that | |
message are called and passed the argument given when the function was | |
subscribed followed by argument given when the message was sent. To subscribe a | |
function you must subclass Receiver and call the accept(...) or acceptOnce(...) | |
methods: | |
self.accept('message name',function,argument) | |
self.acceptOnce('message name',function,argument) | |
You don't need to call Receiver.__init__() when you subclass Receiver, it has no | |
__init__. Receiver works by maintaining a list of the message subscriptions you | |
have made. | |
It is up to you to make sure that functions that accept messages take the right | |
number of arguments, 0, 1 or 2 depending on whether the accept(...) and | |
send(...) methods were called with an argument or not. | |
To unsubscribe a function from a message name use ignore: | |
# Unsubscribe a particular function from a particular message name. | |
self.ignore('message name',function) | |
# Unsubscribe all functions that this object has subscribed to a particular | |
# message name. | |
self.ignore('message name') | |
# Unsubscribe all functions that this object has subscribed to any message | |
# name. | |
self.ignoreAll() | |
You can unsubscribe all functions from all Receiver objects with: | |
messager.clear() | |
If you do `messager.verbose = True` the messager will print whenever it | |
receives a message or subscription, and if you do `print messager` the messager | |
will print out a list of all the registered message names and their subscribers. | |
One last thing to be aware of is that messager keeps references to (functions | |
of) all objects that subscribe to accept messages. For an object to be deleted | |
it must unsubscribe all of its functions from all messages (the ignoreAll() | |
method will do this). | |
""" | |
class Messager: | |
"""Singleton messager object.""" | |
def __init__(self): | |
"""Initialise the dictionary mapping message names to lists of receiver | |
functions.""" | |
self.receivers = {} | |
self.one_time_receivers = {} | |
self.verbose = False | |
def send(self,name,sender_arg=None): | |
"""Send a message with the given name and the given argument. All | |
functions registered as receivers of this message name will be | |
called.""" | |
if self.verbose: | |
print 'Sending message',name | |
if self.receivers.has_key(name): | |
for receiver in self.receivers[name]: | |
args = [] | |
if receiver['arg'] is not None: | |
args.append(receiver['arg']) | |
if sender_arg is not None: | |
args.append(sender_arg) | |
receiver['function'](*args) | |
if self.verbose: | |
print ' received by',receiver['function'] | |
if self.one_time_receivers.has_key(name): | |
for receiver in self.one_time_receivers[name]: | |
args = [] | |
if receiver['arg'] is not None: | |
args.append(receiver['arg']) | |
if sender_arg is not None: | |
args.append(sender_arg) | |
receiver['function'](*args) | |
if self.verbose: | |
print ' received by',receiver['function'] | |
del self.one_time_receivers[name] | |
def _accept(self,name,function,arg=None): | |
"""Register with the messager to receive messages with the given name, | |
messager will call the given function to notify of a message. The arg | |
object given to accept will be passed to the given function first, | |
followed by the arg object given to send by the sender object.""" | |
if not self.receivers.has_key(name): | |
self.receivers[name] = [] | |
self.receivers[name].append({'function':function,'arg':arg}) | |
if self.verbose: | |
print '',function,'subscribed to event',name,'with arg',arg | |
def _acceptOnce(self,name,function,arg=None): | |
"""Register to receive the next instance only of a message with the | |
given name.""" | |
if not self.one_time_receivers.has_key(name): | |
self.one_time_receivers[name] = [] | |
self.one_time_receivers[name].append({'function':function,'arg':arg}) | |
if self.verbose: | |
print '',function,'subscribed to event',name,'with arg',arg,'once only' | |
def _ignore(self,name,function): | |
"""Unregister the given function from the given message name.""" | |
if self.receivers.has_key(name): | |
# FIXME: Could use a fancy list comprehension here. | |
temp = [] | |
for receiver in self.receivers[name]: | |
if receiver['function'] != function: | |
temp.append(receiver) | |
self.receivers[name] = temp | |
if self.one_time_receivers.has_key(name): | |
temp = [] | |
for receiver in self.one_time_receivers[name]: | |
if receiver['function'] != function: | |
temp.append(receiver) | |
self.one_time_receivers[name] = temp | |
if self.verbose: | |
print '',function,'unsubscribed from',name | |
def clear(self): | |
"""Clear all subscriptions with the messager.""" | |
self.receivers = {} | |
self.one_time_receivers = {} | |
def __str__(self): | |
"""Return a string showing which functions are registered with | |
which event names, useful for debugging.""" | |
string = 'Receivers:\n' | |
string += self.receivers.__str__() + '\n' | |
string += 'One time receivers:\n' | |
string += self.one_time_receivers.__str__() | |
return string | |
# Create the single instance of Messager. | |
messager = Messager() | |
class Receiver: | |
"""A class to inherit if you want to register with the messager to receive | |
messages. You don't have to inherit this to register for messages, you can | |
just call messager directly, but this class maintains a list of your message | |
subscriptions and provides a handy ignoreAll() method, and an enhanced | |
ignore(...) method.""" | |
def accept(self,name,function,arg=None): | |
# We initialise subscriptions when we first need it, to avoid having an | |
# __init__ method that subclasses would need to call. | |
if not hasattr(self,'subscriptions'): | |
self.subscriptions = [] | |
messager._accept(name,function,arg) | |
self.subscriptions.append((name,function)) | |
def acceptOnce(self,name,function,arg=None): | |
if not hasattr(self,'subscriptions'): | |
self.subscriptions = [] | |
messager._acceptOnce(name,function,arg) | |
self.subscriptions.append((name,function)) | |
def ignore(self,*args): | |
if not hasattr(self,'subscriptions'): | |
return | |
if len(args) == 1: | |
name = args[0] | |
function = None | |
elif len(args) == 2: | |
name,function = args | |
else: | |
raise Exception('Wrong number of arguments to Receiver.ignore') | |
if function is None: | |
# Remove all of this object's function subscriptions to the given | |
# message name. | |
temp = [] | |
for subscription in self.subscriptions: | |
n,f = subscription | |
if n == name: | |
messager._ignore(n,f) | |
else: | |
temp.append(subscription) | |
self.subscriptions = temp | |
else: | |
# Remove the single subscription (name,function) | |
messager._ignore(name,function) | |
self.subscriptions.remove((name,function)) | |
def ignoreAll(self): | |
if not hasattr(self,'subscriptions'): | |
return | |
for subscription in self.subscriptions: | |
messager._ignore(*subscription) | |
self.subscriptions = [] | |
if __name__ == '__main__': | |
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Unit test for messager.py.""" | |
from messager import messager, Receiver | |
import unittest | |
class Logger(Receiver): | |
"""An object that maintains a log, in self.log, of the order in which its | |
methods foo and bar are called and the arguments that are passed to them. | |
Each item in the list is a 2-tuple: the method that was called, and a tuple | |
of the arguments that were passed to it.""" | |
def __init__(self): | |
# FIXME: log should be a class variable that all instances log to. | |
self.log = [] | |
def foo(self,*args): | |
self.log.append((self.foo,args)) | |
def bar(self,*args): | |
self.log.append((self.bar,args)) | |
class MessagerTest(unittest.TestCase): | |
def setUp(self): | |
self.logger = Logger() | |
def tearDown(self): | |
pass | |
# Accepting and sending messages. | |
def testMultipleAccept(self): | |
"""foo and bar are subscribed to message a, when a is sent both foo and | |
bar should be called.""" | |
self.logger.accept('a',self.logger.foo) | |
self.logger.accept('a',self.logger.bar) | |
messager.send('a') | |
# Two methods were called. | |
self.assertEqual(len(self.logger.log),2) | |
# foo was called once. | |
count = self.logger.log.count((self.logger.foo,())) | |
self.assertEqual(count,1) | |
# bar was called once. | |
count = self.logger.log.count((self.logger.bar,())) | |
self.assertEqual(count,1) | |
def testAcceptOnce(self): | |
"""foo is subscribed to message b once only, bar is subscribed to it | |
permanently. If b is sent twice, foo and bar should be called the first | |
time, only bar should be called the second time.""" | |
self.logger.acceptOnce('b',self.logger.foo) | |
self.logger.accept('b',self.logger.bar) | |
messager.send('b') | |
# foo should have been called once. | |
count = self.logger.log.count((self.logger.foo,())) | |
self.assertEqual(count,1) | |
# bar should have been called once. | |
count = self.logger.log.count((self.logger.bar,())) | |
self.assertEqual(count,1) | |
messager.send('b') | |
# foo should still have been called only once. | |
count = self.logger.log.count(( self.logger.foo,())) | |
self.assertEqual(count,1) | |
# bar should now have been called twice. | |
count = self.logger.log.count((self.logger.bar,())) | |
self.assertEqual(count,2) | |
# Ignoring messages. | |
def testIgnore(self): | |
"""foo and bar are subscribed to c, after ignore(c,foo) only bar should | |
be called when c is sent.""" | |
self.logger.accept('c',self.logger.foo) | |
self.logger.accept('c',self.logger.bar) | |
self.logger.ignore('c',self.logger.foo) | |
messager.send('c') | |
# Only one method should have been called. | |
self.assertEqual(len(self.logger.log),1) | |
# bar should have been called once. | |
count = self.logger.log.count((self.logger.bar,())) | |
self.assertEqual(count,1) | |
def testIgnoreMessage(self): | |
"""foo and bar are subscribed to c, after ignore(c) neither foo nor bar | |
should be called.""" | |
self.logger.accept('c',self.logger.foo) | |
self.logger.accept('c',self.logger.bar) | |
self.logger.ignore('c') | |
messager.send('c') | |
# No methods should have been called. | |
self.assertEqual(self.logger.log,[]) | |
def testIgnoreAll(self): | |
"""After a Receiver object calls ignoreAll() no methods of this object | |
should be called when any message is sent, but methods of other objects | |
should continue to be called.""" | |
self.logger.accept('d',self.logger.foo) | |
self.logger.accept('e',self.logger.bar) | |
second_logger = Logger() | |
second_logger.accept('d',second_logger.foo) | |
second_logger.accept('e',second_logger.bar) | |
self.logger.ignoreAll() | |
messager.send('d') | |
messager.send('e') | |
# No methods of logger should have been called. | |
self.assertEqual(self.logger.log,[]) | |
# Two methods should have been called on second_logger. | |
self.assertEqual(len(second_logger.log),2) | |
# foo should have been called once. | |
count = second_logger.log.count((second_logger.foo,())) | |
self.assertEqual(count,1) | |
# bar should have been called once. | |
count = second_logger.log.count((second_logger.bar,())) | |
self.assertEqual(count,1) | |
# Clear. | |
def testClear(self): | |
"""After clear has been called, sending a message should not result in | |
any functions being called.""" | |
messager.clear() | |
messager.send('a') | |
messager.send('b') | |
messager.send('c') | |
# No methods should have been called. | |
self.assertEqual(self.logger.log,[]) | |
# Sending with arguments. | |
def testSendWithTwoArguments(self): | |
"""If accept is called with an argument and then send is called with an | |
argument (and the same message name) the function subscribed via accept | |
should be called with accept's argument followed by send's argument.""" | |
self.logger.accept('f',self.logger.foo,'accepter arg') | |
messager.send('f','sender arg') | |
# One method should have been called. | |
self.assertEqual(len(self.logger.log),1) | |
# foo should have been called with the two arguments in the right order. | |
count = self.logger.log.count((self.logger.foo,('accepter arg','sender arg'))) | |
self.assertEqual(count,1) | |
def testSendWithNoAccepterArgument(self): | |
"""If no argument is given to the accept method, but an argument is | |
given to the send method, then the subscribed function(s) should be | |
called with the send argument only.""" | |
self.logger.accept('foo',self.logger.foo) | |
messager.send('foo','sender arg') | |
# One method should have been called. | |
self.assertEqual(len(self.logger.log),1) | |
# foo should have been called with the right argument. | |
count = self.logger.log.count((self.logger.foo,('sender arg',))) | |
self.assertEqual(count,1) | |
def testSendWithNoSenderArgument(self): | |
"""If no argument is given to the send method, but an argument is given | |
to the accept method, then the subscribed function(s) should be called | |
with the accept argument only.""" | |
self.logger.accept('foo',self.logger.foo,'accepter arg') | |
messager.send('foo') | |
# One method should have been called. | |
self.assertEqual(len(self.logger.log),1) | |
# foo should have been called with the right argument. | |
count = self.logger.log.count((self.logger.foo,('accepter arg',))) | |
self.assertEqual(count,1) | |
if __name__ == '__main__': | |
unittest.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment