Design Patterns: A Comprehensive Guide with Python Examples


  
MontaF - Sept. 12, 2024

33
0

...

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:

  1. Creational Patterns – Patterns that deal with object creation mechanisms.
  2. Structural Patterns – Patterns that deal with object composition or structure.
  3. 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!



Comments ( 0 )
Login to add comments