Recognizing the Design Patterns You’ve Always Used (Part 1: Creational Patterns)

John Park
13 min readJun 6, 2023

--

Photo by Martin Sanchez on Unsplash

As software developers, we are in constant struggle with complexity. Left unmanaged, it can make it difficult to make sense of our code and make every change a tedious chore.
Yet in our efforts to combat this complexity, you’ve probably experienced it yourself: that moment when you invent a clever solution that provides some clarity in the chaos. What’s fascinating is that these solutions we stumble upon, born of necessity, often mirror established design patterns in object-oriented programming.

Design patterns are tried and tested solutions to common software design problems, and they can dramatically streamline the process of building robust, scalable software. Learning these patterns isn’t about memorizing a set of rules of when to use what, but rather understanding a language that can help articulate complex software design concepts. It’s akin to discovering you’ve been using fragments of a foreign language all along — and now, it’s time to become fluent.

The goal of this article is to make design patterns your natural allies in the battle against complexity, to help you articulate software design solutions, and to help you identify the patterns already lurking in your code.

Article Structure

For this first article, I will focus exclusively on creational design patterns. I will tackle structural and behavioral design patterns in following articles.

In these series of articles, I will be presenting the design patterns in a way that made it intuitive for me. To that end, I want to provide not just definitions and examples, but also the context of their usage and how these design patterns relate to each other. Specifically, for each design pattern, I will provide: a definition, a basic example, intuition on when to use it, and a concrete example that highlights the advantages of the design pattern.
I will be dealing with the original 23 design patterns as presented in the classic Design Patterns book.

The design patterns are not presented in order. Feel free to skip ahead to the sections most relevant to you.

Creational Design Patterns

In all Creational Design Patterns, the common theme is the separation of object creation logic into a distinct object or class.
For instance, let’s assume you have a class that creates objects via its constructor. Applying a creational design pattern, you introduce an abstraction layer (like another object), allowing instantiation to occur through this abstraction, rather than directly invoking the object’s constructor.

This distinction matters because creating an object and using it often involve different concerns. When working with an object, what’s crucial is having the correct type of object available, regardless of whether it’s a reused, cloned, or subclass-originating object. Ideally, when using a creational design pattern, you start with a creation-oriented object, provide it with certain parameters, and request the construction of an object from a particular class. This decouples the process of creating an object from its subsequent use, offering a tidier and more efficient approach to object-oriented programming.

Singleton

Photo by Robert V. Ruggiero on Unsplash

The Singleton pattern ensures that only one instance of a class is ever created. It manages access to this single object, creating it if it doesn’t yet exist, and providing access to the existing one if it does. This single instance persists throughout the application’s lifetime and may maintain its own state.
Using Singleton improves performance by avoiding the overhead of repeated creation and potential memory issues due to redundant objects. It’s a design pattern that promotes reusability and efficiency in your codebase.

Basic Example

class Singleton:
_instance = None # keeps the only instance of the class

@staticmethod
def getInstance():
if Singleton._instance == None:
Singleton()
return Singleton._instance

def __init__(self):
""" Virtually private constructor. """
if Singleton._instance != None:
raise Exception("This class is a singleton!")
else:
Singleton._instance = self

s = Singleton() # Singleton created
print(s)

s = Singleton.getInstance() # Previous singleton referenced
print(s)

s = Singleton.getInstance() # Previous singleton referenced
print(s)

Concrete Applications

A singleton is particularly useful when exactly one object is needed to coordinate actions across the system.
If you’ve done logging in Python, you’ve already used the singleton class before.
The benefits include:
1. A single point of control and coordination for logging makes it easier to manage and adjust the behavior globally, also making sure that how it is done is consistent.
2. Avoids the unnecessary creation of multiple Logger objects, which can be resource-intensive.
3. Multiple logger objects could cause file write conflicts or inconsistencies in the log output.

What to look for in your code

  • Do you only need one of an object? Avoid the overhead of creation and excessive memory usage by just creating and reusing one object.
  • Are you handling access to a shared resource? This could be a database, logs, or configurations. Instantiating a class multiple times might lead to write/read conflicts or inefficiency.
  • Does the object need to remain aware of actions across the system? Then having a single global state is actually beneficial.

