This guide and its related files were created while following the Udemy courses
by Dmitri Nesteruk. I use some C++ examples, by I try to focus primarily in Python.
I found a repository by Dmitri Nesteruk, which I forked
However, the current repository builds up on that one and re-structures the complete code. Nevertheless, note that the original example code is by Dmitri Nesteruk.
In addition to the aforementioned courses, I also used the following resources:
- "Design Patterns" (Addison Wesley), by Gamma, Helm, Johnson & Vlissides (The Gang of Four)
- "Design Patterns" (O'Reilly Head First), by Kathy Sierra and Bert Bates
- Python Design Patterns, by Brandon Rhodes
The notes are divided into the following documents:
DesignPatterns_01_SOLID.md
DesignPatterns_02_Creational.md
DesignPatterns_03_Structural.md
DesignPatterns_04_Behavioral.md
And the code with examples is located in their corresponding folders:
Finally, note that knowledge and experience in Object Oriented Programming (OOP) is required to use this guide; a short summary on OOP using Python is given in the section Object Oriented Programming.
Table of contents:
Design Patterns are common and re-usable programming approaches that were popularized in the book of the same name (by "the Gang of Four": Gamma, Helm, Johnson & Vlissides). They have been internalized to some languages and every programmer should know them, since they are the basic vocabulary and grammar for software architecture.
According to the Gamma categorization (Erich Gamma, from the Gang of Four), there are three types of patterns:
- Creational
- Structural
- Behavioral
This guide compiles all the patterns defined in the book by the Gang of Four and additionally introduces the SOLID principles:
- SOLID Design Principles
- Single Responsibility
- Open-Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
- Creational Patterns
Creational patterns deal with the creation/construction of objects:
- The creation can be explicit (e.g., via constructors) of implicit (e.g., dependency injection, reflection, etc.).
- The creation can be wholesale (single statement creates the object) or piecewise, i.e., it is done step-by-step in a more complicated process.
Patterns:
- Builder
- Factories: Abstract, Factory Method and Class
- Prototype (Factory)
- Singleton: Allocator, Decorator, Metaclass, Monostate
- Structural Patterns
Structural patterns deal with the structure of objects:
- Class members, adherence to interfaces, etc.
- Many patterns are wrappers that mimic the underlying class interface.
- Stress the importance of good API design: usability, etc.
Patterns:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Behavioral Patterns
Behavioral patterns don't really follow any central theme:
- They are all different.
- Sometimes they overlap in their function, i.e., the goal they achieve, but the underlying mechanisms are different.
Patterns:
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Nesteruk warns that, for the sake of simplicity, there are some simplifications in his examples: liberal use of public members, lack of virtual destructors, passing/returning by value, lack of move operations...
I use both Python and C++ in the notes and code examples, but I focus primarily on Python.
The boost libraries are used throughout the course. To install them on a Mac:
brew install boost
Additionally, the CMakeLists.txt
file of each project needs to be updated to contain all necessary Boost library information:
list(APPEND CMAKE_PREFIX_PATH /opt/homebrew)
find_package(Boost REQUIRED)
include_directories(${Boost_INCLUDE_DIR})
add_executable(main main.cpp)
target_link_libraries(main ${Boost_LIBRARIES})
No preliminary installations needed; concrete installations introduced in specific example explanations, if needed.
I use conda
environments.
This repository assumes you have an intermmediate-advanced level in Python and software engineering. Here are two links of guides I made on selected topics that might be of your interest:
Similarly, this repository assumes you have practical experience in Object-Oriented Programming (OOP). Here's a reminder of the most important concepts in OOP focusing on Python:
- Classes and objects
- Constructor:
__init__
- Attributes (i.e., state):
@property
,self.__dict__
,@attribute.setter
. - Methods
- Special methods (dunder):
__init__
,__repr__
,__str__
,__call__
,__getitem__
,__setitem__
,__enter__
,__exit__
, etc.
- Constructor:
- Encapsulation: public and private data and methods, grouped together; properties, getters and setters.
- OOP relationships:
- Inheritance: classes (children) derived from other classes (parents);
is-a
relationships.super()
: It gets the parent class of the class and it's often used to call its special methods, e.g., teh constructor__init__()
. If the class was not inherited, it implicitly inherits fromobject
, so the call doesn't raise an error.
- Composition: complex objects built using other objects; class built using other classes, i.e.
has-a
relationships. We can often choose between inheritance and composition, depending on what we'd like to have:- inheritance: reuse of common logic, clear hierarchy relationships.
- composition: flexibility, reduced coupling between classes.
- Aggregation: type of composition in which the components can exist on their own, e.g.,
Team
andPlayer
(component). - Association: A general relationship where objects of one class are associated with objects of another class. This can be unidirectional or bidirectional. A
Customer
can be associated with multipleOrders
. - Dependency: one class depends on another class to function. This is typically represented by method parameters or local variables. Example: A
PaymentProcessor
class depends on aPaymentMethod
class to process payments.
- Inheritance: classes (children) derived from other classes (parents);
- Overloading of functions or class methods (i.e., using the same function name with different arguments or argument types) is not possible in Python — in a straightforward way; however, we can use decorartors to simulate overloading:
@singledispatch
and@singledispatchmethod
fromfunctools
.@overload
fromtyping
.
- Mixin: we inherit a class from two, they allow for the implementation of specific functionalities to be shared across multiple classes.
- Interfaces: classes with non-implemented methods, i.e., kind of contracts that define which methods should be implemented in the inherited classes. In Python, interfaces can be defined using abstract base classes (
from abc import ABC, abstractmethod
) with abstract methods, i.e., using@abstractmethod
. An abstract method needs to be defined in the derived class, otherwise theTypeError
is raised at the moment we attempt to create an instance of the subclass. - Polymorfism: when a function accepts objects of different classes because they all come from the same parent class; ability of different types of objects to be treated as instances of the same class through inheritance.
- Abstraction: writing code at higher level hiding details; polymorfism is an example: interfaces don't know anything about the underlying implementation but we use them to pass objects to functions!
- Dependency Injection: passing one object into another object's methods or constructor, instead of creating it internally.
- Mocking: passing a minimal implementation that adheres to correct interfaces/signatures for the purpose of testing.
- Static methods via
@staticmethod
: methods that don't change the object, usually they don't access class attributes. - Abstract methods :
- Class methods via
@classmethod
: an attribute of the class is modified, i.e., same value for all class object instances. - Caching via
@functools.lru_cache
: memoization utility, i.e., results of expensive functions (e.g., recursive) are cached in a dictionary. - Overloading via
@functools.singledispatch
: different behaviors allowed for the same function signature depending on the type of the arguments. - Metaclasses: a class of a class that defines how a class behaves; as objects are instances of classes, classes are instances of metaclasses.
- Some other additional points:
- Type hints
- Keywords like
pass
and ellipsis...
- Error/exception raising
Enum
- Fluent interfaces with chaineable calls
- Deep copies
Python examples:
##### -- Classes, Objects, Attributes, Methods, Constructors, Encapsulation
class Person:
def __init__(self, name, id=None):
if id is None:
id = uuid.uuid4()
# In Python all members are public
# but as convention to denote one to be used as private
# we can use a leading _
self._name = name
# If we precede a class attribute with __
# Python carries out name mangling, so the variable
# is even more difficult to be accessed,
# i.e., it seems to be really private.
# However, it is really still accessible via
# object._ClassName__variableName
self.__id = id
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not value:
raise ValueError("Name cannot be empty")
self._name = value
@property
def id(self):
return self.__id
person = Person("Alice")
print(person.name) # Output: Alice
person.name = "Bob"
print(person.name) # Output: Bob
print(person.id) # Output: The UUID value (e.g., 123e4567-e89b-12d3-a456-426614174000)
# Accessing the private variable via name mangling
print(person._Person__id) # Output: The same UUID value as above
##### -- Inheritance
class Employee(Person):
def __init__(self, name, employee_id):
super().__init__(name)
self.employee_id = employee_id
##### -- Composition
class Department:
def __init__(self, name):
self.name = name
self.employees = []
def add_employee(self, employee):
if isinstance(employee, Employee):
self.employees.append(employee)
dept = Department("HR")
dept.add_employee(Employee("Jane Doe", "1234"))
##### -- Overloading: functools.singledispatch and singledispatchmethod
from functools import singledispatch, singledispatchmethod
@singledispatch
def func(val):
raise NotImplementedError
@func.register
def _(val: str):
print('This is a string')
@func.register
def _(val: int):
print('This is an int')
func("test") # "This is a string"
func(1) # "This is an int"
func(None) # NotImplementedError
class MyClass:
@singledispatchmethod
def do_something(self, value):
raise NotImplementedError("Unsupported type")
@do_something.register
def _(self, value: str):
return f"Concatenated string: {value + value}"
@do_something.register
def _(self, value: int):
return f"Sum of integers: {value + value}"
# Example usage
obj = MyClass()
print(obj.do_something("Hello")) # Output: Concatenated string: HelloHello
print(obj.do_something(5)) # Output: Sum of integers: 10
##### -- Overloading: typing.overload
from typing import overload
@overload
def process_data(data: str) -> str: ...
@overload
def process_data(data: int) -> int: ...
@overload
def process_data(data: float) -> TypeError: ...
def process_data(data):
if isinstance(data, str):
# Implementation for str type input
return "Processed " + data
elif isinstance(data, int):
# Implementation for int type input
return data + 10
else:
raise TypeError("Invalid data type")
# Function call with str type parameter and return type
result1 = process_data("Hello")
print(result1) # Output: Processed Hello
# Function call with int type parameter and return type
result2 = process_data(5)
print(result2) # Output: 15
# Function call with unsupported type parameter
result3 = process_data(3.14) # Raises TypeError
##### -- Mixin
class JsonMixin:
"""Mixin to add JSON serialization functionality to a class."""
def to_json(self):
import json
return json.dumps(self.__dict__)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class Student(JsonMixin, Person):
def __init__(self, name, age, school):
super().__init__(name, age)
self.school = school
student = Student('John Doe', 20, 'MIT')
print(student.to_json())
##### -- Interfaces, Polymorphism and Abstraction
from abc import ABC, abstractmethod
# If we don't inherit from ABC the implementation
# of the @abstractmethods is not enforced in subclasses
# and we loose automatic checks from ABC -- but it still works!
class MyInterface(ABC):
@abstractmethod
def needed_method(self) -> str:
pass
class ConcreteImplementationA(MyInterface):
def needed_method(self) -> str:
return "concrete_a"
class ConcreteImplementationB(MyInterface):
def needed_method(self) -> str:
return "concrete_b"
def polymorphic_function(obj: MyInterface) -> None:
print(obj.needed_method())
a = ConcreteImplementationA()
b = ConcreteImplementationB()
polymorphic_function(a)
polymorphic_function(b)
##### -- Dependency Injection
class Logger:
def log(self, message):
print(f"Log: {message}")
class Application:
def __init__(self, logger):
self.logger = logger
def do_something(self):
self.logger.log("Something was done")
app = Application(Logger())
app.do_something()
##### -- Mocking
class MockLogger:
def log(self, message):
print(f"Mock log: {message}")
test_app = Application(MockLogger())
test_app.do_something()
##### -- Static Methods
class MathOperations:
@staticmethod
def add(x, y):
return x + y
##### -- Class Methods
class Person:
population = 0
def __init__(self, name):
self.name = name
Person.population += 1
@classmethod
def how_many(cls):
return cls.population
##### -- Caching
import functools
class Fibonacci:
@staticmethod
@functools.lru_cache(maxsize=None) # Cache results indefinitely
def fib(n):
if n < 2:
return n
return Fibonacci.fib(n-1) + Fibonacci.fib(n-2)
##### -- Overloading
from functools import singledispatch
@singledispatch
def say_hello(value):
print(f"Hello {value}!")
@say_hello.register(int)
def _(value):
print(f"Hello from a number: {value}!")
@say_hello.register(list)
def _(value):
print("Hello from a list!")
for item in value:
print(f"- {item}")
##### -- Metaclasses
# Metaclass = a class of a class that defines
# how a class behaves.
# As objects are instances of classes,
# classes are instances of metaclasses.
# Metaclasses are created by inheriting from type
# The implementation of special methods can be changed with them!
class MyMeta(type):
def __new__(cls, name, bases, dct):
print(f"Creating class {name}")
return super(MyMeta, cls).__new__(cls, name, bases, dct)
def __init__(cls, name, bases, dct):
print(f"Initializing class {name}")
super(MyMeta, cls).__init__(name, bases, dct)
def __call__(cls, *args, **kwargs):
print(f"Creating instance of {cls.__name__}")
return super(MyMeta, cls).__call__(*args, **kwargs)
# Metaclasses are used by passing them as metaclass
class MyClass(metaclass=MyMeta):
def __init__(self, value):
self.value = value
def show(self):
print(f"MyClass instance with value: {self.value}")
# Creating an instance of MyClass
obj = MyClass(42)
obj.show()
# Output:
# Creating class MyClass
# Initializing class MyClass
# Creating instance of MyClass
# MyClass instance with value: 42
##### -- Type Hints
def greet(name: str) -> str:
return f"Hello, {name}!"
##### -- Pass
class MyAbstractClass:
def do_something(self):
pass # No operation here
def unimplemented_function():
... # Another placeholder, functionally similar to 'pass' in this context
my_object = MyAbstractClass()
my_object.do_something() # Does nothing
unimplemented_function() # Also does nothing
##### -- Enum
from enum import Enum, auto
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
favorite_color = Color.RED
print(favorite_color) # Outputs: Color.RED
print(favorite_color.name, favorite_color.value) # Outputs: RED 1
# With auto() we don't need to assign a value
class Drink(Enum):
COFFEE = auto()
TEA = auto()
JUICE = auto()
WATER = auto()
##### -- Error handling
def divide(x: int, y: int) -> float:
if y == 0:
raise ValueError("Cannot divide by zero.")
return x / y
try:
result = divide(10, 0)
except ValueError as e:
print(e) # Outputs: Cannot divide by zero.
##### -- Fluent interfaces
class Car:
def __init__(self):
self.color = None
self.brand = None
def set_color(self, color):
self.color = color
return self # Return self to allow chaining
def set_brand(self, brand):
self.brand = brand
return self # Return self to allow chaining
def __str__(self):
return f"Car(Brand={self.brand}, Color={self.color})"
# Usage
my_car = Car().set_color('red').set_brand('Toyota')
print(my_car) # Outputs: Car(Brand=Toyota, Color=red)
##### -- Deep copies
import copy
car_vw = Car().set_color('red').set_brand('VW')
car_vw_2 = car_vw # This is not a real copy! Car is mutable, so we assign a pointer!
car_vw_3 = copy.deepcopy(car_vw) # This is a real copy, new bytes in memory!
- All 23 OOP software design patterns with examples in Python
- SOLID Principles In Python
- Python Design Patterns In Real World Projects
- Architecture Patterns: The Cheat Sheet
- It All Comes Down To Design Patterns
- Refactoring Guru: Design Patterns
- Design Patterns Quick Reference by Jason McDonald
Original code and examples primarily from Dmitri Nesteruk.
Code modifications and notes: Mikel Sagardia, 2022.
No guaranties.