Das Entwurfsmuster Beobachter (engl. Observer pattern), auch Publisher/Subscriber Pattern genannt, ist ein objekt-orientiertes Struktur- bzw. Verhaltensmuster, das die Beziehung und Interaktion zwischen mehreren Objekten beschreibt, die Ereignisse generieren und verarbeiten. Es wird verwendet, um Objektbeziehungen zu entkoppeln und ist oft Grundlage des Event-Handlings in objekt-orienterten Anwendungen.
Der folgende Vortrag stellt das Beobachter-Pattern vor und bietet eine Beispiel-Implementierung in Python. Der Vortrag wurde bei einem der Treffen von pyCologne gehalten.
Beispielcode mit Implementierung des Patterns
Tar/Bz2-Archiv mit der ODP- und PDF-Version, dem Python-Code und allen Diagrammen als dia- und PNG-Dateien.
Beispiel-Implementierung
1 """Class implementing Publisher/Subscriber object pattern.
2
3 Also called Observer pattern. The Observer/Subscriber class is not included
4 because it is just an interface, requiring only a notify() method. See the
5 StockViewer class in the test code at the end of the module for a minimal
6 example.
7 """
8
9 __all__ = ['UnsupportedEventError', 'Event', 'Publisher']
10
11
12 __author__ = "Christopher Arndt"
13 __version__ = "1.0.1"
14 __revision__ = "$Rev: 199 $"
15 __date__ = "$Date: 2008-01-11 16:15:28 +0100 (Fr, 11 Jan 2008) $"
16 __copyright__ = "MIT license"
17
18 # standard library modules
19 import sys
20 from weakref import WeakKeyDictionary
21
22 if sys.version_info[:2] < (2,4):
23 try:
24 from sets import Set as set
25 except ImportError:
26 raise NotImplemented, "Needs either Python >= 2.4 or the 'sets' module."
27
28 class UnsupportedEventError(Exception):
29 pass
30
31 class Event(object):
32 name = "Generic event"
33
34 def __init__(self, **state):
35 self.state = state
36
37 def __str__(self):
38 state = ", ".join(["%s: %r" % (k,v)
39 for k,v in sorted(self.state) if not callable(k)])
40 return "%s (%s)\n" % (self.name, state)
41
42 class Publisher(object):
43 """Publisher class with support for push/pull method and event broking.
44
45 Can be used as a subclass for every object that generates signals
46 to which observers can subscribe.
47
48 Can also be used as an Event Broker, where Publishers register their
49 signals and Subscribers subscribe to them.
50
51 Subscribers may get the state of the Publisher by calling the ``get_state``
52 method of the Publisher (pull method - only makes sense when every event
53 generating object implements the Publisher interface) or the Publishers
54 should attach their current state to the event object, which is sent
55 to subscribers (push method).
56 """
57
58 def __init__(self):
59 self.subscribers = {}
60 self.events = set([Event])
61 self.debug = False
62
63 def subscribe(self, subscriber, events=None):
64 """Subscribe ``subscriber`` to be notified if one of ``events`` occurs.
65
66 ``subscriber`` must be an object with a ``notify`` method, which
67 accepts one argument, the event signalled.
68
69 ``events`` should be a list of ``Event`` subclasses class or instances
70 thereof. When subscribing to the generic ``Event`` class (or omitting
71 the ``events`` argument), the subscriber will get notified of all
72 events.
73 """
74
75 # observer must have 'notify()' method
76 assert hasattr(subscriber, 'notify')
77 # 'notify()' must accept at least two arguments
78 assert subscriber.notify.func_code.co_argcount >= 2
79 if events is None:
80 events = [Event]
81 for event in events:
82 if isinstance(event, Event):
83 event = event.__class__
84 if event not in self.events:
85 raise UnsupportedEventError, "Event '%s' not supported." % event
86 self.subscribers.setdefault(event, WeakKeyDictionary()
87 )[subscriber] = True
88
89 def unsubscribe(self, subscriber, events=None):
90 """Unscubscribe ``subscriber`` from being notified of ``events``.
91
92 ``events`` should be a list of ``Event`` subclasses class or instances
93 thereof. When omitting the ``events`` argument, the subscriber will
94 get unsubscribed from all events.
95 """
96
97 if isinstance(event, Event):
98 event = event.__class__
99 if events is None:
100 events = list(self.events)
101 for event in events:
102 try:
103 del self.subscribers[event][subscriber]
104 except KeyError:
105 pass
106
107 def notify_subscribers(self, event):
108 """Notify all subscribers of an event.
109
110 Basically just calls ``notify(event)`` on every object subscribed
111 to this event or to the generic ``Event``. The order in which
112 subscribers are notified is not defined.
113 """
114
115 if event.__class__ not in self.events:
116 raise UnsupportedEventError, "Event '%s' not supported." % event
117 # build list of subscribers to given and catch-all event
118 subscribers = dict(self.subscribers.get(event.__class__, {}))
119 subscribers.update(self.subscribers.get(Event, {}))
120 if not getattr(event, 'publisher', None):
121 event.publisher = self
122 for subscriber in subscribers:
123 if self.debug and getattr(event, '_log', True):
124 print event
125 subscriber.notify(event)
126
127 def update(self, event):
128 """Signal event.
129
130 To be called by Publishers in a distributed architecture when this
131 class acts as an event broker.
132
133 The default implementation just calls ``self.notify_subscribers(event)``.
134 Subclasses might want to overwrite this method to filter/alter events
135 or do some checks on them before publishing.
136 """
137
138 self.notify_subscribers(event)
139
140 def get_supported_events(self):
141 """Return list of registered events."""
142
143 return self.events
144
145 def add_event(self, event):
146 """Register new event.
147
148 ``event`` may be a subclass of ``Event`` or an instance thereof.
149 """
150
151 if isinstance(event, Event):
152 event = event.__class__
153 self.events.add(event)
154
155 def remove_event(self, event):
156 """Unregister an event.
157
158 ``event`` may be a subclass of ``Event`` or an instance thereof.
159 """
160
161 if isinstance(event, Event):
162 event = event.__class__
163 if event is Event:
164 raise ValueError, "Can't remove catch-all event."
165 try:
166 del self.subscribers[event]
167 except: pass
168 try:
169 self.events.remove(event)
170 except: pass
171
172 def get_state(self, *args):
173 """Return current state of object.
174
175 To be called by subscribers on notify().
176 """
177
178 raise NotImplemented, "Method must be implemented by subclass."
179
180
181 def _test():
182 """Module test code."""
183
184 class AddGoodsEvent(Event):
185 name = 'Goods added event'
186
187 class RemoveGoodsEvent(Event):
188 name = 'Goods removed event'
189
190 # publisher
191 class Stock(Publisher):
192
193 def __init__(self):
194 super(Stock, self).__init__()
195 self.stock = {}
196 self.add_event(AddGoodsEvent)
197 self.add_event(RemoveGoodsEvent)
198
199 def add_goods(self, what, amount):
200 try:
201 self.stock[what] += amount
202 except KeyError:
203 self.stock[what] = amount
204 self.notify_subscribers(AddGoodsEvent())
205
206 def remove_goods(self, what, amount):
207 try:
208 stock = self.stock[what]
209 if amount > stock:
210 amount = stock
211 self.stock[what] -= amount
212 stock = self.stock
213 except KeyError:
214 stock = 0
215 self.notify_subscribers(RemoveGoodsEvent())
216 return self.stock
217
218 def get_state(self):
219 return self.stock
220
221 # subscriber
222 class StockViewer(object):
223
224 def notify(self, event):
225 if isinstance(event, AddGoodsEvent):
226 print "Goods have been added to the stock!"
227 elif isinstance(event, RemoveGoodsEvent):
228 print "Goods have been taken from the stock!"
229 goods = event.publisher.get_state()
230 print "Stock now:", goods
231 for good, amount in goods.items():
232 if amount <= 5:
233 print "Running low on %s!" % good
234 print
235
236 stock = Stock()
237 stockviewer = StockViewer()
238 # by default, get notified of all events
239 #stock.subscribe(stockviewer)
240 stock.subscribe(stockviewer, [AddGoodsEvent, RemoveGoodsEvent])
241
242 stock.add_goods('apples', 10)
243 stock.add_goods('bananas', 30)
244 stock.add_goods('cherries', 100)
245
246 stock.remove_goods('apples', 3)
247 stock.remove_goods('bananas', 25)
248
249 if __name__ == '__main__':
250 _test()