However, the main downside of a singleton is that they maintain state information within the base object which is maintained for the lifetime of an application. This could cause issues such as making it more difficult to identify bugs from unpredictable changes in the underlying state.

Builder

A craftsman works on his piece in stages; Photo by Adam Patterson on Unsplash

The Builder design pattern is an approach to manage the construction of complex objects in stages. This is done by isolating the creation logic into a separate ‘Builder’ class that typically includes methods to assign values to individual attributes, allowing an object to be built incrementally and flexibly. Depending on specific requirements, these steps can be performed in any sequence, or some may even be skipped. The result of this is that the final object can be built piecemeal, where attributes can even be set in different places in the code. An added benefit is that this enables the creation of different representations of a product using the same construction process.

Conventionally, the pattern concludes with a straightforward “build()” method that generates the final product. The main aim of the Builder pattern is to ensure the creation of complex objects is clear, readable, and maintainable. As such, the “build()” method should be kept as simple as possible. If this method becomes overly complicated, it would be functionally akin to a conventional constructor, thereby undermining the pattern’s benefits.

What to look for in your code

  • Unwieldy constructor. Too many parameters, too many options, too much order to keep track of. In this case decomposing it into stages can lead to cleaner, more readable code.
  • Using a Builder can offer flexibility when the object’s construction parameters might change. For example, if your object initially required parameters A, B, and C, but now only needs A and B, a Builder allows for these modifications in a centralized location, instead of needing extensive code changes. However, over-reliance on this pattern can lead to accumulating technical debt, so it’s recommended only where such flexibility is truly beneficial.
  • The steps of object creation are dispersed across the codebase. In this case, it’s necessary to be able to set certain creation parameters as necessary until all requirements have been met to create the object.
  • When the usage of too many subclasses seems cumbersome, a Builder offers a neat alternative.
  • If an object’s creation logically aligns with a series of related operations, executing in stages, Builder serves as an intuitive design choice.

Basic Example

class Car:
def __init__(self):
self.model = None
self.tires = None
self.engine = None

def __str__(self):
return f'{self.model} | {self.tires} tires | {self.engine} engine'


class CarBuilder:
def __init__(self):
self.car = Car()

def set_model(self, model):
self.car.model = model
return self

def set_tires(self, tires):
self.car.tires = tires
return self

def set_engine(self, engine):
self.car.engine = engine
return self

def build(self):
return self.car


builder = CarBuilder()
car = (builder.set_model('Sedan')
.set_tires('Winter')
.set_engine('V8')
.build())
print(car) # Output: Sedan | Winter tires | V8 engine

Concrete Applications

Here are a few examples where the Builder pattern might be used:

  1. GUI Construction: Say that you need to create a popup menu. You would need to create and set the fields, buttons, controls, text, and other elements separately. Only when all the elements are in place would you want to construct the working object.
  2. Query Construction: When building queries to interact with databases, it’s more intuitive to construct queries step by step, adding filters, sorting, and joins as needed before final execution.
  3. API Request Construction: When interacting with a complex API that requires various parameters, headers, and request bodies, each of these also represents a distinct stage that is more intuitive handled separately.

Factory

They’re all cars, they can all drive, but you get to pick which one; Photo by Felix Fuchs on Unsplash

The Factory design pattern works with a superclass and its related subclasses, all sharing a common interface, as defined by the superclass. This shared interface allows client code to use objects of different subclasses interchangeably, focusing on their common features rather than the specific types. The Factory pattern enables the introduction or alteration of subclasses without disrupting existing code, offering a degree of flexibility.

Take, for instance, a Vehicle superclass with Car, Bike, and Bus as subclasses. Without a Factory, you’d directly call the Car constructor to create a car. With a Factory pattern in place, you’d use a method like “get_car” instead. Should you need to switch from a car to a bus, you could simply call “get_bus”. As Car and Bus share the same interface, this swap won’t lead to issues. The Factory isolates the client from the object creation process but still facilitates access to instances of different classes.

Unlike the Builder pattern, which constructs an object in steps, the Factory creates objects in a single step, making it a suitable choice for less complex objects.

