Design Patterns: A Comprehensive Guide with Python Examples
MontaF - Sept. 12, 2024
Design patterns are proven solutions to common software design problems that arise during the development process. These patterns are not specific to any programming language but are applicable across many languages, including Python. They serve as templates to solve problems efficiently and make the system more maintainable and scalable.
Design patterns fall into three main categories:
- Creational Patterns – Patterns that deal with object creation mechanisms.
- Structural Patterns – Patterns that deal with object composition or structure.
- Behavioral Patterns – Patterns that focus on communication between objects.
Below, I will explain the most famous design patterns from each category, with examples in Python.
1. Creational Design Patterns
Creational patterns provide various ways to instantiate objects, making the system more flexible by decoupling the creation of objects from their usage.
1.1 Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance.
Example in Python:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
# Usage
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2) # True, both are the same instance
In this example, no matter how many times you instantiate Singleton
, the same instance is returned.
1.2 Factory Method Pattern
The Factory Method pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created.
Example in Python:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError("Unknown animal type")
# Usage
animal = AnimalFactory.create_animal("dog")
print(animal.make_sound()) # Output: Woof!
The AnimalFactory
class encapsulates the logic of object creation, making the code more flexible and easier to extend.
2. Structural Design Patterns
Structural patterns deal with object composition, ensuring that if one part of a system changes, the entire structure of objects does not need to change.
2.1 Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together. It wraps an object with a new interface that the client expects.
Example in Python:
class EuropeanSocket:
def voltage(self):
return 220
def live(self):
return 1
def neutral(self):
return -1
class USASocket:
def voltage(self):
return 120
def live(self):
return 1
def neutral(self):
return -1
class SocketAdapter:
def __init__(self, socket):
self.socket = socket
def voltage(self):
if isinstance(self.socket, EuropeanSocket):
return 120 # Convert to USA voltage
return self.socket.voltage()
# Usage
euro_socket = EuropeanSocket()
adapter = SocketAdapter(euro_socket)
print(adapter.voltage()) # Output: 120, adapted to work with USA voltage
The SocketAdapter
allows a European socket to work in an American environment by adapting the voltage interface.
2.2 Decorator Pattern
The Decorator pattern dynamically adds responsibilities to objects without modifying their class.
Example in Python:
class Coffee:
def cost(self):
return 5
class MilkDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost() + 2
class SugarDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost() + 1
# Usage
coffee = Coffee()
milk_coffee = MilkDecorator(coffee)
sugar_milk_coffee = SugarDecorator(milk_coffee)
print(sugar_milk_coffee.cost()) # Output: 8 (5 for coffee + 2 for milk + 1 for sugar)
In this example, the Decorator
classes dynamically add behavior (like milk and sugar) to the Coffee
object without modifying the original Coffee
class.
3. Behavioral Design Patterns
Behavioral patterns are concerned with communication between objects, focusing on how objects interact with and relate to each other.
3.1 Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Example in Python:
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 set_state(self, state):
self._state = state
self.notify()
class Observer:
def update(self, subject):
pass
class ConcreteObserver(Observer):
def update(self, subject):
print(f"Observer notified. Subject state is {subject._state}")
# Usage
subject = ConcreteSubject()
observer1 = ConcreteObserver()
observer2 = ConcreteObserver()
subject.attach(observer1)
subject.attach(observer2)
subject.set_state("New State") # Output: Observer notified. Subject state is New State
The ConcreteObserver
instances automatically react when the state of the ConcreteSubject
changes.
3.2 Strategy Pattern
The Strategy pattern allows a family of algorithms to be defined and made interchangeable. It lets the algorithm vary independently from the clients that use it.
Example in Python:
class Strategy(ABC):
@abstractmethod
def execute(self, a, b):
pass
class AddStrategy(Strategy):
def execute(self, a, b):
return a + b
class SubtractStrategy(Strategy):
def execute(self, a, b):
return a - b
class Context:
def __init__(self, strategy: Strategy):
self._strategy = strategy
def set_strategy(self, strategy: Strategy):
self._strategy = strategy
def perform_operation(self, a, b):
return self._strategy.execute(a, b)
# Usage
context = Context(AddStrategy())
print(context.perform_operation(10, 5)) # Output: 15
context.set_strategy(SubtractStrategy())
print(context.perform_operation(10, 5)) # Output: -5
In this example, the Context
class can switch between different strategies (addition, subtraction) dynamically, without modifying the client code.
4. Other Notable Design Patterns
4.1 Prototype Pattern
The Prototype pattern creates new objects by cloning an existing object, avoiding the cost of creating an object from scratch.
Example in Python:
import copy
class Prototype:
def __init__(self, value):
self.value = value
def clone(self):
return copy.deepcopy(self)
# Usage
prototype = Prototype([1, 2, 3])
clone = prototype.clone()
print(clone.value) # Output: [1, 2, 3]
4.2 Command Pattern
The Command pattern turns a request into a stand-alone object that contains all information about the request, thus allowing it to be passed, stored, and executed at different times.
Example in Python:
class Command:
def execute(self):
pass
class LightOnCommand(Command):
def execute(self):
print("Light is on")
class LightOffCommand(Command):
def execute(self):
print("Light is off")
class RemoteControl:
def __init__(self, command: Command):
self._command = command
def press_button(self):
self._command.execute()
# Usage
light_on = LightOnCommand()
light_off = LightOffCommand()
remote = RemoteControl(light_on)
remote.press_button() # Output: Light is on
remote = RemoteControl(light_off)
remote.press_button() # Output: Light is off
Conclusion
Design patterns are essential for writing maintainable, flexible, and scalable software. By applying these patterns, developers can avoid common pitfalls, improve code reuse, and make their systems easier to modify or extend. Each design pattern offers a unique solution to common problems and can be adapted to different programming languages, including Python.
If you'd like further clarification or more patterns with examples, feel free to ask!