Статьи
July 20, 2022

Принцип подстановки Барбары Лисков (LSP)

Поговорим о третьем из пяти основных принципов объектно-ориентированного программирования:

S — принцип единой ответственности (Single responsibility Principle)

O — принцип открытости / закрытости (Open-closed Principle)

L — принцип подстановки Барбары Лисков (Liskov Substitution Principle)

I — принцип разделения интерфейса (Interface Segregation Principle)

D — принцип инверсии зависимостей (Dependency Inversion Principle)

Принцип подстановки Лисков гласит: дочерний класс должен быть написан так, чтобы он мог заменить родительский. Таким образом дочерний класс мог бы занять место своего родительского класса, не вызывая никаких ошибок.

Рассмотрим следующий пример:

from abc import ABC, abstractmethod


class Notification(ABC):
    @abstractmethod
    def notify(self, message, email):
        pass


class Email(Notification):
    def notify(self, message, email):
        print(f'Send {message} to {email}')


class SMS(Notification):
    def notify(self, message, phone):
        print(f'Send {message} to {phone}')


if __name__ == '__main__':
    notification = SMS()
    notification.notify('Hello', '[email protected]')

У нас есть три класса: Notification, Email и SMS. Классы Email и SMS наследуются от класса Notification.

В абстрактном классе Notification есть метод notify(), который отправляет сообщение на электронную почту.

Метод notify() в классе Email отправляет сообщение на электронную почту, что вполне нормально.

Однако в классе SMS для отправки сообщения используется номер телефона, а не адрес электронной почты. Поэтому нам нужно изменить сигнатуру метода notify() класса SMS, чтобы он принимал номер телефона вместо электронной почты.

Следующий класс NotificationManager использует объект Notification для отправки сообщения контакту (Contact):

class Contact:
    def __init__(self, name, email, phone):
        self.name = name
        self.email = email
        self.phone = phone


class NotificationManager:
    def __init__(self, notification, contact):
        self.contact = contact
        self.notification = notification

    def send(self, message):
        if isinstance(self.notification, Email):
            self.notification.notify(message, contact.email)
        elif isinstance(self.notification, SMS):
            self.notification.notify(message, contact.phone)
        else:
            raise Exception('The notification is not supported')


if __name__ == '__main__':
    contact = Contact('Some Name', '[email protected]', '8-999-999-9999')
    notification_manager = NotificationManager(SMS(), contact)
    notification_manager.send('Hello!')

Метод send() класса NoticationManager принимает некое уведомление и проверяет, является ли оно экземпляром класса Email или класса SMS. После метод передает электронную почту и телефон контакта в метод notify() соответственно.

Делаем всё по канонам ООП

Для начала переопределим метод notify() класса Notification так, чтобы он не включал параметр email:

class Notification(ABC):
    @abstractmethod
    def notify(self, message):
        pass

Затем добавляем параметр email в метод __init__ класса Email:

class Email(Notification):
    def __init__(self, email):
        self.email = email

    def notify(self, message):
        print(f'Send "{message}" to {self.email}')

Добавляем параметр phone в метод __init__ класса SMS:

class SMS(Notification):
    def __init__(self, phone):
        self.phone = phone

    def notify(self, message):
        print(f'Send "{message}" to {self.phone}')

Изменяем класс NotificationManager:

class NotificationManager:
    def __init__(self, notification):
        self.notification = notification

    def send(self, message):
        self.notification.notify(message)

Просто как 0, 1, 2, 3. Собираем всё в кучу:

from abc import ABC, abstractmethod


class Notification(ABC):
    @abstractmethod
    def notify(self, message):
        pass


class Email(Notification):
    def __init__(self, email):
        self.email = email

    def notify(self, message):
        print(f'Send "{message}" to {self.email}')


class SMS(Notification):
    def __init__(self, phone):
        self.phone = phone

    def notify(self, message):
        print(f'Send "{message}" to {self.phone}')


class Contact:
    def __init__(self, name, email, phone):
        self.name = name
        self.email = email
        self.phone = phone


class NotificationManager:
    def __init__(self, notification):
        self.notification = notification

    def send(self, message):
        self.notification.notify(message)


if __name__ == '__main__':
    contact = Contact('John Doe', '[email protected]', '(408)-888-9999')

    sms_notification = SMS(contact.phone)
    email_notification = Email(contact.email)

    notification_manager = NotificationManager(sms_notification)
    notification_manager.send('Hello John')

    notification_manager.notification = email_notification
    notification_manager.send('Hi John')

Заключение

Принцип подстановки Барбары Лисков гласит: дочерний класс должен быть написан так, чтобы он мог заменить свой родительский класс, не вызывая при этом ошибок.

Источник: Python Tutorial
Перевод и адаптация: Екатерина Прохорова

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