What to look for in your code

  • Lots of subclasses from the same superclass that are able to share the same interface.
  • When in some place in your code you know that you’ll need to use some subclass coming from a superclass that may change (ie. if you need to drive a vehicle at some place in your code, but may be provided a car, bike, or bus)
  • In the future you’ll be writing a lot of subclasses based on the superclass. Create a factory so that functions can depend on the factory, and updating the factory to support additional subclasses is easy.

Basic Example


class Vehicle:
def drive(self):
pass

class Car(Vehicle):
def __init__(self):
self.type = "Car"

def drive(self):
return "The car drives smoothly."

class Bike(Vehicle):
def __init__(self):
self.type = "Bike"

def drive(self):
return "The bike rides swiftly."

class VehicleFactory:
@staticmethod
def create_vehicle(type_of_vehicle):
if type_of_vehicle == "Car":
return Car()
elif type_of_vehicle == "Bike":
return Bike()
else:
raise ValueError("Invalid type of vehicle")

# usage
factory = VehicleFactory()
my_car = factory.create_vehicle("Car")
print(my_car.type) # Outputs: Car
print(my_car.drive()) # Outputs: The car drives smoothly.

Concrete Applications

Let’s say you’re working with an app that uses various databases, like PostgreSQL, MySQL, or SQLite. Each one has its own quirks — different SQL syntax, connection methods, and ways to handle data. A Factory object like `DatabaseConnectionFactory` can simplify this by creating the right type of database connection based on the parameters you give it, handling all the nuances behind the scenes. All the different connection objects it creates will have the same methods, but those methods will work differently depending on the database. So if you need a PostgreSQL connection, you just tell the Factory, and it hands you a `PostgreSQLConnection` object all set up and ready to go. The same goes for MySQL, SQLite, or any other database you might use. The value of this approach is that it lets you focus on what you’re doing with the database, not on how you’re connecting to it.

Abstract Factory

All in one style — all from one Abstract Factory; Photo by Spacejoy on Unsplash

An Abstract Factory provides a way to create groups of related objects without having to know the specific classes they come from. Imagine you’re building a car and need wheels, a body, and an engine. Instead of creating each part separately, you could use an Abstract Factory, `CarAbstractFactory`, which offers methods like `get_wheels`, `get_body`, and `get_engine`. These methods return the parts you need, which you can then put together to form a car. Just remember that before using the `CarAbstractFactory`, it must be turned into a concrete factory.

The Abstract Factory pattern is excellent for organizing factories with a shared theme that produce objects from multiple class hierarchies. It shines when the creation, composition, and representation of products should be separate from the system and when related products should be used together, enforcing this requirement.

What to look for in your code

  • You have a lot of classes related by a common purpose. All needed to work with a cloud provider, some API, or another purpose.
  • You have objects that are dependent on each other and it wouldn’t make sense to handle them separately.
  • There are enough objects that, for the sake of clarity, it’s easier to be dealing with a single point of creation in an Abstract Factory rather than with the individual classes.

Basic Example

from abc import ABC, abstractmethod


# Abstract products
class AbstractButton(ABC):
@abstractmethod
def click(self):
pass


class AbstractCheckbox(ABC):
@abstractmethod
def check(self):
pass


# Concrete products
class WindowsButton(AbstractButton):
def click(self):
return "Windows Button clicked"


class MacOSButton(AbstractButton):
def click(self):
return "MacOS Button clicked"


class WindowsCheckbox(AbstractCheckbox):
def check(self):
return "Windows Checkbox checked"


class MacOSCheckbox(AbstractCheckbox):
def check(self):
return "MacOS Checkbox checked"


# Abstract factory
class AbstractFactory(ABC):
@abstractmethod
def create_button(self):
pass

@abstractmethod
def create_checkbox(self):
pass


# Concrete factories
class WindowsFactory(AbstractFactory):
def create_button(self):
return WindowsButton()

def create_checkbox(self):
return WindowsCheckbox()


class MacOSFactory(AbstractFactory):
def create_button(self):
return MacOSButton()

def create_checkbox(self):
return MacOSCheckbox()


# Client code
def client_code(factory: AbstractFactory):
button = factory.create_button()
checkbox = factory.create_checkbox()

print(button.click())
print(checkbox.check())


