Messager pattern from Panda3D


/ Published in: Python
Save to your folder(s)

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.


Copy this code and paste it in your HTML
  1. """
  2. messager.py -- a simple message-passing pattern for one-many or many-many
  3. dependencies. Useful for event notifications, for example.
  4.  
  5. To send a message use the singleton messager instance:
  6.  
  7. from messager import messager
  8.  
  9. messager.send('message name',argument)
  10.  
  11. You can pass a single argument with a message, and this argument can be anything
  12. you like. For example, event objects that simply hold a number of attributes can
  13. be constrcuted and passed as arguments with messages.
  14.  
  15. Messager maintains a mapping of message names to lists of functions (and their
  16. arguments). When a message is sent, all of the functions subscribed to that
  17. message are called and passed the argument given when the function was
  18. subscribed followed by argument given when the message was sent. To subscribe a
  19. function you must subclass Receiver and call the accept(...) or acceptOnce(...)
  20. methods:
  21.  
  22. self.accept('message name',function,argument)
  23.  
  24. self.acceptOnce('message name',function,argument)
  25.  
  26. You don't need to call Receiver.__init__() when you subclass Receiver, it has no
  27. __init__. Receiver works by maintaining a list of the message subscriptions you
  28. have made.
  29.  
  30. It is up to you to make sure that functions that accept messages take the right
  31. number of arguments, 0, 1 or 2 depending on whether the accept(...) and
  32. send(...) methods were called with an argument or not.
  33.  
  34. To unsubscribe a function from a message name use ignore:
  35.  
  36. # Unsubscribe a particular function from a particular message name.
  37. self.ignore('message name',function)
  38.  
  39. # Unsubscribe all functions that this object has subscribed to a particular
  40. # message name.
  41. self.ignore('message name')
  42.  
  43. # Unsubscribe all functions that this object has subscribed to any message
  44. # name.
  45. self.ignoreAll()
  46.  
  47. You can unsubscribe all functions from all Receiver objects with:
  48.  
  49. messager.clear()
  50.  
  51. If you do `messager.verbose = True` the messager will print whenever it
  52. receives a message or subscription, and if you do `print messager` the messager
  53. will print out a list of all the registered message names and their subscribers.
  54.  
  55. One last thing to be aware of is that messager keeps references to (functions
  56. of) all objects that subscribe to accept messages. For an object to be deleted
  57. it must unsubscribe all of its functions from all messages (the ignoreAll()
  58. method will do this).
  59.  
  60. """
  61.  
  62. class Messager:
  63. """Singleton messager object."""
  64.  
  65. def __init__(self):
  66. """Initialise the dictionary mapping message names to lists of receiver
  67. functions."""
  68.  
  69. self.receivers = {}
  70. self.one_time_receivers = {}
  71. self.verbose = False
  72.  
  73. def send(self,name,sender_arg=None):
  74. """Send a message with the given name and the given argument. All
  75. functions registered as receivers of this message name will be
  76. called."""
  77.  
  78. if self.verbose:
  79. print 'Sending message',name
  80.  
  81. if self.receivers.has_key(name):
  82. for receiver in self.receivers[name]:
  83. args = []
  84. if receiver['arg'] is not None:
  85. args.append(receiver['arg'])
  86. if sender_arg is not None:
  87. args.append(sender_arg)
  88. receiver['function'](*args)
  89. if self.verbose:
  90. print ' received by',receiver['function']
  91.  
  92. if self.one_time_receivers.has_key(name):
  93. for receiver in self.one_time_receivers[name]:
  94. args = []
  95. if receiver['arg'] is not None:
  96. args.append(receiver['arg'])
  97. if sender_arg is not None:
  98. args.append(sender_arg)
  99. receiver['function'](*args)
  100. if self.verbose:
  101. print ' received by',receiver['function']
  102. del self.one_time_receivers[name]
  103.  
  104. def _accept(self,name,function,arg=None):
  105. """Register with the messager to receive messages with the given name,
  106. messager will call the given function to notify of a message. The arg
  107. object given to accept will be passed to the given function first,
  108. followed by the arg object given to send by the sender object."""
  109.  
  110. if not self.receivers.has_key(name):
  111. self.receivers[name] = []
  112. self.receivers[name].append({'function':function,'arg':arg})
  113.  
  114. if self.verbose:
  115. print '',function,'subscribed to event',name,'with arg',arg
  116.  
  117. def _acceptOnce(self,name,function,arg=None):
  118. """Register to receive the next instance only of a message with the
  119. given name."""
  120.  
  121. if not self.one_time_receivers.has_key(name):
  122. self.one_time_receivers[name] = []
  123. self.one_time_receivers[name].append({'function':function,'arg':arg})
  124.  
  125. if self.verbose:
  126. print '',function,'subscribed to event',name,'with arg',arg,'once only'
  127.  
  128. def _ignore(self,name,function):
  129. """Unregister the given function from the given message name."""
  130.  
  131. if self.receivers.has_key(name):
  132. # FIXME: Could use a fancy list comprehension here.
  133. temp = []
  134. for receiver in self.receivers[name]:
  135. if receiver['function'] != function:
  136. temp.append(receiver)
  137. self.receivers[name] = temp
  138.  
  139. if self.one_time_receivers.has_key(name):
  140. temp = []
  141. for receiver in self.one_time_receivers[name]:
  142. if receiver['function'] != function:
  143. temp.append(receiver)
  144. self.one_time_receivers[name] = temp
  145.  
  146. if self.verbose:
  147. print '',function,'unsubscribed from',name
  148.  
  149. def clear(self):
  150. """Clear all subscriptions with the messager."""
  151.  
  152. self.receivers = {}
  153. self.one_time_receivers = {}
  154.  
  155. def __str__(self):
  156. """Return a string showing which functions are registered with
  157. which event names, useful for debugging."""
  158.  
  159. string = 'Receivers:\n'
  160. string += self.receivers.__str__() + '\n'
  161. string += 'One time receivers:\n'
  162. string += self.one_time_receivers.__str__()
  163. return string
  164.  
  165. # Create the single instance of Messager.
  166. messager = Messager()
  167.  
  168. class Receiver:
  169. """A class to inherit if you want to register with the messager to receive
  170. messages. You don't have to inherit this to register for messages, you can
  171. just call messager directly, but this class maintains a list of your message
  172. subscriptions and provides a handy ignoreAll() method, and an enhanced
  173. ignore(...) method."""
  174.  
  175. def accept(self,name,function,arg=None):
  176.  
  177. # We initialise subscriptions when we first need it, to avoid having an
  178. # __init__ method that subclasses would need to call.
  179. if not hasattr(self,'subscriptions'):
  180. self.subscriptions = []
  181.  
  182. messager._accept(name,function,arg)
  183. self.subscriptions.append((name,function))
  184.  
  185. def acceptOnce(self,name,function,arg=None):
  186.  
  187. if not hasattr(self,'subscriptions'):
  188. self.subscriptions = []
  189.  
  190. messager._acceptOnce(name,function,arg)
  191. self.subscriptions.append((name,function))
  192.  
  193. def ignore(self,*args):
  194.  
  195. if not hasattr(self,'subscriptions'):
  196. return
  197.  
  198. if len(args) == 1:
  199. name = args[0]
  200. function = None
  201. elif len(args) == 2:
  202. name,function = args
  203. else:
  204. raise Exception('Wrong number of arguments to Receiver.ignore')
  205.  
  206. if function is None:
  207. # Remove all of this object's function subscriptions to the given
  208. # message name.
  209. temp = []
  210. for subscription in self.subscriptions:
  211. n,f = subscription
  212. if n == name:
  213. messager._ignore(n,f)
  214. else:
  215. temp.append(subscription)
  216. self.subscriptions = temp
  217. else:
  218. # Remove the single subscription (name,function)
  219. messager._ignore(name,function)
  220. self.subscriptions.remove((name,function))
  221.  
  222. def ignoreAll(self):
  223.  
  224. if not hasattr(self,'subscriptions'):
  225. return
  226.  
  227. for subscription in self.subscriptions:
  228. messager._ignore(*subscription)
  229. self.subscriptions = []
  230.  
  231. if __name__ == '__main__':
  232.  
  233. pass

Report this snippet


Comments

RSS Icon Subscribe to comments

You need to login to post a comment.