Software Design Patterns: A Comprehensive Guide

Pedro J. Caceres
4 min readApr 16, 2023

Software design patterns are a set of proven solutions to common problems that software developers face when designing and building software systems. These patterns provide a framework for developers to follow, helping them to produce more maintainable, scalable, and extensible code. In this article, we will explore some of the most common software design patterns and discuss their benefits and drawbacks.

Photo by Francis Naung on Unsplash

Creational Patterns

Singleton Pattern

The Singleton pattern is used when we need to ensure that there is only one instance of a class and that it can be accessed globally. This is useful when we want to limit the number of objects in a system or when we need to coordinate access to shared resources.

Example:

class Singleton:
__instance = None

def __init__(self):
if Singleton.__instance is not None:
raise Exception("Singleton can only be accessed through getInstance() method.")
else:
Singleton.__instance = self

@staticmethod
def getInstance():
if Singleton.__instance is None:
Singleton()
return Singleton.__instance

Factory Pattern

The Factory pattern is used when we want to create objects without specifying the exact class of object that will be created. This is useful when we want to decouple the creation of objects from their use, allowing us to change the type of object created without changing the code that uses them.

Example:

class Animal:
def speak(self):
pass

class Dog(Animal):
def speak(self):
return "Woof!"

class Cat(Animal):
def speak(self):
return "Meow!"

class AnimalFactory:
def create_animal(self, animal_type):
if animal_type == "Dog":
return Dog()
elif animal_type == "Cat":
return Cat()
else:
return None

Builder Pattern

The Builder pattern is used when we want to create complex objects by breaking down the creation process into smaller, more manageable steps. This is useful when we have objects with many dependencies or when we want to separate the construction of an object from its representation.

Example:

class Car:
def __init__(self):
self.__wheels = None
self.__engine = None
self.__body = None

def set_wheels(self, wheels):
self.__wheels = wheels

def set_engine(self, engine):
self.__engine = engine

def set_body(self, body):
self.__body = body

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

def add_wheels(self, wheels):
self.__car.set_wheels(wheels)

def add_engine(self, engine):
self.__car.set_engine(engine)

def add_body(self, body):
self.__car.set_body(body)

def get_car(self):
return self.__car

Structural Patterns

Adapter Pattern

The Adapter pattern is used when we need to adapt an existing interface to meet the needs of a new system. This is useful when we want to reuse existing code in a new context or when we need to integrate systems with incompatible interfaces.

Example:

class EuropeanSocketInterface:
def voltage(self):
pass

def live(self):
pass

def neutral(self):
pass

class USASocket:
def voltage(self):
return 120

def live(self):
return 1

def neutral(self):
return -1

class Adapter(EuropeanSocketInterface):
def __init__(self, socket):
self.__socket = socket

def voltage(self):
return self.__socket.voltage()

def live(self):
return self.__socket.live()

def neutral(self):
return self.__socket.neutral()

Decorator Pattern

The Decorator pattern is used when we want to add new functionality to an existing object without modifying its structure. This is useful when we want to add functionality to objects at runtime or when we have a large number of objects that need to be modified.

Example:

class Component:
def operation(self):
pass

class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"

class Decorator(Component):
def __init__(self, component):
self._component = component

def operation(self):
return self._component.operation()

class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self._component.operation()})"

class ConcreteDecoratorB(Decorator

### Bridge Pattern

The Bridge pattern is used when we want to separate the abstraction of an object from its implementation. This is useful when we want to have multiple implementations of an object and we want to be able to switch between them at runtime.

Example:

```python
class Abstraction:
def __init__(self, implementation):
self.implementation = implementation

def operation(self):
return self.implementation.operation_implementation()

class Implementation:
def operation_implementation(self):
pass

class ConcreteImplementationA(Implementation):
def operation_implementation(self):
return "ConcreteImplementationA"

class ConcreteImplementationB(Implementation):
def operation_implementation(self):
return "ConcreteImplementationB"

Facade Pattern

The Facade pattern is used when we want to provide a simplified interface to a complex subsystem. This is useful when we have a large number of classes and methods in a subsystem and we want to simplify the interface for clients.

Example:

class SubsystemA:
def method_a(self):
pass

class SubsystemB:
def method_b(self):
pass

class Facade:
def __init__(self):
self.__subsystem_a = SubsystemA()
self.__subsystem_b = SubsystemB()

def operation(self):
self.__subsystem_a.method_a()
self.__subsystem_b.method_b()

Behavioral Patterns

Observer Pattern

The Observer pattern is used when we want to notify a set of objects about a change in the state of another object. This is useful when we have a one-to-many relationship between objects and we want to decouple the objects that are observing from the object being observed.

Example:

class Observer:
def update(self, subject):
pass

class Subject:
def __init__(self):
self.__observers = []

def attach(self, observer):
self.__observers.append(observer)

def detach(self, observer):
self.__observers.remove(observer)

def notify(self):
for observer in self.__observers:
observer.update(self)

class ConcreteSubject(Subject):
def __init__(self):
super().__init__()
self.__state = None

def get_state(self):
return self.__state

def set_state(self, state):
self.__state = state
self.notify()

class ConcreteObserver(Observer):
def update(self, subject):
print(f"Subject state changed to {subject.get_state()}")

subject = ConcreteSubject()
observer1 = ConcreteObserver()
observer2 = ConcreteObserver()

subject.attach(observer1)
subject.attach(observer2)

subject.set_state("new state")

Command Pattern

The Command pattern is used when we want to encapsulate a request as an object, allowing us to parameterize clients with different requests, queue or log requests, and support undoable operations. This is useful when we want to decouple the object making a request from the object that executes it.

Example:

class Command:
def execute(self):
pass

class Receiver:
def action(self):
pass

class ConcreteCommand(Command):
def __init__(self, receiver):
self.__receiver = receiver

def execute(self):
self.__receiver.action()

class Invoker:
def set_command(self, command):
self.__command = command

def execute_command(self):
self.__command.execute()

receiver = Receiver()
command = ConcreteCommand(receiver)
invoker = Invoker()

invoker.set_command(command)
invoker.execute_command()

Conclusion

Software design patterns are a powerful tool for software developers. They provide a set of proven solutions to common problems, helping developers to produce more maintainable, scalable, and extensible code. By understanding these patterns and applying them appropriately, developers can create more robust and reliable software systems.

--

--

Pedro J. Caceres

I am a self taugth developer who strives to become better than yesterday. And hopes for a better future.