본문 바로가기
Mastering Python Design Patterns

The Observer Pattern

by 자동매매 2023. 3. 22.

The Observer Pattern

When we need to update a group of objects when the state of another object changes, a popular solution is offered by the Model-View-Controller (MVC) pattern. Assume that we are using the data of the same model in two views, for instance in a pie chart and in a spreadsheet. Whenever the model is modified, both the views need to be updated. That's the role of the Observer design pattern.

The Observer pattern describes a publish-subscribe relationship between a single object, the publisher, which is also known as the subject or observable, and one or more objects, the subscribers, also known as observers.

In the case of MVC, the publisher is the model and the subscribers are the views. There are other examples and we will discuss them throughout this chapter.

The ideas behind Observer are the same as those behind the separation of concerns principle, that is, to increase decoupling between the publisher and subscribers, and to make it easy to add/remove subscribers at runtime.

In this chapter, we will discuss:

  • Real-world examples
    • Use cases
      • Implementation

Real-world examples

In reality, an auction resembles the Observer pattern. Every auction bidder has a number paddle that is raised whenever they want to place a bid. Whenever the paddle is raised by a bidder, the auctioneer acts as the subject by updating the price of the bid and broadcasting the new price to all bidders (subscribers).

The Observer Pattern Chapter 131*

In software, we can cite at least two examples:

  • Kivy, the Python Framework for developing user interfaces, has a module called Properties, which implements the Observer pattern. Using this technique, you can specify what should happen when a property's value changes.
    • The RabbitMQ library can be used to add asynchronous messaging support to an application. Several messaging protocols are supported, such as HTTP and AMQP. RabbitMQ can be used in a Python application to implement a publish- subscribe pattern, which is nothing more than the Observer design pattern (j.mp/rabbitmqobs).

Use cases

We generally use the Observer pattern when we want to inform/update one or more objects (observers/subscribers) about a change that happened on a given object (subject/publisher/observable). The number of observers, as well as who those observers are may vary and can be changed dynamically.

We can think of many cases where Observer can be useful. One such use case is news feeds. With RSS, Atom, or other related formats, you follow a feed, and every time it is updated, you receive a notification about the update.

The same concept exists in social networking. If you are connected to another person using a social networking service, and your connection updates something, you are notified about it. It doesn't matter if the connection is a Twitter user that you follow, a real friend on Facebook, or a business colleague on LinkedIn.

Event-driven systems are another example where Observer is usually used. In such systems, you have listeners that listen for specific events. The listeners are triggered when an event they are listening to is created. This can be typing a specific key (on the keyboard), moving the mouse, and more. The event plays the role of the publisher and the listeners play the role of the observers. The key point in this case is that multiple listeners (observers) can be attached to a single event (publisher).

Implementation

