SOLID - Open-Closed Principle (OCP)

SOLID - Open-Closed Principle (OCP)

The Open-Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. It means that you should be able to extend the behavior of a software entity without modifying its existing code.

from abc import ABC, abstractmethod

class Area(ABC):
    @abstractmethod
    def calculate_area(self):
        pass

class Rectangle(Area):
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def calculate_area(self):
        return self.length * self.width

class Circle(Area):
    def __init__(self, radius):
        self.radius = radius

    def calculate_area(self):
        return 3.14 * self.radius ** 2

def print_areas(shapes):
    for shape in shapes:
        area = shape.calculate_area()
        print(f"Area: {area}")

# Client code
shapes = [Rectangle(4, 5), Circle(3)]
print_areas(shapes)

SOLID and Design Pattern

Open-Closed Principle (OCP) and Strategy Design Pattern

The Strategy pattern allows us to define a family of interchangeable algorithms and encapsulate each one separately, making it easy to add or modify behaviors without modifying the existing code. This promotes the open-closed principle by allowing new strategies to be added without modifying the existing codebase.

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paying ${amount} via credit card.")

class PayPalPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paying ${amount} via PayPal.")

class PaymentProcessor:
    def __init__(self, payment_strategy):
        self.payment_strategy = payment_strategy

    def process_payment(self, amount):
        self.payment_strategy.pay(amount)

# Client code
payment_processor = PaymentProcessor(CreditCardPayment())
payment_processor.process_payment(100)

payment_processor = PaymentProcessor(PayPalPayment())
payment_processor.process_payment(50)

The PaymentProcessor class encapsulates the payment processing logic and accepts a PaymentStrategy object during initialization. The process_payment() method of PaymentProcessor delegates the payment operation to the provided strategy object. By utilizing the Strategy pattern, new payment strategies can be added by creating new classes implementing the PaymentStrategy interface without modifying the existing code in PaymentProcessor. This adheres to the Open-Closed Principle, allowing the system to be easily extended with new payment options while remaining closed for modification.

In more simple words, the Strategy pattern is like having different tools in a toolbox. Each tool performs a specific task, but you can choose which tool to use based on the situation. You don't need to modify the toolbox itself or create a new toolbox for each task. Imagine you have a task to cut different shapes out of paper: circles, squares, and triangles. Instead of having a single pair of scissors that can only cut one shape, you have a set of interchangeable cutting tools. Each tool represents a specific shape cutter.

Open-Closed Principle (OCP) and Observer Design Pattern

Imagine you have a weather monitoring system that needs to notify various display modules whenever the weather conditions change. Instead of directly coupling the display modules to the weather monitoring system, you can apply the Observer pattern to achieve the OCP.

# Observer interface
class Observer:
    def update(self, weather):
        pass

# Concrete Observer 1
class DisplayModule1(Observer):
    def update(self, weather):
        print("Display Module 1 - Weather: {}".format(weather))

# Concrete Observer 2
class DisplayModule2(Observer):
    def update(self, weather):
        print("Display Module 2 - Weather: {}".format(weather))

# Weather monitoring system (Subject)
class WeatherMonitoringSystem:
    def __init__(self):
        self.observers = []

    def add_observer(self, observer):
        self.observers.append(observer)

    def remove_observer(self, observer):
        self.observers.remove(observer)

    def notify_observers(self, weather):
        for observer in self.observers:
            observer.update(weather)

    def update_weather(self, weather):
        print("Weather updated: {}".format(weather))
        self.notify_observers(weather)

# Usage
if __name__ == "__main__":
    # Create the weather monitoring system
    weather_system = WeatherMonitoringSystem()

    # Create display modules
    display_module1 = DisplayModule1()
    display_module2 = DisplayModule2()

    # Register display modules as observers
    weather_system.add_observer(display_module1)
    weather_system.add_observer(display_module2)

    # Simulate weather update
    weather_system.update_weather("Sunny")
    

System Output
Weather updated: Sunny
Display Module 1 - Weather: Sunny
Display Module 2 - Weather: Sunny

By applying the Observer pattern, the weather monitoring system remains closed for modification because it doesn't need to change when new display modules are added or existing ones are modified. You can introduce new display modules simply by implementing the "Observer" interface, registering them with the weather monitoring system, and they will start receiving updates. This promotes the OCP principle by allowing the system to be easily extended without modifying its core implementation.

Popular posts from this blog

Atom - Jupyter / Hydrogen

Design Patterns

Robson Koji Moriya disambiguation name