Как изменение интерфейса в классе-наследнике связано с принципом открытости/закрытости (OCP)?

Ответ

Изменение публичного интерфейса (например, сигнатуры метода) в классе-наследнике является прямым нарушением Принципа подстановки Барбары Лисков (LSP), который тесно связан с Принципом открытости/закрытости (OCP).

OCP гласит, что сущности должны быть открыты для расширения, но закрыты для изменения. Когда наследник меняет контракт родителя, любой код, рассчитанный на работу с базовым классом, сломается при передаче ему экземпляра класса-наследника. Это заставляет изменять существующий код, что нарушает OCP.

Неправильный подход (Нарушение LSP и OCP)

class Notifier:
    def send(self, message: str):
        print(f"Sending: {message}")

class EmailNotifier(Notifier):
    # Нарушение: изменена сигнатура, добавлен обязательный параметр `recipient`
    def send(self, message: str, recipient: str):
        print(f"Sending email to {recipient}: {message}")

# Этот код сломается, если notifier будет экземпляром EmailNotifier
def broadcast(notifier: Notifier, message: str):
    notifier.send(message) # TypeError: send() missing 1 required positional argument: 'recipient'

Правильный подход (Соблюдение принципов)

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

class Notifier:
    def send(self, message: str, **kwargs):
        print(f"Sending: {message}")

class EmailNotifier(Notifier):
    # Расширение: используется опциональный параметр
    def send(self, message: str, **kwargs):
        recipient = kwargs.get('recipient')
        if recipient:
            print(f"Sending email to {recipient}: {message}")
        else:
            super().send(message, **kwargs)

# Этот код будет работать с любым наследником Notifier
def broadcast(notifier: Notifier, message: str):
    notifier.send(message)

Ключевая идея: наследник должен полностью соответствовать контракту родителя, чтобы его можно было безопасно подставить вместо родительского объекта.