In this section, we will implement a data formatter. The ideas described here are based on the ActiveState Python Observer code recipe (https:// code.activestate.com/ ). There is a default formatter that shows a value in the decimal format. However, we can add/register more formatters. In this example, we will add a hex and binary formatter. Every time the value of the default formatter is updated, the registered formatters are notified and take action. In this case, the action is to show the new value in the relevant format.

Observer is actually one of the patterns where inheritance makes sense. We can have a base Publisher class that contains the common functionality of adding, removing, and

notifying observers. Our DefaultFormatter class derives from Publisher and adds the formatter-specific functionality. And, we can dynamically add and remove observers on demand.

We begin with the Publisher class. The observers are kept in the observers list. The add() method registers a new observer, or throws an error if it already exists. The remove() method unregisters an existing observer, or throws an exception if it does not exist. Finally, the notify() method informs all observers about a change:

class Publisher:

def __init__(self):

self.observers = []

def add(self, observer):

if observer not in self.observers: self.observers.append(observer) else:

print(f'Failed to add: {observer}')

def remove(self, observer):

try: self.observers.remove(observer)

except ValueError:

print(f'Failed to remove: {observer}')

def notify(self):

[o.notify(self) for o in self.observers]

Let's continue with the DefaultFormatter class. The first thing that its __init__() does is call the __init__() method of the base class, since this is not done automatically in Python.

A DefaultFormatter instance has a name to make it easier for us to track its status. We

use name mangling in the _data variable to state that it should not be accessed directly. Note that this is always possible in Python but fellow developers have no excuse for doing so, since the code already states that they shouldn't. There is a serious reason for using name mangling in this case. Stay tuned. DefaultFormatter treats the _data variable as

an integer, and the default value is zero:

class DefaultFormatter(Publisher): def __init__(self, name): Publisher.__init__(self) self.name = name

self._data = 0

The __str__() method returns information about the name of the publisher and the value of the _data attribute. type(self).__name__ is a handy trick to get the name of a class without hardcoding it. It is one of those tricks that makes your code easier to maintain:

def __str__(self):

return f"{type(self).__name__}: '{self.name}' has data = {self._data}"

There are two data() methods. The first one uses the @property decorator to give read access to the _data variable. Using this, we can just execute object.data instead of object.data():

@property

def data(self):

return self._data

The second data() method is more interesting. It uses the @setter decorator, which is called every time the assignment (=) operator is used to assign a new value to the _data variable. This method also tries to cast a new value to an integer, and does exception handling in case this operation fails:

@data.setter

def data(self, new_value): try:

self._data = int(new_value) except ValueError as e:

print(f'Error: {e}') else:

self.notify()

The next step is to add the observers. The functionality of HexFormatter and

BinaryFormatter is very similar. The only difference between them is how they format

the value of data received by the publisher, that is, in hexadecimal and binary, respectively:

class HexFormatterObs:

def notify(self, publisher):

value = hex(publisher.data)

print(f"{type(self).__name__}: '{publisher.name}' has now hex data

  • {value}")

class BinaryFormatterObs:

def notify(self, publisher):

value = bin(publisher.data)

print(f"{type(self).__name__}: '{publisher.name}' has now bin data

  • {value}")

To help us use those classes, the main() function initially creates a DefaultFormatter instance named test1 and, afterwards, attaches (and detaches) the two available observers. We also have some exception handling to make sure that the application does not crash when erroneous data is passed by the user.

The code is as follows:

def main():

df = DefaultFormatter('test1') print(df)

print()

hf = HexFormatterObs() df.add(hf)

df.data = 3 print(df)

print()

bf = BinaryFormatterObs() df.add(bf)

df.data = 21 print(df)

Moreover, tasks such as trying to add the same observer twice or removing an observer that does not exist should cause no crashes:

print() df.remove(hf) df.data = 40 print(df) print() df.remove(hf) df.add(bf)

df.data = 'hello' print(df)

print() df.data = 15.8 print(df)

Let's now recapitulate the full code of the example (the observer.py file):

  1. We define the Publisher class:

class Publisher:

def __init__(self):

self.observers = []

def add(self, observer):

if observer not in self.observers: self.observers.append(observer) else:

print(f'Failed to add: {observer}')

def remove(self, observer):

try: self.observers.remove(observer)

except ValueError:

print(f'Failed to remove: {observer}') def notify(self):

[o.notify(self) for o in self.observers]

  1. We define the DefaultFormatter class, with its special __init__ and __str__ methods:

class DefaultFormatter(Publisher):

def __init__(self, name):

Publisher.__init__(self)

self.name = name

self._data = 0

def __str__(self):

return f"{type(self).__name__}: '{self.name}' has data = {self._data}"

  1. We add the data property getter and setter methods to the DefaultFormatter class:

@property def data(self):

return self._data @data.setter

def data(self, new_value): try:

self._data = int(new_value) except ValueError as e:

print(f'Error: {e}') else:

self.notify()

  1. We define our two observer classes, as follows:

class HexFormatterObs:

def notify(self, publisher):

value = hex(publisher.data)

print(f"{type(self).__name__}: '{publisher.name}' has now hex data = {value}")

class BinaryFormatterObs:

def notify(self, publisher):

value = bin(publisher.data)

print(f"{type(self).__name__}: '{publisher.name}' has now bin data = {value}")

  1. Now, we take care of the main part of the program; the first part of the main() function is as follows:

def main():

df = DefaultFormatter('test1') print(df)

print()

hf = HexFormatterObs() df.add(hf)

df.data = 3

print(df)

print()

bf = BinaryFormatterObs() df.add(bf)

df.data = 21

print(df)

  1. Here is the end of the main() function:

print() df.remove(hf) df.data = 40 print(df) print() df.remove(hf)

df.add(bf) df.data = 'hello' print(df)

print()

df.data = 15.8 print(df)

  1. We do not forget the usual snippet that calls the main() function: if __name__ == '__main__':

main()

Executing the python observer.py command gives the following output:

What we see in the output is that as the extra observers are added, more (and relevant) output is shown, and when an observer is removed, it is not notified any longer. That's exactly what we want: runtime notifications that we are able to enable/disable on demand.

The defensive programming part of the application also seems to work fine. Trying to do funny things, such as removing an observer that does not exist or adding the same observer twice, is not allowed. The messages shown are not very user-friendly, but I leave that up to you as an exercise. Runtime failures such as trying to pass a string when the API expects a number are also properly handled without causing the application to crash/terminate.

This example would be much more interesting if it were interactive. Even a simple menu that allows the user to attach/detach observers at runtime and to modify the value of DefaultFormatter would be nice because the runtime aspect becomes much more visible. Feel free to do it.

Another nice exercise is to add more observers. For example, you can add an octal formatter, a Roman numeral formatter, or any other observer that uses your favorite representation. Be creative!

Summary

In this chapter, we covered the Observer design pattern. We use Observer when we want to be able to inform/notify all stakeholders (an object or a group of objects) when the state of an object changes. An important feature of Observer is that the number of subscribers/observers, as well as who the subscribers are, may vary and can be changed at runtime.

To understand Observer, you can think of an auction, with the bidders being the subscribers and the auctioneer being the publisher. This pattern is used quite a lot in the software world.

As specific examples of software using Observer, we mentioned the following:

  • Kivy, the framework for developing innovative user interfaces, with its Properties concept and module.
    • The Python bindings of RabbitMQ. We referred to a specific example of RabbitMQ used to implement the publish-subscribe (also known as Observer) pattern.

In the implementation example, we saw how to use Observer to create data formatters that can be attached and detached at runtime to enrich the behavior of an object. Hopefully, you will find the recommended exercises interesting.

The next chapter introduces the State design pattern, which can be used to implement a core computer science concept: state machines.

[ 131 ])

  • 2

'Mastering Python Design Patterns' 카테고리의 다른 글

Other Behavioral Patterns  (0) 2023.03.22
The State Pattern  (0) 2023.03.22
The Command Pattern  (0) 2023.03.22
The Chain of Responsibility  (0) 2023.03.22
Other Structural Patterns  (0) 2023.03.22

댓글