Factory Patterns, SOLID Principles, and Error Handling the Tic Tac Toe Example
Exploring Software Development Modelling
Many people may think that Software Development is just all about coding. Surprisingly, coding is just a small part of it. In reality, developers spend most of their time planning and designing the system.
But how should we design our systems? Since each application has unique functionality and structure, there is no standard answer to this question. However, we may try looking at some real-life examples and use those takeaways to help model our software.
“Modelling Real Life Behaviours helps with Software Development”
Applying Factory Pattern
A daily example could be modeling a Tic Tac Toe game.
Let’s imagine the game in real life and try to model the behavior. To play a Tic Tac Toe game, we need 2 players, and a game interface of a 3x3 Grid which contains 9 boxes.These could be modelled as our classes to build the game.
The Box
class represents a box in the tic tac toe game. It has a getter and setter method to the box value.
class Box: def __init__(self, value: str = " ") -> None:
self.value = value def set_value(self, value) -> None:
self.value = value def get_value(self) -> str:
return self.value
The Grid
class is the grid in the game. The most common one would be a 3x3 grid. It contains 9 boxes and also some methods such as display()
, set_box_value()
, grid_win()
, etc…
import box
from box.box import Boxclass Grid: def __init__(self) -> None:
self.grid = [Box(i+1, str(i+1)) for i in range(9)] def display(self) -> str:
... # display the grid def set_box_value(self, box_num: int, value: str) -> None:
self.grid[box_num - 1].set_value(value) def grid_win(self) -> bool:
... # Check the grid is win or not
The Player
class will be the players of the game. It takes a Grid
as an argument and players will take turns to play the game and set the box to their own value.
from grid.grid import Gridclass Player(): def __init__(self, value: str, grid: Grid) -> None:
self.value = value
self.grid = grid def get_box_input(self) -> int:
... # Get and validate user input
def take_turn(self, next_player: Player) -> Player | None:
box_num = self.get_box_input()
self.grid.set_box_value(box_num, self.value) if self.grid.grid_full() or self.grid.grid_win():
return None
return next_player
Congratulation! You have now modeled a simple Tic Tac Toe game. If you want to extend the example even further, since there will be different kinds of grid such as 4x4 Grid
, 5x5 Grid
, you may consider using an interface for the Grid
with a design pattern, the Factory Pattern.
We first change the Grid
to an interface.
from abc import ABC, abstractmethodclass Grid(ABC): def __init__(self) -> None:
super().__init__() @abstractmethod
def display(self) -> str:
pass @abstractmethod
def set_box_value(self, box_num: int, value: str) -> None:
pass @abstractmethod
def grid_win(self) -> bool:
pass
And now, the 3x3 Grid
will implement the interface.
from box.box import Box
from grid.grid import Gridclass ThreeThreeGrid(Grid):
ROWS = 3
COLS = 3
NUM_BOX = ROWS * COLS def __init__(self) -> None:
super().__init__()
self.grid = [Box(i+1, str(i+1)) for i in range(self.NUM_BOX)] def display(self) -> str:
... # display the 3x3 grid def set_box_value(self, box_num: int, value: str) -> None:
self.grid[box_num - 1].set_value(value) def grid_win(self) -> bool:
... # Check the 3x3 grid is win or not
The factory pattern will return the corresponding Grid
object based on the grid size that the user input.
from grid.grid import Grid
from grid.three_three import ThreeThreeGridclass GridFactory: def create_grid(self, grid_size: int) -> Grid:
if grid_size == 3:
return ThreeThreeGrid()
elif grid_size == 4:
... # return a 4x4 Grid
else:
print("Grid is not supported!")
The Player
class will now depends on the Grid
interface instead of the concrete 3x3 Grid object. This pattern echoes the dependency inversion principle in the SOLID principle which will be introduced soon.
A simple real life example is a good start to learning about modeling more complicated software applications. It serves as a guide to better design and plan components, which can help us to visualise the connections between different classes and have a clear image for the whole system.
Introducing SOLID Principles
Imagine building a system on a large scale. You won’t just write your code in a single main file, because it will be difficult to reuse and read in the future. Instead, we can follow some rules and best practices to structure our code.
“Developers write codes that are understandable, flexible and maintainable.”
One of the best practices is following the SOLID principle introduced by Robert C. Martin(also known as Uncle Bob).
- Single-Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
The Box
, Grid
and Player
classes follow the Single-Reponsibility Principle as each of them carry out only one responsibility.
The Grid
interface design is based on the Open-Closed Principle, because when I want to play on a 5x5 Grid, I just need to create a FiveFiveGrid
class which implements the Grid
interface without modifying the existing code.
The Player
class is dependents on the abstraction of the Grid
interface, but not directly to the concrete grid object, demonstrating the dependency inversion principle.
Applying these principles can help you structure your code, making your code more understandable and maintainable. It will save you a lot of time in the future to reuse the code.
Finishing up with Error Handling
Another important thing that I have learned for software development modeling is error handling.
Let’s take a closer look at the GridFactory
in the Tic Tac Toe example. Currently I only have a ThreeThreeGrid
class for users to play. When users ask for a grid size larger than 3, we need to raise an error since we don’t have such Grid.
from grid.grid import Grid
from grid.three_three import ThreeThreeGrid
from grid.error import UnsupportedGridclass GridFactory:def create_grid(self, grid_size: int) -> Grid:
if grid_size == 3:
return ThreeThreeGrid()
else:
raise UnsupportedGrid(f"{grid_size}x{grid_size} grid is not supported!")
So I created a custom error handler UnspportedGrid
for handling the error mentioned above.
class UnsupportedGrid(Exception):
pass
You can find all the codes in my GitHub Repository for your reference.
In 2021 Winter, I joined DaytaAI as a software engineer intern. Here I learned about the SOLID principle, design patterns and some best practices on how to structure my code.
Thank You very much for looking into this article. I hope you learn some of the best practices in writing code and try to apply in your codebase to develop more understandable and reusable code.