The Facade Pattern¶
Before you can write automated tests, you need to make sure that your code is testable. It is not a great idea to test any function or class in your program, because that makes the program harder to modify in the future. Whatever you test, you want to be stable and not change very often. Testable code means that you need to define an interface you are testing against.
Also, some things are harder to test than others, graphics and keyboard input for instance. We won’t test them for now. Instead, we want to make the code more testable by separating the graphics engine and game logic.
The Design¶
In the Facade Pattern, you define a single class
that serves as the interface to an entire subsysten.
We will define such a Facade class for the game logic named DungeonExplorer:
class DungeonExplorer(BaseModel):
player: Player
level: Level
def get_objects() -> list[DungeonObject]:
"""Returns everything in the dungeon to be used by a graphics engine"
...
def execute_command(cmd: str) -> None:
"""Performs a player action, such as 'left', 'right', 'jump', 'fireball'"""
...
Note that the attributes player and level of the game might change in the future.
We will treat them as private.
All the communication should happen through the two methods.
In the following exercise, you refactor the code to use the Facade pattern.
Step 1: Separate Modules¶
Split the existing code into two Python modules graphics_engine.py and game_logic.py.
For each paragraph of code decide, which of the two modules it belongs to.
Step 2: Implement the Facade class¶
Copy the skeleton code for the DungeonExplorer class to game_logic.py.
Leave the methods empty for now.
Step 3: Define a class for data exchange¶
In the get_objects() method, we use the type DungeonObject to send everything that
should be drawn to the graphics engine.
This includes walls, the player for now, but will include more stuff later.
Define it as follows:
class DungeonObject(BaseModel):
position: Position
name: str
Example objects could be:
DungeonObject(Position(x=1, y=1), "wall")
DungeonObject(Position(x=4, y=4), "player")
Note
This is really a very straightforward approach to send the information for drawing. In fact, it makes a couple of things very hard, e.g. animation. This is an example of a design decision: we choose that we do not want animations in the game. Our design makes adding them more expensive.
Step 4: Implement the get_objects method¶
Implement the get_objects() method from scratch.
Create a list of the player and all walls as a list of DungeonObject.
Step 5: Implement the execute_command method¶
Move the code you have for handling keyboard input into the execute_command() method.
Replace the keys by explicit commands like “left”, “right” etc.
The idea behind that is that we do not want the game logic to know anything about
which key you press to walk right. This belongs to the user interface.
Step 6: Import the Facade class¶
In the module graphics_engine.py, import the Facade class DungeonExplorer.
The only things the user interface needs to know about are the Facade class and
the data exchange class DungeonObject (although we do not have to import the latter).
Create an instance of it.
Step 7: Adjust the graphics engine¶
Make sure the graphics engine does the following:
it calls
DungeonExplorer.get_objectsin the draw function.it does not access the level or player attributes in the draw function.
it translates the keys to commands
it calls the
DungeonExplorer.execute_commandmethod.