본문 바로가기
Practical Python Design Patterns

Factory Pattern

by 자동매매 2023. 3. 28.

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.

Getting Started

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.

62
CHAPTER 5 FACTORY PATTERN

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 Game Loop

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).

66
CHAPTER FACTORY PATTERN

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

The Factory Method

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.

The Abstract 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.

Summary

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

Exercises

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.

73

'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

댓글