Design Patterns. Part 1
The programmers say that there are 3 types of "laziness":
- when you really don't want to proceed with the task and the start off is delayed for as long as possible (procrastination);
- the reluctance to waste time on the task that results in it being done extremely quickly and, often, not quite properly which therefore leads to it's low-quality;
- the same nasty task that you have no desire to work on, but a completely different approach to the matter - the maximal automation and templating approach to its solution, so that you really had to work only once, and then just use the previous developments in all similar situations.
Maybe the same reflections as was mentioned in the last point used to guide the famous "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in the process of writing the "Design Patterns: Elements of Reusable Object-Oriented Software". This book, published almost a quarter of a century ago (in 1994), describes 23 classic design patterns which are still being actively used.
In this article you'll get acquainted with the two of them: Abstract Factory and Strategy. In addition to the theoretical description of the patterns and examples of the code for their implementation, at the end of each section there will also be a link to a challenge where you can in practice apply the gained knowledge.
An abstract factory is a generative design pattern that allows you to create families of related objects without getting attached to specific classes of created objects. The pattern is being implemented by creating an abstract class (for example - Factory), which is represented as an interface for creating system components. Then the classes that implement this interface are being written.
Suppose you need to develop a prototype furniture store, which will sell chairs, tables and sofas (a family of related products), and in addition they'll have several varieties: the Victorian style, modern and futuristic. You need such a way of creating the product objects so that they are combined with other products of the same family. This is important because the customers are getting frustrated when they receive not matching furniture. Furthermore, you don't want to make changes to existing code when adding new products or families to the program. The suppliers often update their catalogs, and you wouldn't want to change the already written code each time you get new furniture models.
To begin with, the Abstract Factory pattern proposes to highlight the common interfaces for individual products that make up the families. This way all the different kinds of chairs will get a common "Chair" interface, all the sofas will implement the "Sofa" interface and so on.
Next, you create an abstract factory - a common interface that contains methods for creating all the family products (for example, create_chair, create_sofa, and create_table). These operations must return the abstract types of products, represented by the interfaces we've previously highlited - Chair, Sofa, and Table.
What about the previously mentioned product variations? For each variation of the product family, we must create their own factory by implementing an abstract interface. The factories are creating the products of one variation. For example, ModernFactory will return only 'modern chair', 'modern sofa', and 'modern table'. The client code should work with both factories and products only through their common interfaces. This will allow to use any type of factory and produce any products.
And now let's see how the situation described above can be realized in practice.
class AbstractFactory(object): def create_chair(self): raise NotImplementedError() def create_sofa(self): raise NotImplementedError() def create_table(self): raise NotImplementedError() class Chair(object): def __init__(self, name): self._name = name def __str__(self): return self._name class Sofa(object): def __init__(self, name): self._name = name def __str__(self): return self._name class Table(object): def __init__(self, name): self._name = name def __str__(self): return self._name class VictorianFactory(AbstractFactory): def create_chair(self): return Chair('victorian chair') def create_sofa(self): return Sofa('victorian sofa') def create_table(self): return Table('victorian table') class ModernFactory(AbstractFactory): def create_chair(self): return Chair('modern chair') def create_sofa(self): return Sofa('modern sofa') def create_table(self): return Table('modern table') class FuturisticFactory(AbstractFactory): def create_chair(self): return Chair('futuristic chair') def create_sofa(self): return Sofa('futuristic sofa') def create_table(self): return Table('futuristic table')
Now, to look at the result of the work, you can choose one copy of each factory and check what products they produce.
factory_1 = VictorianFactory() factory_2 = ModernFactory() factory_3 = FuturisticFactory() '' Note that the way the factory selection is implemented in this example, you could pass any argument to the third factory - not necessarily the word 'Futuristic' print(factory_1.create_chair()) print(factory_1.create_sofa()) print(factory_1.create_table()) print(factory_2.create_chair()) print(factory_2.create_sofa()) print(factory_2.create_table()) print(factory_3.create_chair()) print(factory_3.create_sofa()) print(factory_3.create_table())
As a result, we get exactly what we've expected:
victorian chair victorian sofa victorian table modern chair modern sofa modern table futuristic chair futuristic sofa futuristic table
In the future, if the store plans to sell chairs, tables and sofas of new styles, the changes in the code will be minimal - it'll be enough to add a new factory for a particular style and add another option to the style selection function.
This pattern, like all the others, has its advantages and disadvantages.
- isolates specific classes;
- simplifies the replacement of the product families;
- guarantees the products' compatibility.
- it's difficult to add support for the new kinds of products.
Having dealt with theory, I propose to proceed to practice. This pattern is perfect for solving the "Army Units" mission.
A strategy is a behavioral design pattern that defines a family of similar algorithms and places each of them in its own class, after which the algorithms can be interchanged right during the execution of the program.
Imagine that you've decided to write a navigation app for travelers. It should show a beautiful and convenient map, allowing you to easily navigate in an unfamiliar city. One of the most popular functions is the search and mapping of routes. The user has to be able to set a starting point and a destination, and the navigator will have to pave the optimal way.
The first version of your navigator could map the route only along the roads, so it was perfect for traveling by car. But, obviously, not everyone is going on vacation by car. Therefore, the next thing you did was adjust the navigator by adding the mapping of walking routes. After a while it turned out that some people prefer to travel around the city by public transport. That's why you've added this option of route mapping as well. But there’s more. In the short term, you'd like to add route mapping along the bike lanes. And in a more distant future - interesting routes for sightseeing.
While the navigator's popularity was not a problem, the technical part raised questions and the occasional headache. With each new algorithm, the navigator's main class code doubled. In such a large class it became quite difficult to navigate. Any changes in the search algorithms, whether it's fixing the bugs or adding a new algorithm, affects the main class. This increased the risk of making a mistake, accidentally impacting the rest of the running code. Moreover, the teamwork with other programmers, which you've hired after the successful release of the navigator, was becoming harder. Your changes often affected the same code, creating conflicts that required additional time for being resolved.
The Strategy pattern proposes to define a family of similar algorithms, which often change or expand, and put them into their own classes, called strategies. Instead of the original class executing a certain algorithm, it'll play the role of a context, referring to one of the strategies and delegating it the work performance. To change the algorithm, you'll just need to substitute an object-strategy in the context. It's important for all of the strategies to have a common interface. By using this interface, the context will be independent of the specific strategy classes. On the other hand, you can modify and add new types of algorithms without touching the context code.
In our example, each route searching algorithm will move to its own class. In these classes only one method will be defined, and it'll take as parameters the coordinates of the beginning and the end of the route, and return an array of route points. Although each class will map the route in its own way, for the navigator this won't make any difference, since its work consists only of drawing the route. It's enough for the navigator to submit data about the beginning and end of the route to the strategy and get an array of route points in a defined format. The navigator's class will have a method for setting the strategy, allowing you to change the route search strategy instantly. This method will come in handy for the navigator's client code, for the route type switches in the user interface, for example.
Since the working implementation of such an example can be a very difficult (and quite hefty) activity, let's look at a practical example of the Strategy pattern for a simpler situation. Suppose you need to create a program that would help with opening files of different formats, and also display a message stating that the user doesn't have a suitable program for files with an unknown extension (the one that's not included in the list of all of the available ones). By using the pattern, we'll get the following:
class ImageOpener(object): @staticmethod def open(filename): raise NotImplementedError() class PNGImageOpener(ImageOpener): @staticmethod def open(filename): print('PNG: open with Paint') class JPEGImageOpener(ImageOpener): @staticmethod def open(filename): print('JPG/JPEG: open with ImageViewer') class SVGImageOpener(ImageOpener): @staticmethod def open(filename): print('SVG: open with Illustrator') class UnknownImageOpener(ImageOpener): @staticmethod def open(filename): print("You don't have program for %s extension" % filename.split('.')[-1].upper()) class Image(object): @classmethod def open_file(cls, filename): ext = filename.split('.')[-1] if ext == 'png': opener = PNGImageOpener elif ext in ('jpg', 'jpeg'): opener = JPEGImageOpener elif ext == 'svg': opener = SVGImageOpener else: opener = UnknownImageOpener byterange = opener.open(filename) return cls(byterange, filename) def __init__(self, byterange, filename): self._byterange = byterange self._filename = filename
Now, having checked the work of the program by using the files of four different formats
Image.open_file('picture.png') Image.open_file('picture.jpg') Image.open_file('picture.svg') Image.open_file('picture.raw')
PNG: open with Paint
JPG/JPEG: open with ImageViewer
SVG: open with Illustrator
You don't have the program for RAW extension
In the future, if the new programs that can work with other extensions (not specified in the current implementation of our program) are installed on the computer, you'll need to make minimal changes - add a separate class for the new strategy and add another.4/ alternative option to the Image class that is engaged in the choice of strategy.
Despite its efficiency, the Strategy pattern has both positive and negative sides.
- "hot" algorithm replacement on the fly;
- the algorithms' code and data isolation from other classes.
- complicates the program due to the additional classes.
After learning how this pattern works, you can consolidate knowledge by solving the "Geometry Figures" mission.
Finally, I would like to quote the words of the two authors of the book mentioned at the beginning of the article - "Design Patterns". Erich Gamma: "Patterns are the tools, not dogmas. Adapt them for your tasks". Ralph Johnson: "Choose simple solutions and don't get carried away. If there is a simple solution without a pattern usage - choose it".
For this article the following sources were used:
- Object Oriented Design
- Design Patterns
- Design Patterns in Python (GitHub repo)
- Refactoring Guru
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