Перейти к содержанию

SOLID

SOLID - это акроним, который обозначает пять основных принципов применимых к ООП.

Примеры буду писать на питоне, так как я на нём пишу, лол.

Single-responsibility principle

Это такой принцип, который означает, что каждый объект должен иметь только одну ответственность и всё поведение класса должно быть направлено исключительно на обеспечение этой ответственности.

Допустим, возьмем класс который генерирует отчеты и отправляет их на email:

class ReportGenerator:

    def __init__(self):
        self._report: Optional[Report] = None

    def _add_header(self):
        self._report.add("some header")

    def _add_content(self, content: str):
        self._report.add(content)

    def _send(self):
        mailsender.auth("something")
        mailsender.send(self._report)

    def generate(self):
        self._add_header()
        self._add_content("test")
        self._send(self._report)

Такой класс может поменяться по двум причинам - при изменении формата генерации отчета и при изменении механизма отправки. Я думаю, понятно, что этот класс отвечает сразу за 2 вещи, поэтому исходя из SRP их следует разделить:

class ReportSender:

    def send(self, report: Report):
        mailsender.auth("something")
        mailsender.send(report)

class ReportGenerator:

    def __init__(self):
        self._header = "some header"
        self._content_logger = Logger()

    def _add_header(self, report: Report):
        report.add(self._header)

    def _add_content(self, report: Report, content: str):
        report.add(content)
        self._content_logger(content)

    def generate(self) -> Report:
        report = Report()
        self._add_header(report)
        self._add_content(report, "test")
        return self._report

Open–closed principle

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

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

class ReportSender(ABC):

    @abstractmethod
    def send(self, report: Report):
        ...

class EmailReportSender(ReportSender):

    def send(self, report: Report):
        print("Отправили отчет на мыло")

class TelegramReportSender(ReportSender):

    def send(self, report: Report):
        print("Отправили отчет в телегу")

Теперь мы можем создавать новые сущности путем наследования.

Liskov Substitution Principle

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

А если проще, то это значит следующее: допустим, у нас есть класс A и отнаследованный класс B. Если мы заменим все использования класса A на класс B, то ничего не должно измениться, так как класс B всего лишь расширяет класс A. Если нет - вы нарушаете LSP.

А теперь к коду. Допустим у нас есть те же самые классы с отчетами, который были в предыдущем принципе. Добавим несколько новых классов - PrintReportSender и функцию send, которая будет принимать экземпляр класса для осуществления отправки:

class ReportSender(ABC):

    @abstractmethod
    def send(self, report: Report):
        ...

class TelegramReportSender(ReportSender):

    def send(self, report: Report):
        print("Отправили отчет в телегу")

class PrintReportSender:
    # заметили, что мы не унаследываемся от ReportSender?
    def print(self, report: Report):
        print(report)

def send(sender: ReportSender, report: Report):
    sender.send(report)
    print("отправлено")

report = Report()
# Вот тут всё ок, наш telegram_report_sender имеет метод send
telegram_report_sender = TelegramReportSender()
send(telegram_report_sender, report)

# пробуем заменить его на print_report_sender...
print_report_sender = PrintReportSender()
send(print_report_sender, dummy) # всё сломалось

В итоге, telegram_report_sender отработает без проблем, так как он является подтипом ReportSender и реализует метод send, а когда мы вызовем наш print_report_sender всё сломается, так как он не является подтипом ReportSender, ибо не имеет метода send (а только print).

Interface Segregation Principle

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

Самый частый пример - это система ввода вывода. Сделаем класс, который её будет описывать:

class IODevice(ABC):

    @abstractmethod
    def input(self, data: Any):
        ...

    @abstractmethod
    def output(self, data: Any):
        ...

И попробуем сделать клавиатуру наследуясь от этого интерфейса:

class Keyboard(IODevice):

    def input(self, data: Any):
        print("Ну тут понятно, читаем что нам передает клавиатура")

    def output(self, data: Any):
        # а тут что делаем? этот метод явно здесь лишний

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

class InputDevice(ABC):

    @abstractmethod
    def input(self, data: Any):
        ...

class OutputDevice(ABC):

    @abstractmethod
    def output(self, data: Any):
        ...

class Keyboard(InputDevice):

    def input(self, data: Any):
        print("Ну тут понятно, читаем что нам передает клавиатура")

class Monitor(InputDevice, OutputDevice):

    def input(self, data: Any):
        print("Обрабатываем нажатие кнопок на мониторе")

    def output(self, data: Any):
        print("Выводим изображение")

Dependency Inversion Principle

Принцип инверсии зависимостей говорит о том, что классы должны зависеть от абстракций, а не от конкретных реализаций. Если переводить на понятный, то не стоит создавать объекты в классе/функции которая использует эти объекты. Лучше передавать уже готовые. Это уменьшает связанность между классами, делает их более гибкими и более тестируемыми.

Вернёмся к отчётам. Возьмем класс, который имеет сервис отправки нотификаций:

class ReportSender(ABC):

    @abstractmethod
    def send(self, report: Report):
        ...

class TelegramReportSender(ReportSender):

    def send(self, report: Report):
        print("Отправили отчет в телегу")

class EmailReportSender(ReportSender):

    def send(self, report: Report):
        print("Отправили отчет на мыло")

class UserService:

    def __init__(username: str)
        self._username = username
        self._report_sender = TelegramReportSender()

В этом классе мы зависим от конкретной реализации ReportSender'а - от TelegramReportSender. Чтобы такого не было, мы можем передавать реализации ReportSender'а в конструктор:

class UserService:

    def __init__(username: str, report_sender: ReportSender)
        self._username = username
        self._report_sender = report_sender

user_service_with_email = UserService("kiriharu", EmailReportSender())
user_service_with_tg = UserService("kiriharu", TelegramReportSender())