Design Patterns. Part 2
In the previous article, we've talked about such patterns as the Abstract Factory and Strategy. As have been pointed out by our users, these patterns are making more sense when used in such languages as Java, C++, C#, but not in Python, since its OOP style is much more flexible. This time we've decided to describe more useful patterns for Python, namely the Observer and Mediator. If you want to learn to use patterns, pay attention to the following video shared shared by suic - Python Design Patterns.
An observer is a behavioral design pattern that creates a subscription mechanism that allows some objects to monitor and respond to the events occurring in other objects.
Imagine that you have two objects: a Buyer and a Store. The store is about to bring a new product, which the buyer is interested in. The buyer can go to the store every day to check the product's availability. But at the same time he'll be getting angry wasting his valuable time. On the other hand, the store can massively send out the information about the product to all of its customers. But this might upset many of them, because the product is very specific and not everyone needs it. And the conflict occurs: either the buyer spends time on periodic checkings, or the store spends resources on useless notifications.
Let's call the objects that contain an important or interesting for others state - the Publishers. Other objects that want to track changes of that state we'll call the Subscribers.
The Observer pattern suggests storing a list of links to the subscriber objects inside the publisher object, and the publisher shouldn't maintain the subscription list on its own. He'll provide methods by which subscribers can add or remove themselves from the list.
Now the most interesting part. When an important event occurs in the Publisher, it will go through the list of subscribers and notify them about it, calling a certain method of subscriber objects. The Publisher doesn't care which class a Subscriber will have, since they all have to follow the common interface and have a single notification method.
After seeing how everything works, you can highlight the common interface that describes methods of subscription and unsubscription for all Publishers. After that, the Subscribers will be able to work with different types of Publishers, as well as receive notifications from them through the same method.
Here is another good example:
After you've subscribed to a newspaper or magazine, you no longer need to go to the supermarket and check if the next issue is available. Instead, the publishing house will send new issues by mail directly to your home right after they've been released. The publishing house maintains a list of subscribers and knows which magazine send to whom. You can unsubscribe at any time, and you'll stop receiving this magazine.
Situations in which the pattern is worth using:
1. When after changing the state of one object you need to do something in others, but you don't know up-front which objects should react.
The described problem can occur during the development of the user interface libraries when you need to enable third-party classes to react to the button clicks. The Observer pattern allows any object with the subscriber interface to register for receiving the notifications about the events occurring in publisher objects.
2. When some objects have to observe others, but only in certain cases. The publishers maintain dynamic lists. All observers can subscribe or unsubscribe from receiving the notifications right during program execution.
Let's look at a practical example. Imagine that you are a bestselling author and every bookstore wants to be the first one to get your new books and put them on the shelves. In this case, many bookstores will overwhelm you with calls, messages and e-mails, asking for you to specify an exact date of the new book's release. To stop this madness, but at the same time don't cut off all ties with the shops, you can use this pattern, where the stores will act as Observers. In practice, it will look like this:
class Author: def __init__(self): self._info = None self._observers = set() def add_bookshop(self, observer): if not isinstance(observer, Bookshop): raise TypeError() self._observers.add(observer) def del_bookshop(self, observer): self._observers.remove(observer) def set_info(self, info): self._info = info self.notify(info) def notify(self, info): for observer in self._observers: observer.update(info) class Bookshop: def __init__(self, name): self._name = name def update(self, info): print (self._name, 'has got the notification:', info)
Thus, it'll be enough for the bookstores to become the Observers and just wait for a newsletter from you. As soon as you know exactly when your book is going to be published - you won't have to personally call each separate store (there can be hundreds or even thousands of them!). It'll be enough to specify the date in the text of a newsletter and send the information to everyone who is on the observers list. Let's see how it works:
author = Author() bookshop_1 = Bookshop("'Bookworm'") bookshop_2 = Bookshop("'McCandall and the sons'") author.add_bookshop(bookshop_1) author.add_bookshop(bookshop_2) author.set_info('New book will be available from 29.07.2018')
As a result, every bookstore will receive the notification:
'McCandall and the sons' has received the notification: New book will be available from 29.07.2018
'Bookworm' has received the notification: New book will be available from 29.07.2018
- the publishers are independent from specific subscriber classes and vice versa;
- you can subscribe and unsubscribe recipients on the fly.
- the subscribers are notified at random (it's worth bearing in mind, since this may be important for some situations).
To make sure of this pattern effectiveness, I suggest that you solve the "Party Invitations" task with its help.
A mediator is a behavioral design pattern that allows you to reduce the connectivity of multiple classes with each other, by moving these connections to the one mediation class.
Suppose you have a dialog for creating a user profile. It consists of all possible controls - the text fields, checkboxes, buttons.
The individual elements of the dialog should interact with each other. For example, the checkbox "I have a dog" opens a hidden field for entering the name of a pet, and the clicking on the submit button initiates the checking of all fields values of the form.
By introducing this very logic directly in the control's code, you'll put an end to their reuse in other places of the application. They'll become too closely connected to the elements of the profile editing dialog that aren't needed in other contexts. Therefore, you can use either all elements at once, or none.
The Mediator pattern causes objects to communicate not directly with each other, but through a separate mediator object that knows to which one of them a particular request needs to be redirected. Due to this, the system components will depend only on the mediator, and not on dozens of other components. In our example, a dialog could become a mediator. Most likely, the dialog class already knows its elements, so no new connections will have to be added.
The main changes will occur within the individual elements of the dialog. Whereas previously, upon the receiving of the user's click, the button object checked the values of the dialog fields itself, now its only responsibility is to inform the dialog that there was a click. The notified dialog will perform all necessary field verifications. Thus, instead of having several dependencies on other elements, for the button there will be only one - from the dialog itself.
To make code even more flexible, you can select a common interface for all of the mediators, that is, the program dialogs. Our button will become dependent not on the specific dialog of creating the user, but on the abstract one, thus allowing to use it in other dialogs. This way, the mediator conceals all complex connections and dependencies between the classes of individual program components within itself. And the fewer connections the classes have, the easier it is to modify, extend and reuse them.
Let's look at another example:
The pilots of landing aircrafts or the ones that are taking off don't communicate directly with other pilots. Instead, they contact the dispatcher, who coordinates the actions of several aircrafts at the same time. Without a dispatcher, pilots would have to be on a high alert all the time and monitor all the surrounding aircraft on their own, which would lead to frequent disasters in the sky. It's important to understand that the dispatcher is not needed during the entire flight. He’s involved only in the area of the airport, when it's necessary to coordinate the interactions of a lot of aircrafts.
Situations in which the pattern is worth using:
1. When it's difficult for you to change some classes because they have many chaotic connections to other classes.
The mediator allows you to put all these connections in one class, after which it'll be easier for you to correct them (if necessary), make them more understandable and flexible.
2. When you can't reuse a class, because it depends on a lot of other classes.
After applying the pattern, the components lose their previous connections to other components, and all their interactions occur strictly through the mediator object.
3. When you have to create multiple subclasses of components to use the same components in different contexts.
If earlier the connection changes in one component could lead to a huge avalanche of changes in all other components, now it's enough for you to create a mediator subclass and change the connections between its components.
Let's take a look at the implementation of this pattern in the following example - we have a program in which you can switch between 3 windows - the MainWindow, SettingsWindow, and HelpWindow. Consequently, in order to ensure that all information is displayed correctly, you need to hide the other windows when switching to a particular one of them. To apply the logic of interaction with other windows inside the class of each of that windows may not be such a good idea and it’s preferable to take it to some other place. In this case, we can use the Mediator for our purposes.
class WindowBase(object): def show(self): raise NotImplementedError() def hide(self): raise NotImplementedError() class MainWindow(WindowBase): def show(self): print('Show MainWindow') def hide(self): print('Hide MainWindow') class SettingsWindow(WindowBase): def show(self): print('Show SettingsWindow') def hide(self): print('Hide SettingsWindow') class HelpWindow(WindowBase): def show(self): print('Show HelpWindow') def hide(self): print('Hide HelpWindow') class WindowMediator(object): def __init__(self): self.windows = dict.fromkeys(['main', 'settings', 'help']) def show(self, win): for window in self.windows.values(): if not window is win: window.hide() win.show() def set_main(self, win): self.windows['main'] = win def set_settings(self, win): self.windows['settings'] = win def set_help(self, win): self.windows['help'] = win
Now we can make sure that the display of windows is correct and when addressing one of them, the others are hidden.
main_win = MainWindow() settings_win = SettingsWindow() help_win = HelpWindow() med = WindowMediator() med.set_main(main_win) med.set_settings(settings_win) med.set_help(help_win)
When you address the settings window, the main window and help window will be hidden:
med.show(settings_win) # Hide MainWindow # Hide HelpWindow # Show SettingsWindow
It's the same for other windows:
med.show(help_win) # Hide MainWindow # Hide SettingsWindow # Show HelpWindow
- it eliminates the dependencies between the components allowing them to be reused;
- simplifies the interaction between the components;
- centralizes management in one place.
- the mediator class can "blow itself up" quite a bit, which can complicate the work with it.
To follow up all that gained knowledge, try solving the "Dialogues" mission with the use of a Mediator pattern.
As you can see, the Observer and Mediator patterns are very useful in certain situations and can greatly help in the development of quality programs. However, I would like to remind you that patterns aren’t a panacea and they should be used only in those situations where they are really needed and will bring a considerable benefit.
The following resources were used for this article:
- Design Patterns in Python (GitHub repo)
- FlatIcon (the source of illustrations)
Welcome to CheckiO - games for coders where you can improve your codings skills.
The main idea behind these games is to give you the opportunity to learn by exchanging experience with the rest of the community. Every day we are trying to find interesting solutions for you to help you become a better coder.Join the Game