Factory Patterns, SOLID Principles, and Error Handling the Tic Tac Toe Example

Oscarli916
Dayta AI
Published in
5 min readMar 21, 2022

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.

Photo by Visual Stories || Micheile on Unsplash

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 Box
class 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 Grid
class 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 ThreeThreeGrid
class 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 UnsupportedGrid
class 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.

--

--