-
Design Patterns. Part 3
In this article, we'll continue going through the design patterns that can help the developer in the commonly occurring situations and will become more familiar with the Memento and Bridge design patterns. The first of them is appropriate when it's not enough to have the information only about the last/current state of the object - you need to have data about its past states with the ability to return to them. The second pattern is suitable for situations in which it's necessary to divide the abstraction and the implementation, so that they could change independently of one another, as well as to share responsibility between classes.
Memento
Memento is a behavioral design pattern that allows you to save and restore the past states of the objects without revealing the details of their implementation.
Let's say that you are writing a text editor program. Apart from the usual addition and removal of the text, your editor allows you to change the text formatting, insert pictures and other. At some point, you've decided to make all of these actions cancelable. To do this, you need to save the current state of the text before performing any actions. If you then decide to cancel your action, you'll get a copy of the state from the history and restore the old state of the text.
Let's now take a look at the copies of the editor's state. It should have several fields for storing the current text and all of its properties (font, color, etc.), cursor and scroll position of the screen, inserted images and much more. To make a copy of the state, you need to write the values of all these fields in a certain "container". Most likely, you'll need to store a lot of such containers as a history of operations, so the most convenient thing is to make them the objects of the same class. This class must have many fields, but almost no methods.
The Memento pattern perfectly handles all of these requirements. It suggests holding a copy of the state in a special image object with a limited interface, which allows, for example, to find out the date/time of storage or the name of the snapshot. But, on the other hand, the snapshot should be open to its creator, allowing you to read and restore its internal state.
Such a scheme allows the creators to take snapshots and give them away for storage to other objects called guardians. The guardians will only have access to a limited image interface, so they won't be able to affect the "insides" of the image itself. At the right time, the guardian can ask the creator to restore its state, providing it with the appropriate snapshot.
In the example with the editor, you can choose a separate class to be the guardian that will store the list of completed operations. The limited interface of images will show the user a beautiful list with names and dates of the performed operations. And when the user decides to roll back the operation, the history class takes the last snapshot from the stack and sends it to the editor to restore it.
Situations in which it's appropriate to use the pattern:
1) When you need to save snapshots of the object's state (or a part of it), so that the object could later be restored in the same state.
The Memento pattern allows you to create any number of the object's snapshots and store them, regardless of the object from which they've been taken. Snapshots are often used not only to implement a cancellation operation, but also for transactions when the state of the object needs to be "rolled back" if the operation failed.
2) When the direct interface for obtaining the state of the object reveals details of the implementation and violates the encapsulation of the object.
Pros:
- Doesn't violate the encapsulation of the input object.
- Simplifies the structure of the input object. It doesn't need to keep a history of its state versions.Cons:
- Requires a lot of memory if clients create images too often.
- It can entail additional memory costs, if the objects storing the history don't release the resources occupied by the outdated images.
Let's look at an abstract example of how the Memento pattern can be implemented using Python:
class Memento(object): def __init__(self, state): self._state = state def get_saved_state(self): return self._state class Originator(object): _state = "" def set(self, state): print("Originator: Setting state to", state) self._state = state def save_to_memento(self): print("Originator: Saving to Memento.") return Memento(self._state) def restore_from_memento(self, memento): self._state = memento.get_saved_state() print("Originator: State after restoring from Memento:", self._state) saved_states = [] originator = Originator() originator.set("State1") originator.set("State2") saved_states.append(originator.save_to_memento()) originator.set("State3") saved_states.append(originator.save_to_memento()) originator.set("State4") originator.restore_from_memento(saved_states[0])
As a result, our object will return to the "State2" state, since it was the first one saved (the "State1" state hasn't been added to the list of the ones saved).
Bridge
Bridge is a structural design pattern that divides one or more classes into two separate hierarchies - abstraction and implementation, allowing them to be changed independently of each other.
Let's take a simple example. You have a class called Figure that has the subclasses - Circle and Square. You want to expand the shapes hierarchy in color, that is, to have the Red and Blue figures. But to combine all of this, you'll have to create 4 combinations of subclasses, like BlueCircle and RedSquare.
When adding new types of shapes and colors, the number of combinations will grow exponentially. For example, to introduce triangles into the program, you'll have to create two new subclasses of triangles for each color. After this, a new color will require the creation of three classes for all kinds of figures. It only gets worse. Visually, it can be featured as:
The root of the problem lies in the fact that we're trying to expand the classes of shapes in two independent planes - in form and color, which leads to the class tree growth.
The Bridge pattern suggests replacing inheritance with delegation. In order to do this, one of these "planes" needs to be established as a separate hierarchy and you'll have to refer to the object of this hierarchy, instead of storing its state and behavior within one class.
Thus, we can make Color a separate class with the Red and Blue subclasses. The Figure class gets a link to the Color object and can delegate the work to it, if necessary. This connection will become the bridge between Figure and Color. When adding new color classes, you won’t need to touch the shape classes, and vice versa. Schematically it will look like this:
Situations in which the Bridge pattern can help out:
1) When you want to divide a monolithic class that contains several different implementations of some kind of functionality (for example, if the class can work with the different database systems).
The larger the class, the harder it is to understand its code, and the more it drags out the development. In addition, changes made to one of the implementations lead to editing the entire class, which can cause random errors in the code. The bridge allows you to divide a monolithic class into several separate hierarchies. After that you can change their code independently from each other. This simplifies the work on the code and reduces the likelihood of making errors.
2) When a class needs to be expanded in two independent planes.
The bridge proposes to allocate one of these planes to a separate hierarchy of classes, storing a link to one of its objects in the original class.
3) When you want to be able to change the implementation during the execution of the program.
The bridge allows you to replace the implementation even during the program execution, since a particular implementation is not included in the abstraction class.
By the way, because of this point Bridge is often confused with Strategy. Note that this function is the lowest on the scale on importance for Bridge, since its main task is structural.
Pros:
- Allows to build platform-independent programs.
- Hides unnecessary or dangerous implementation details from the client code.
Cons:
- Complicates the program code due to the introduction of additional classes.
An example with geometric shapes and colors can be implemented using Python as follows:
class Color: def fill_color(self): pass class Shape: def __init__(self, color): self.color = color def color_it(self): pass class Rectangle(Shape): def __init__(self, color): super(Rectangle, self).__init__(color) def color_it(self): print("Rectangle filled with ", end="") self.color.fill_color() class Circle(Shape): def __init__(self, color): super(Circle, self).__init__(color) def color_it(self): print("Circle filled with ", end="") self.color.fill_color() class RedColor(Color): def fill_color(self): print("red color") class BlueColor(Color): def fill_color(self): print("blue color") if __name__ == '__main__': s1 = Rectangle(RedColor()) s1.color_it() s2 = Circle(BlueColor()) s2.color_it()
Conclusion
In this series of articles we've managed to get familiar with 6 different design patterns:
Abstract Factory and Strategy in the first part, Observer and Mediator in the second, Memento and Bridge in the third.
As you've noticed, not all patterns are equally useful and not all of them are equally widely applicable. Some of them are good for many situations, while others can manifest themselves only in special tasks that the developer faces not so often.
Thus, the design patterns can be compared to a useful tool, a hammer, for example, that can help out if used wisely, but can also do harm, if applied recklessly and negligently.
The following resources were used in this article:
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