OCP - принцип открытости и закрытости

Описание
Класс должен быть открыть для расширения, но закрыт для модификации.

Принцип гласит, что добавление функциональности должно осуществляться через расширение, а не через модификацию.

Основная цель принципа — помочь разработать проект, устойчивый к изменениям, срок жизни которых превышает срок существования первой версии проекта. Вносить изменения бывает трудно или дорого — когда небольшое изменение в одной части системы вызывает лавину изменений в других частях.
Пример
Опишем классы для создания объекта продукт Product.

Опишем классы перечисления:
- цвета
- размера

from enum import Enum
from dataclasses import dataclass

class Color(Enum):
    RED = 1
    YELLOW = 2
    GREEN = 3

class Size(Enum):
    SMALL = 1
    MEDIUM = 2
    LARGE = 3

@dataclass
class Product:
    name: str
    color: Color
    size: Size
Пример
Опишем класс фильтрации продуктов по цвету, а в дальнейшем и по размеру.

@dataclass
class ProductFilter:
    def filter_by_color(self, products, color):
        for product in products:
            if product.color == color:
                yield product
    
    def filter_by_size(self, products, size):
        for product in products:
            if product.size == size:
                yield product
Описания принципа
При добавлении в класс дополнительных принципов нарушается принцип открытости/закрытости.

Принцип гласит, что добавление функциональности должно осуществляться через расширение, а не через модификацию.

Рассмотрим пример, когда необходимо будет создать компбинацию фильтров по цвету, размеру и т. д.
Данный подход не является расширяемым, поэтому опишем новый подход.

Необходимо создать класс спецификации фильтра и непосредственно сам фильтр.

Опишем корпоративный шаблон Specification

from abc import ABC, abstractmethod

class Specification(ABC):
    @abstractmethod
    def is_satisfied(self, item):
        pass
 
class Filter(ABC):
    @abstractmethod
    def filter(self, items, spec):
        pass
Опишем непосредственно примеры спецификаций цвета и размера.

@dataclass
class ColorSpecification(Specification):
    color: Color

    def is_satisfied(self, item) -> bool:
        return item.color == self.color

@dataclass
class SizeSpecification(Specification):
    size: Size

    def is_satisfied(self, item) -> bool:
        return item.size == self.size
Опишем классы фильтров.

@dataclass
class BetterFilter(Filter):
    def filter(self, items, spec):
        for item in items:
            if spec.is_satisfied(item):
                yield item
Описание
Теперь при изменении требований к фильтрации Вам необходимо дописать спецификацию и по необходимости фильтрацию.
Пример создания объектов

apple = Product('Apple', Color.GREEN, Size.SMALL)
tree = Product('Tree', Color.GREEN, Size.LARGE)
tomat = Product('Tomat', Color.RED, Size.SMALL)

products = [apple, tree, tomat]

bf = BetterFilter()
green = ColorSpecification(Color.GREEN)
for p in bf.filter(products, green):
    print(f'- {p.name} is green')
Написание комбинатора фильтра
Теперь необходимо описать класс комбинатор спецификаций для фильтрации. Данный класс может принимать любое количество спецификаций и проверять его на соответствие.
Комбинатор спецификации

@dataclass
class СombinatorSpecification(Specification):
    def __init__(self, *args):
        self.args = args 

    def is_satisfied(self, item) -> bool:
        return all(map(
            lambda spec: spec.is_satisfied(item), self.args
        ))
Использование комбинатора

if __name__ == '__main__':
   ...

    print('Проверка нескольких параметров фильтра - комбинатор')
    small_green = СombinatorSpecification(SizeSpecification(Size.SMALL),
                                          ColorSpecification(Color.GREEN))
    for p in bf.filter(products, small_green):
        print(f'- {p.name} is small and green')
Выводы
Принцип открытости-закрытости:

  • заставляет проектировать модули так, чтобы они делали только одну вещь;
  • побуждает связывать сущности через абстракции (а не реализацию) там, где могут поменяться бизнес-требования;
  • обращает внимание проектировщиков на места стыка и взаимодействие сущностей;
  • позволяет сократить количество кода, который необходимо менять при изменении бизнес-требований;
  • делает внесение изменений безопасным и относительно дешёвым.