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.
Let's start with the kid's game. It is called FrogWorld. The main hero is a frog who enjoys eating bugs. Every hero needs a good name, and in our case, the name is given by the user at runtime. The interact_with() method is used to describe the interaction of the frog with an obstacle (for example, a bug, puzzle, and other frogs) as follows:
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)
There can be many different kinds of obstacles but for our example, an obstacle can only be a bug. When the frog encounters a bug, only one action is supported. It eats it:
class Bug:
def __str__(self):
return 'a bug'
def action(self):
return 'eats it'
The FrogWorld class is an abstract factory. Its main responsibilities are creating the main character and the obstacle(s) in the game. Keeping the creation methods separate and their names generic (for example, make_character() and make_obstacle()) allows us to change the active factory (and therefore the active game) dynamically without any code changes. In a statically typed language, the abstract factory would be an abstract class/interface with empty methods, but in Python, this is not required because the types are checked at runtime (j.mp/ginstromdp). The code is as follows:
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()
The WizardWorld game is similar. The only difference is that the wizard battles against monsters such as orks instead of eating bugs!
Here is the definition of the Wizard class, which is similar to the Frog one:
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)
Then, the definition of the Ork class is as follows:
class Ork:
def __str__(self):
return 'an evil ork'
def action(self):
return 'kills it'
We also need to define the WizardWorld class, similar to the FrogWorld one that we have discussed; the obstacle, in this case, is an Ork instance:
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()
The GameEnvironment class is the main entry point of our game. It accepts the factory as an input and uses it to create the world of the game. The play() method initiates the interaction between the created hero and the obstacle, as follows:
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)
The validate_age() function prompts the user to give a valid age. If the age is not valid, it returns a tuple with the first element set to False. If the age is fine, the first element of the tuple is set to True and that's the case where we actually care about the second element of the tuple, which is the age given by the user, as follows:
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)
Last but not least comes the main() function. It asks for the user's name and age, and decides which game should be played, given the age of the user, as follows:
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()
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.