Design Patterns: Elements of Reusable Object-Oriented Software
by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
객체 지향 프로그래밍 에서 사용되는 디자인 패턴에는 여러 범주가 있으며 , 해결하는 문제의 유형 및/또는 빌드하는데 도움이 되는 솔루션의 유형에 따라 다릅니다. 그들의 저서에서 The Gang of Four는 23가지 디자인 패턴을 제시하며 creational, structural, behavioral의 세 가지 범주로 나뉩니다 .
These patterns deal with different aspects of object creation. Their goal is to provide better alternatives for situations where direct object creation, which in Python happens within the __init__() function, is not convenient.
We will start with the first creational design pattern from the Gang of Four book: the factory design pattern. In the factory design pattern, a client (meaning client code) asks for an object without knowing where the object is coming from (that is, which class is used to generate it). The idea behind a factory is to simplify the object creation process. It is easier to track which objects are created if this is done through a central function, compared to letting a client create objects using a direct class instantiation. A factory reduces the complexity of maintaining an application by decoupling the code that creates an object from the code that uses it.
Factories typically come in two forms—the factory method, which is a method (or simply a function for a Python developer) that returns a different object per input parameter, and the abstract factory, which is a group of factory methods used to create a family of related objects.
In this chapter, we will discuss:
- The factory method
- The abstract factory
factory method는 개체 생성 작업을 처리하기 위해 작성된 단일 함수를 기반으로 합니다. 원하는 정보를 제공하는 매개 변수를 전달하여 실행하면 원하는 개체가 생성됩니다.
흥미롭게도 factory method를 사용할 때 결과 객체가 어떻게 구현되고 어디에서 왔는지에 대한 세부 정보를 알 필요가 없습니다.
An example of the factory method pattern used in reality is in the context of a plastic toy construction kit. The molding material used to construct plastic toys is the same, but different toys (different figures or shapes) can be produced using the right plastic molds. This is like having a factory method in which the input is the name of the toy that we want (for example, duck or car) and the output (after the molding) is the plastic toy that was requested.
In the software world, the Django web framework uses the factory method pattern for creating the fields of a web form. The forms module, included in Django, supports the creation of different kinds of fields (for example, CharField, EmailField, and so on). And parts of their behavior can be customized using attributes such as max_length or required (j.mp/djangofac). As an illustration, there follows a snippet that a developer could write for a form (the PersonForm form containing the fields name and birth_date) as part of a Django application's UI code:
from django import forms
class PersonForm(forms.Form):
name = forms.CharField(max_length=100)
birth_date = forms.DateField(required=False)
If you realize that you cannot track the objects created by your application because the code that creates them is in many different places instead of in a single function/method, you should consider using the factory method pattern. The factory method centralizes object creation and tracking your objects becomes much easier. Note that it is absolutely fine to create more than one factory method, and this is how it is typically done in practice. Each factory method logically groups the creation of objects that have similarities. For example, one factory method might be responsible for connecting you to different databases (MySQL, SQLite), another factory method might be responsible for creating the geometrical object that you request (circle, triangle), and so on.
The factory method is also useful when you want to decouple object creation from object usage. We are not coupled/bound to a specific class when creating an object; we just provide partial information about what we want by calling a function. This means that introducing changes to the function is easy and does not require any changes to the code that uses it.
Another use case worth mentioning is related to improving the performance and memory usage of an application. A factory method can improve the performance and memory usage by creating new objects only if it is absolutely necessary. When we create objects using a direct class instantiation, extra memory is allocated every time a new object is created (unless the class uses caching internally, which is usually not the case). We can see that in practice in the following code (file id.py), it creates two instances of the same class, A, and uses the id() function to compare their memory addresses. The addresses are also printed in the output so that we can inspect them. The fact that the memory addresses are different means that two distinct objects are created as follows:
class A:
pass
if __name__ == '__main__':
a = A()
b = A()
print(id(a) == id(b))
print(a, b)
Executing the python id.py command on my computer results in the following output:
False
<__main__.A object at 0x7f5771de8f60> <__main__.A object at 0x7f5771df2208>
Note that the addresses that you see if you execute the file are not the same as the ones I see because they depend on the current memory layout and allocation. But the result must be the same—the two addresses should be different. There's one exception that happens if you write and execute the code in the Python Read-Eval-Print Loop (REPL)—or simply put, the interactive prompt—but that's a REPL-specific optimization which does not happen normally.
Implementing the factory method
Data comes in many forms. There are two main file categories for storing/retrieving data: human-readable files and binary files. Examples of human-readable files are XML, RSS/Atom, YAML, and JSON. Examples of binary files are the .sq3 file format used by SQLite and the .mp3 audio file format used to listen to music.
In this example, we will focus on two popular human-readable formats—XML and JSON. Although human-readable files are generally slower to parse than binary files, they make data exchange, inspection, and modification much easier. For this reason, it is advised that you work with human-readable files, unless there are other restrictions that do not allow it (mainly unacceptable performance and proprietary binary formats).
In this case, we have some input data stored in an XML and a JSON file, and we want to parse them and retrieve some information. At the same time, we want to centralize the client's connection to those (and all future) external services. We will use the factory method to solve this problem. The example focuses only on XML and JSON, but adding support for more services should be straightforward.
First, let's take a look at the data files.
The JSON file, movies.json, is an example (found on GitHub) of a dataset containing information about American movies (title, year, director name, genre, and so on). This is actually a big file but here is an extract, simplified for better readability, to show how its content is organized:
[
{"title":"After Dark in Central Park",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Boarding School Girls' Pajama Parade",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Buffalo Bill's Wild West Parad",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Caught",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Clowns Spinning Hats",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Capture of Boer Battery by British",
"year":1900,
"director":"James H. White", "cast":null, "genre":"Short documentary"},
{"title":"The Enchanted Drawing",
"year":1900,
"director":"J. Stuart Blackton", "cast":null,"genre":null},
{"title":"Family Troubles",
"year":1900,
"director":null, "cast":null, "genre":null},
{"title":"Feeding Sea Lions",
"year":1900,
"director":null, "cast":"Paul Boyton", "genre":null}
]
The XML file, person.xml, is based on a Wikipedia example (j.mp/wikijson), and contains information about individuals (firstName, lastName, gender, and so on) as follows:
<persons>
<person>
<firstName>John</firstName>
<lastName>Smith</lastName>
<age>25</age>
<address>
<streetAddress>21 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="fax">646 555-4567</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
<person>
<firstName>Jimy</firstName>
<lastName>Liar</lastName>
<age>19</age>
<address>
<streetAddress>18 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
<person>
<firstName>Patty</firstName>
<lastName>Liar</lastName>
<age>20</age>
<address>
<streetAddress>18 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="mobile">001 452-8819</phoneNumber>
</phoneNumbers>
<gender>
<type>female</type>
</gender>
</person>
</persons>
1. We start with the enclosing tag of the persons XML container:
<persons>
2. Then, an XML element representing a person's data code is presented as follows:
<person>
<firstName>John</firstName>
<lastName>Smith</lastName>
<age>25</age>
<address>
<streetAddress>21 2nd Street</streetAddress>
<city>New York</city>
<state>NY</state>
<postalCode>10021</postalCode>
</address>
<phoneNumbers>
<phoneNumber type="home">212 555-1234</phoneNumber>
<phoneNumber type="fax">646 555-4567</phoneNumber>
</phoneNumbers>
<gender>
<type>male</type>
</gender>
</person>
3. Finally, we close the XML container:
</persons>
4. code
import json
import xml.etree.ElementTree as etree
class JSONDataExtractor:
def __init__(self, filepath):
self.data = dict()
with open(filepath, mode='r', encoding='utf-8') as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
class XMLDataExtractor:
def __init__(self, filepath):
self.tree = etree.parse(filepath)
@property
def parsed_data(self):
return self.tree
def dataextraction_factory(filepath):
if filepath.endswith('json'):
extractor = JSONDataExtractor
elif filepath.endswith('xml'):
extractor = XMLDataExtractor
else:
raise ValueError('Cannot extract data from {}'.format(filepath))
return extractor(filepath)
def extract_data_from(filepath):
factory_obj = None
try:
factory_obj = dataextraction_factory(filepath)
except ValueError as e:
print(e)
return factory_obj
def main():
sqlite_factory = extract_data_from('data/person.sq3')
print()
json_factory = extract_data_from('data/movies.json')
json_data = json_factory.parsed_data
print(f'Found: {len(json_data)} movies')
for movie in json_data:
print(f"Title: {movie['title']}")
year = movie['year']
if year:
print(f"Year: {year}")
director = movie['director']
if director:
print(f"Director: {director}")
genre = movie['genre']
if genre:
print(f"Genre: {genre}")
print()
xml_factory = extract_data_from('data/person.xml')
xml_data = xml_factory.parsed_data
liars = xml_data.findall(f".//person[lastName='Liar']")
print(f'found: {len(liars)} persons')
for liar in liars:
firstname = liar.find('firstName').text
print(f'first name: {firstname}')
lastname = liar.find('lastName').text
print(f'last name: {lastname}')
[print(f"phone number ({p.attrib['type']}):", p.text)
for p in liar.find('phoneNumbers')]
print()
print()
if __name__ == '__main__':
main()
Here is the summary of the implementation (you can find the code in the factory_method.py file):
- We start by importing the modules we need (json and ElementTree).
- We define the JSON data extractor class (JSONDataExtractor).
- We define the XML data extractor class (XMLDataExtractor).
- We add the factory function, dataextraction_factory(), for getting the right data extractor class.
- We also add our wrapper for handling exceptions, the extract_data_from() function.
- Finally, we have the main() function, followed by Python's conventional trick for calling it when invoking this file from the command line. The following are the aspects of the main function:
- We try to extract data from an SQL file (data/person.sq3), to show how the exception is handled
- We extract data from a JSON file and parse the result
- We extract data from an XML file and parse the result
The following is the type of output (for the different cases) you will get by calling the python factory_method.py command.
First, there is an exception message when trying to access an SQLite (.sq3) file:
Then, we get the following result from processing the movies file (JSON):
Finally, we get this result from processing the person XML file to find the people whose last name is Liar:
Notice that although JSONDataExtractor and XMLDataExtractor have the same interfaces, what is returned by parsed_data() is not handled in a uniform way. Different Python code must be used to work with each data extractor. Although it would be nice to be able to use the same code for all extractors, this is not realistic for the most part, unless we use some kind of common mapping for the data, which is very often provided by external data providers. Assuming that you can use exactly the same code for handling the XML and JSON files, what changes are required to support a third format, for example, SQLite? Find an SQLite file, or create your own and try it.
The abstract factory design pattern is a generalization of the factory method. Basically, an abstract factory is a (logical) group of factory methods, where each factory method is responsible for generating a different kind of object.
We are going to discuss some examples, use cases, and a possible implementation.
The abstract factory is used in car manufacturing. The same machinery is used for stamping the parts (doors, panels, hoods, fenders, and mirrors) of different car models. The model that is assembled by the machinery is configurable and easy to change at any time.
In the software category, the factory_boy (https:/ / github.com/ FactoryBoy/factory_ boy) package provides an abstract factory implementation for creating Django models in tests. It is used for creating instances of models that support test-specific attributes. This is important because, this way, your tests become readable and you avoid sharing unnecessary code.
Since the abstract factory pattern is a generalization of the factory method pattern, it offers the same benefits, it makes tracking an object creation easier, it decouples object creation from object usage, and it gives us the potential to improve the memory usage and performance of our application.
But, a question is raised: How do we know when to use the factory method versus using an abstract factory? The answer is that we usually start with the factory method which is simpler. If we find out that our application requires many factory methods, which it makes sense to combine to create a family of objects, we end up with an abstract factory.
A benefit of the abstract factory that is usually not very visible from a user's point of view when using the factory method is that it gives us the ability to modify the behavior of our application dynamically (at runtime) by changing the active factory method. The classic example is the ability to change the look and feel of an application (for example, Apple-like, Windows-like, and so on) for the user while the application is in use, without the need to terminate it and start it again.
Implementing the abstract factory pattern
To demonstrate the abstract factory pattern, I will reuse one of my favorite examples, included in the book, Python 3 Patterns, Recipes and Idioms, by Bruce Eckel. Imagine that we are creating a game or we want to include a mini-game as part of our application to entertain our users. We want to include at least two games, one for children and one for adults. We will decide which game to create and launch at runtime, based on user input. An abstract factory takes care of the game creation part.
# Frog game
class Frog:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
act = obstacle.action()
msg = f'{self} the Frog encounters {obstacle} and {act}!'
print(msg)
class Bug:
def __str__(self):
return 'a bug'
def action(self):
return 'eats it'
class FrogWorld:
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return '\n\n\t------ Frog World -------'
def make_character(self):
return Frog(self.player_name)
def make_obstacle(self):
return Bug()
# Wizard game
class Wizard:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
act = obstacle.action()
msg = f'{self} the Wizard battles against {obstacle} and {act}!'
print(msg)
class Ork:
def __str__(self):
return 'an evil ork'
def action(self):
return 'kills it'
class WizardWorld:
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return '\n\n\t------ Wizard World -------'
def make_character(self):
return Wizard(self.player_name)
def make_obstacle(self):
return Ork()
# Game environment
class GameEnvironment:
def __init__(self, factory):
self.hero = factory.make_character()
self.obstacle = factory.make_obstacle()
def play(self):
self.hero.interact_with(self.obstacle)
def validate_age(name):
try:
age = input(f'Welcome {name}. How old are you? ')
age = int(age)
except ValueError as err:
print(f"Age {age} is invalid, please try again...")
return (False, age)
return (True, age)
def main():
name = input("Hello. What's your name? ")
valid_input = False
while not valid_input:
valid_input, age = validate_age(name)
game = FrogWorld if age < 18 else WizardWorld
environment = GameEnvironment(game(name))
environment.play()
if __name__ == '__main__':
main()
The summary for the implementation we just discussed (see the complete code in the abstract_factory.py file) is as follows:
- We define the Frog and Bug classes for the FrogWorld game.
- We add the FrogWorld class, where we use our Frog and Bug classes.
- We define the Wizard and Ork classes for the WizardWorld game.
- We add the WizardWorld class, where we use our Wizard and Ork classes.
- We define the GameEnvironment class.
- We add the validate_age() function.
- Finally, we have the main() function, followed by the conventional trick for calling it. The following are the aspects of this function:
- We get the user's input for name and age
- We decide which game class to use based on the user's age
- We instantiate the right game class, and then the GameEnvironment class
- We call .play() on the environment object to play the game
Let's call this program using the python abstract_factory.py command, and see some sample output.
The sample output for a teenager is as follows:
The sample output for an adult is as follows:
Try extending the game to make it more complete. You can go as far as you want; create many obstacles, many enemies, and whatever else you like.
In this chapter, we have seen how to use the factory method and the abstract factory design patterns. Both patterns are used when we want to track object creation, decouple object creation from object usage, or even improve the performance and resource usage of an application. Improving the performance was not demonstrated in this chapter. You might consider trying it as a good exercise.
The factory method design pattern is implemented as a single function that doesn't belong to any class and is responsible for the creation of a single kind of object (a shape, a connection point, and so on). We saw how the factory method relates to toy construction, mentioned how it is used by Django for creating different form fields, and discussed other possible use cases for it. As an example, we implemented a factory method that provided access to the XML and JSON files.
The abstract factory design pattern is implemented as a number of factory methods that belong to a single class and are used to create a family of related objects (the parts of a car, the environment of a game, and so forth). We mentioned how the abstract factory is related to car manufacturing, how the django_factory package for Django makes use of it to
create clean tests, and then we covered its common use cases. Our implementation example of the abstract factory is a mini-game that shows how we can use many related factories in a single class.
In the next chapter, we will discuss the builder pattern, which is another creational pattern that can be used for fine-tuning the creation of complex objects.
'Mastering Python Design Patterns' 카테고리의 다른 글
The Decorator Pattern (0) | 2023.03.22 |
---|---|
The Adapter Pattern (0) | 2023.03.22 |
Other Creational Patterns (0) | 2023.03.22 |
The Builder Pattern (0) | 2023.03.22 |
Index (0) | 2023.03.22 |
댓글