Introduction of 5 Design Patterns: Explained by Python
Tags: systemdesign, designpatterns, beginners, python
Published: 2023-08-02 01:37:29

# Introduction

I am writing this article since I felt compelled to learn this topic in order to step up to a higher level position as a software engineer. In software engineering, design patterns play a crucial role in crafting robust, scalable, and maintainable applications. This article explores various design patterns and demonstrates their implementation in Python, a versatile and user-friendly programming language.


# 1. Singleton Pattern
This pattern ensures that a class has only one instance throughout the application's lifecycle. it provides a global access point to this instance, making it ideal for scenarios where a single shared resource is required.
```python
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Usage
singleton_obj1 = Singleton()
singleton_obj2 = Singleton()
assert singleton_obj1 is singleton_obj2  # Both objects point to the same instance
```


# 2. Factory Pattern

This pattern centralizes object creation by utilizing a factory method that instantiates different classes based on specific conditions. it promotes loose coupling and simplifies object creation.
```python
class Shape:
    def draw(self):
        pass

class Circle(Shape):
    def draw(self):
        print("Drawing a circle")

class Square(Shape):
    def draw(self):
        print("Drawing a square")

class ShapeFactory:
    @staticmethod
    def create_shape(shape_type):
        if shape_type == "circle":
            return Circle()
        elif shape_type == "square":
            return Square()

# Usage
factory = ShapeFactory()
circle = factory.create_shape("circle")
square = factory.create_shape("square")
circle.draw()  # Output: Drawing a circle
square.draw()  # Output: Drawing a square
```


# 3. Observer Pattern
This pattern establishes a one-to-many relationship between objects, allowing multiple observers to be notified of changes in a subject. It promotes flexibility and reduces dependencies between objects.
```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, message):
        for observer in self._observers:
            observer.update(message)

class Observer:
    def update(self, message):
        print(f"Received message: {message}")

# Usage
subject = Subject()
observer1 = Observer()
observer2 = Observer()

subject.attach(observer1)
subject.attach(observer2)
subject.notify("Hello, Observers!")  # Output: Received message: Hello, Observers!
```


# 4. Strategy Pattern
This pattern allows algorithms to be selected dynamically at runtime by encapsulating them in separate classes. This promotes code reusability and flexibility in choosing different strategies.
```python
# Strategy Pattern in Python

# Context
class PaymentContext:
    def __init__(self, payment_strategy):
        self._payment_strategy = payment_strategy

    def set_payment_strategy(self, payment_strategy):
        self._payment_strategy = payment_strategy

    def make_payment(self, amount):
        self._payment_strategy.pay(amount)

# Strategy Interfaces
class PaymentStrategy:
    def pay(self, amount):
        pass

# Concrete Strategies
class CreditCardPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paid ${amount} with Credit Card")

class PayPalPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paid ${amount} with PayPal")

class BankTransferPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paid ${amount} with Bank Transfer")

# Usage
credit_card_payment = CreditCardPayment()
paypal_payment = PayPalPayment()
bank_transfer_payment = BankTransferPayment()

payment_context = PaymentContext(credit_card_payment)
payment_context.make_payment(100)  # Output: Paid $100 with Credit Card

payment_context.set_payment_strategy(paypal_payment)
payment_context.make_payment(50)  # Output: Paid $50 with PayPal

payment_context.set_payment_strategy(bank_transfer_payment)
payment_context.make_payment(200)  # Output: Paid $200 with Bank Transfer
```


# 5. MVC(Model-View-Controller) Pattern
This pattern is a software architectural design that separates an application into three interconnected components - Model, View, and Controller. The Model represents the data and business logic, the View handles the user interface, and the controller manages user input and interactions. This separation enhances maintainability.
```python
# Model
class UserModel:
    def __init__(self, name):
        self.name = name

# View
class UserView:
    def show_user_details(self, user):
        print(f"User Name: {user.name}")

# Controller
class UserController:
    def __init__(self, model, view):
        self.model = model
        self.view = view

    def update_user_name(self, name):
        self.model.name = name

    def show_user_details(self):
        self.view.show_user_details(self.model)

# Usage
user_model = UserModel("John Doe")
user_view = UserView()
user_controller = UserController(user_model, user_view)

user_controller.show_user_details()  # Output: User Name: John Doe
user_controller.update_user_name("Jane Smith")
user_controller.show_user_details()  # Output: User Name: Jane Smith
```


# Conclusion
Design patterns are valuable tools that every software engineer should be acquainted with. They empower us to construct flexible, modular, and scalable systems. Through our implementation in Python, we have demonstrated how these patterns can be effectively utilized in code. Gaining a comprehensive understanding and applying these design patterns will significantly improve the quality and efficiency of your software projects. I look forward to delving deeper into design patterns in the future.