# Usage
windows_factory = WindowsFactory()
macos_factory = MacOSFactory()

print("Windows GUI:")
client_code(windows_factory)

print("\nMacOS GUI:")
client_code(macos_factory)

Concrete Applications

In an application where you want to support multiple themes (like Dark Mode, Light Mode), each theme might require different versions of UI elements. An Abstract Factory can be responsible for creating these theme-specific UI elements. If you used an Abstract Factory pattern, you might have a `UIFactory` interface with `createButton`, `createTextBox`, `createCheckBox` methods, and then `DarkModeUIFactory` and `LightModeUIFactory` subclasses that implement these methods. This way, you can create a whole family of Dark Mode or Light Mode UI elements, ensuring that they all work together correctly.

Prototype

It’s easier to clone plants than to grow a new one; Photo by Chi Pham on Unsplash

The Prototype design pattern creates new objects by cloning an existing instance, instead of instantiating a class. Here’s how it works: you initialize an object and set its properties. When you need a similar object, you simply create a copy or ‘prototype’ of the original. This method is advantageous in situations where object creation is resource-intensive or depends on complex state configurations. So, instead of going through the laborious instantiation process each time, you create an object once, and then make copies as needed, promoting efficiency and ease of use.

What to look for in your code

  • Creation process has a lot of overhead but you need many instances of this class.
  • You already have an object and need almost the same object, but with only a couple of things changed.
  • You already have an object with most of the required state, so you can just clone it instead of having to go through all the difficulty of creating a new object.
  • If a system uses a large number of subclasses that differ only in their state, the Prototype pattern could be employed to reduce the number of classes created.

Basic Example

import copy

class Prototype:
def __init__(self):
self._objects = {}

def register_object(self, name, obj):
"""Register an object."""
self._objects[name] = obj

def unregister_object(self, name):
"""Unregister an object."""
del self._objects[name]

def clone(self, name, **attr):
"""Clone a registered object and update its attributes."""
obj = copy.deepcopy(self._objects.get(name))
obj.__dict__.update(attr)
return obj

class Car:
def __init__(self):
self.name = "Skylark"
self.color = "Red"
self.options = "Ex"

car = Car()
prototype = Prototype()
prototype.register_object('skylark', car)

car1 = prototype.clone('skylark')
print(car1)

car2 = prototype.clone('skylark', color="Green")
print(car2.color) # Output: Green

Concrete Applications

The Prototype pattern is particularly useful when the cost of creating an object is high in terms of resources or time, or when the system needs to be independent of how its products are created, composed, and represented.

  1. Game Development: In game development, objects like enemies, trees, or terrain tiles can be clones of a prototype, to reduce memory usage. This is crucial when the game needs to instantiate hundreds or thousands of similar objects.
  2. Database Operations: When dealing with heavy database operations, you can use the Prototype pattern to clone the query’s data model rather than making a fresh database call.
  3. Multithreaded Environment: In a multithreaded environment, prototypes can be useful for maintaining the state of the objects, where each thread can have a separate instance of the object cloned from the prototype, avoiding concurrent modification conflicts.

The Prototype pattern should be considered when an object creation would involve more complexity than simply instantiating a class, like involving database operations, network requests, or high computation tasks. Also, when objects of a class can have only a few different states, it’s advantageous to install a corresponding number of prototypes and clone them rather than instantiating the class manually each time.

Conclusion

As we conclude this exploration of Creational Design Patterns, remember that they’re not just tools - they’re part of a language to articulate complex software designs. Coming to understanding these foundational patterns — the Singleton, Builder, Prototype, Factory, and Abstract Factory — is the first step in acquiring this language. Thank you for your patience and commitment.

If you would like to learn about the remaining Structural and Behavioral Design Patterns, please stay tuned for my upcoming articles.

Disclaimer

For transparency, this series of articles started as personal conversations with chatGPT to clarify my understanding of design patterns. However, as I found it very insightful, I decided to collect, edit, and organize the contents of my conversations into this format for dissemination to a wider audience. I hope that you find it helpful to your own understanding too.

--

--

John Park

Software engineer decoding complex coding challenges, exploring data analysis, and full stack development. Eager to embrace challenges & innovation.