Как изменение интерфейса в классе-наследнике связано с принципом открытости/закрытости (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)

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

Ответ 18+ 🔞

Да ты посмотри, какая дичь творится в мире наследования, блядь! Прямо как в том анекдоте про Герасима и Муму — вроде всё просто, а потом раз — и контракт нарушил, всё к чертям собачьим!

Вот представь, есть у тебя базовый класс Notifier. Он как тот самый Герасим, простой и понятный: «Муму!» — и всё, отправил сообщение. Сигнатура у него простая — send(message: str). И все его знают и любят.

А потом появляется его наследничек, EmailNotifier. И этот пидрила, блядь, решает, что он умнее всех! Вместо того чтобы тихо-мирно расширять функциональность, он выкатывает свою версию метода send(message: str, recipient: str). Смотри-ка, обязательный параметр добавил! Это ж чистый Принцип подстановки Лисков (LSP) в мусорку, ёпта! А за ним и Принцип открытости/закрытости (OCP) накрывается медным тазом.

Почему? Да потому что весь существующий код, который заточен под работу с Notifier, ебётся в сраку! Ему передают EmailNotifier, а он пытается вызвать старый добрый send с одним аргументом — и получает TypeError прямо в ебало! «А где, сука, recipient?» — кричит интерпретатор. И тебе теперь приходится лезть и переписывать кучу старого кода. Нарушение 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'

Видишь? Функция broadcast думает, что работает с любым Notifier. А тут вылезает этот EmailNotifier со своими выебонами и всё ломает. Доверия к такому коду — ноль ебать!

А как надо-то, спросишь? А надо, блядь, как умный человек! Наследник должен быть полной заменой родителя. Хочешь добавить фичу — делай это аккуратно, не ломая старый контракт. Используй, например, **kwargs для опциональных параметров. Пусть старый код работает как работал, а новый — использует дополнительные возможности.

Смотри, как красивше:

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)

Вот теперь — красота! EmailNotifier ведёт себя как полная замена Notifier. Если ему не дали recipient — он либо шлёт как обычный нотифаер, либо как-то ещё по-умолчанию. Старый код broadcast работает и не парится. Новый код может передавать recipient. Все довольны, принципы не нарушены. LSP и OCP ликуют, как сука!

Запомни, чувак: наследник — это не повод выёбываться и менять правила игры. Это как с Муму — можно было найти другой выход, а не топиться сразу. Так и тут — расширяй, но не ломай. И будет тебе счастье, а не пиздец в продакшене.