CHAPTER 5 Factory Pattern
Quality means doing it right when no one is looking.
Henry Ford
In Chapter 3, you started thinking about writing your own game. To make sure you don t feel duped by a text-only game, let s take a moment and look at drawing something
on the screen. In this chapter, we will touch on the basics of graphics using Python. We will use the PyGame package as our weapon of choice. We will be creating factory classes. Factory classes define objects that take a certain set of parameters and use that to create objects of some other class. We will also define abstract factory classes that serve as the template used to build these factory classes.
In a virtual environment, you can use pip to install PyGame by using this command: pip install pygame
This should be fairly painless. Now, to get an actual window to work with:
graphic_base.py import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
Save graphic_base.py and run the file: python graphic_base.py
A blank window that is 400 pixels wide and 300 pixels high will pop up on your screen and immediately vanish again. Congratulations, you have created your first
61
' Wessel Badenhorst 2017
W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_4
CHAPTER 5 FACTORY PATTERN
screen. Obviously, we want the screen to stay up a little longer, so let s just add a sleep function to graphic_base.py.
graphic_base.py
import pygame
from time import sleep
pygame.init()
screen = pygame.display.set_mode((800, 600))
sleep(10)
From the time package (part of the standard library) we import the sleep function, which suspends execution for a given number of seconds. A ten-second sleep was added to the end of the script, which keeps the window open for ten seconds before the script completes execution and the window vanishes.
I was very excited when I created a window on screen the first time, but when I called my roommate over to show him my window, he was completely underwhelmed. I suggest you add something to the window before showing off your new creation. Extend graphic_base.py to add a square to the window.
graphic_base.py
import pygame import time
pygame.init()
screen = pygame.display.set_mode((800,600))
pygame.draw.rect(screen, (255, 0, 34), pygame.Rect(42, 15, 40, 32)) pygame.display.flip()
time.sleep(10)
The pygame.draw.rect function draws a rectangle to the screen variable that points
to your window. The second parameter is a tuple containing the color used to fill the shape, and, lastly, the pygame rectangle is passed in with the coordinates of the top left and bottom right corners. The three values you see in the color tuple make up what is known as the RGB value (for Red Green Blue), and each component is a value out of 255, which indicates the intensity of the component color in the final color mix.
If you leave out the pygame.display.flip(), no shape is displayed. This is because PyGame draws the screen on a memory buffer and then flips the whole image onto the active screen (which is what you see). Every time you update the display, you have to call pygame.display.flip() to have the changes show up on the screen.
Experiment with drawing different rectangles in many colors on screen.
The most basic concept in game programming is called the game loop. The idea works like this: the game checks for some input from the user, who does some calculation to update the state of the game, and then gives some feedback to the player. In our case, we will just be updating what the player sees on the screen, but you could include sound or haptic feedback. This happens over and over until the player quits. Every time the screen is updated, we run the pygame.display.flip() function to show the updated display to
the player.
The basic structure for a game will then be as follows:
Some initialization occurs, such as setting up the window and initial
position and color of the elements on the screen.
While the user does not quit the game, run the game loop. When the user quits, terminate the window.
In code, it could look something like this:
graphic_base.py import pygame
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
player_quits = False
while not player_quits:
for event in pygame.event.get(): if event.type == pygame.QUIT:
player_quits = True
pygame.display.flip()
63
CHAPTER FACTORY PATTERN
At the moment, this code does nothing but wait for the player to click the Close button on the window and then terminate execution. To make this more interactive, let s add a small square and have it move when the user presses one of the arrow keys.
For this, we will need to draw the square on the screen in its initial position, ready to react to arrow key events.
shape_game.py (see ch04_03.py) import pygame
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
x = 100 y = 100
player_quits = False
while not player_quits:
for event in pygame.event.get(): if event.type == pygame.QUIT:
player_quits = True
pressed = pygame.key.get_pressed() if pressed[pygame.K_UP]: y -= 4
if pressed[pygame.K_DOWN]: y += 4 if pressed[pygame.K_LEFT]: x -= 4
if pressed[pygame.K_RIGHT]: x += 4
screen.fill((0, 0, 0))
pygame.draw.rect(screen, (255, 255, 0), pygame.Rect(x, y, 20, 20))
pygame.display.flip()
Play around with the code for a bit to see if you can get the block to not move out of the boundaries of the window.
Now that your block can move around the screen, how about doing all of this for a circle? And then for a triangle. And now for a game character icon. . . You get the picture; suddenly, there is a lot of display code clogging up the game loop. What if we cleaned this up a bit using an object-oriented approach to the situation?
64
CHAPTER 5 FACTORY PATTERN
shape_game.py import pygame
class Shape(object):
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
raise NotImplementedError()
def move(self, direction):
if direction == ’up’: self.y -= 4
elif direction == ’down’:
self.y += 4
elif direction == ’left’:
self.x -= 4
elif direction == ’right’:
self.x += 4
class Square(Shape):
def draw(self):
pygame.draw.rect(
screen,
(255, 255, 0), pygame.Rect(self.x, self.y, 20, 20)
)
class Circle(Shape):
def draw(self): pygame.draw.circle(
screen,
(0, 255, 255), (selfx, self.y), 10
)
65
CHAPTER 5 FACTORY PATTERN
if __name__ == ’__main__’:
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
square = Square(100, 100) obj = square
player_quits = False
while not player_quits:
for event in pygame.event.get(): if event.type == pygame.QUIT:
player_quits = True
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: obj.move(’up’)
if pressed[pygame.K_DOWN]: obj.move(’down’) if pressed[pygame.K_LEFT]: obj.move(’left’) if pressed[pygame.K_RIGHT]: obj.move(’right’)
screen.fill((0, 0, 0)) obj.draw()
pygame.display.flip()
Since you now have the circle and square objects available, think about how you would go about altering the program so that when you press the C key the object on screen turns into a circle (if it is not currently one). Similarly, when you press the S key the shape should change to a square. See how much easier it is to use the objects than it is to handle all of this inside the game loop.
Tip Look at the keybindings for PyGame together with the code in this chapter.
There is still an improvement or two we could implement, such as abstracting things like move and draw that have to happen with each class so we do not have to keep track of what shape we are dealing with. We want to be able to refer to the shape in general and just tell it to draw itself without worrying about what shape it is (or if it is a shape, to begin with, and not some image or even a frame of animation).
Clearly, polymorphism is not a complete solution, since we would have to keep updating code whenever we create a new object, and in a large game this will happen in many places. The issue is the creation of the new types and not the use of these types.
Since you want to write better code, think about the following characteristics of good code as you try to come up with a better way of handling the expansion we want to add to our shapeshifter game.
Good code is
easy to maintain,
easy to update,
easy to extend, and
clear in what it is trying to accomplish.
Good code should make working on something you wrote a couple of weeks back as painless as possible. The last thing you want is to create these areas in your code you dread working on.
We want to be able to create objects through a common interface rather than spreading the creation code throughout the system. This localizes the code that needs to change when you update the types of shapes that can be created. Since adding new types is the most likely addition you will make to the system, it makes sense that this is one of the areas you have to look at first in terms of code improvement.
One way of creating a centralized system of object creation is by using the factory pattern. This pattern has two distinct approaches, and we will cover both, starting with the simpler factory method and then moving on to the abstract factory implementation. We will also look at how you could implement each of these in terms of our game skeleton.
Before we get into the weeds with the factory pattern, I want you to take note of the fact that there is one main difference between the prototype pattern and the factory pattern. The prototype pattern does not require sub-classing, but requires an initialize operation, whereas the factory pattern requires sub-classing but not initialization. Each of these has its own advantages and places where you should opt for one over the other, and over the course of this chapter this distinction should become clearer.
67
CHAPTER FACTORY PATTERN
When we want to call a method such that we pass in a string and get as a return value
a new object, we are essentially calling a factory method. The type of the object is determined by the string that is passed to the method.
This makes it easy to extend the code you write by allowing you to add functionality to your software, which is accomplished by adding a new class and extending the factory method to accept a new string and return the class you created.
Let s look at a simple implementation of the factory method.
shape_ factory.py import pygame
class Shape(object):
def __init__(self, x, y): self.x = x
self.y = y
def draw(self):
raise NotImplementedError()
def move(self, direction):
if direction == ’up’: self.y -= 4
elif direction == ’down’:
self.y += 4
elif direction == ’left’:
self.x -= 4
elif direction == ’right’:
self.x += 4
@staticmethod
def factory(type):
if type == "Circle":
return Circle(100, 100) if type == "Square":
return Square(100, 100)
68
CHAPTER FACTORY PATTERN
assert 0, "Bad shape requested: " + type
class Square(Shape):
def draw(self):
pygame.draw.rect(
screen,
(255, 255, 0), pygame.Rect(self.x, self.y, 20, 20)
)
class Circle(Shape):
def draw(self): pygame.draw.circle(
screen,
(0, 255, 255), (selfx, self.y), 10
)
if __name__ == ’__main__’:
window_dimensions = 800, 600
screen = pygame.display.set_mode(window_dimensions)
obj = Shape.factory("square") player_quits = False
while not player_quits:
for event in pygame.event.get(): if event.type == pygame.QUIT:
player_quits = True
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]: obj.move(’up’)
if pressed[pygame.K_DOWN]: obj.move(’down’) if pressed[pygame.K_LEFT]: obj.move(’left’) if pressed[pygame.K_RIGHT]: obj.move(’right’)
69
CHAPTER FACTORY PATTERN
screen.fill((0, 0, 0)) obj.draw()
pygame.display.flip()
How much easier would it be to adapt this piece of code above to change from a square to a circle, or to any other shape or image you might want?
Some advocates of the factory method suggest that all constructors should be private or protected since it should not matter to the user of the class whether a new object is created or an old one recycled. The idea is that you decouple the request for an object from its creation.
This idea should not be followed as dogma, but try it out in one of your own projects and see what benefits you derive from it. Once you are comfortable with using factory methods, and possibly factory patterns, you are free to use your own discretion in terms of the usefulness of the pattern in the project at hand.
Whenever you add to your game a new class that needs to be drawn on the screen, you simply need to change the factory() method.
What happens when we need different kinds of factories? Maybe you would like to add sound effect factories or environmental elements versus player characters factories? You want to be able to create different types of factory sub-classes from the same basic factory.
When you want to create a single interface to access an entire collection of factories, you can confidently reach for an abstract factory. Each abstract factory in the collection needs to implement a predefined interface, and each function on that interface returns another abstract type, as per the factory method pattern.
import abc
class AbstractFactory(object): __metaclass__ = abc.ABCMeta
@abc.abstractmethod def make_object(self):
return
70
CHAPTER FACTORY PATTERN
class CircleFactory(AbstractFactory): def make_object(self):
- do something
return Circle()
class SquareFactory(AbstractFactory): def make_object(self):
- do something
return Square()
Here, we used the built-in abc module, which lets you define an abstract base class. The abstract factory defines the blueprint for defining the concrete factories that then create circles and squares in this example.
Python is dynamically typed, so you do not need to define the common base class. If we were to make the code more pythonic, we would be looking at something like this:
class CircleFactory(AbstractFactory): def make_object(self):
- do something
return Circle()
class SquareFactory(AbstractFactory): def make_object(self):
- do something
return Square()
def draw_function(factory):
drawable = factory.make_object() drawable.draw()
def prepare_client():
squareFactory = SquareFactory() draw_function(squareFactory)
circleFactory = CircleFactory() draw_function(circleFactory)
71
CHAPTER FACTORY PATTERN
In the barebones game we have set up so far, you could imagine the factory generating objects that contain a play() method, which you could run in the game loop to play sounds, calculate moves, or draw shapes and images on the screen. Another popular use for abstract factories is to create factories for the GUI elements of different operating systems. All of them use the same basic functions, but the factory that gets created gets selected based on the operating system the program is running on.
Congratulations! You just leveled up your ability to write great code. Using abstract factories, you can write code that is easier to modify, and code that can be tested easily.
When developing software, you want to guard against the nagging feeling that you
should build something that will be able to cater to every possible future eventuality. Although it is good to think about the future of your software, it is often an exercise in futility to try to build a piece of software that is so generic that it will solve every possible future requirement. The simplest reason for this is that there is no way to envision
where your software will go. I m not saying you should be naive and not consider the immediate future of your code, but being able to evolve your code to incorporate new functionality is one of the most valuable skills you will learn as a software developer. There is always the temptation to throw out all the old code you have previously written and start from scratch to do it right. That is a dream, and as soon as you do it right you will learn something new that will make your code look less than pretty, and so you will have to redo everything again, never finishing anything.
The general principle is YAGNI; you will probably come across this acronym in your career as a software developer. What does it mean? You ain t gonna need it! It is the principle that you should write code that solves the current problem well, and only when needed alter it to solve subsequent problems.
This is why many software designs start out using the simpler factory method, and only once the developer discovers where more flexibility is needed does he evolve the program to use the abstract factory, prototype, or builder patterns.
72
CHAPTER FACTORY PATTERN
Add the functionality to switch between a circle, triangle, and square
via key press.
Try to implement an image object instead of just a shape.
Enhance your image object class to switch between separate images to be drawn when moving up, down, left, and right, respectively.
As a challenge, try to add animation to the image when moving.
'Practical Python Design Patterns' 카테고리의 다른 글
Adapter Pattern (0) | 2023.03.28 |
---|---|
Builder Pattern (0) | 2023.03.28 |
The Prototype Pattern (0) | 2023.03.28 |
The Singleton Pattern (0) | 2023.03.28 |
Before We Begin (0) | 2023.03.28 |
댓글