Изображение — Fachrizal Maulana — Unsplash.com

Привет, Хабр! Меня зовут Павел Корсаков, я python-разработчик в облачном провайдере beeline cloud.

Почти на всех собеседованиях задают вопросы про SOLID:

  • Что это такое?

  • Зачем нужен?

  • Как его применяет кандидат?

  • Как понимает принципы из него?

Мы тоже спрашиваем про SOLID, потому что он часто выступает аргументом на ревью. Знание принципов помогает снять градус накала в комментариях под мёрдж-реквестом.

Но вернемся к кандидатам. Чаще всего они рассказывают, что SOLID — это акроним, озвучивают все его принципы, но объяснить и привести примеры могут лишь для половины. На остальных либо плавают, либо сливаются.

Интернет предлагает множество статей про SOLID как на русском, так и на английском языке. В тех статьях, что я видел, подача информации одинаковая. Именно в этом кроется причина, почему многие получают фрагментированные знания по SOLID, которые не собираются в единую картину. 

Принципы SOLID разбираются в последовательности по буквам S, O, L, I, D, что неверно. Акроним сам по себе мнемонический и больше нужен для запоминания, а не для того, чтобы именно в таком порядке разбираться. К тому же такой «последовательный подход» не дает полного понимания SOLID. А вот подача в логической последовательности с закрепляющими примерами и комментариями помогает собрать нужную картинку. Это основной посыл, которым я руководствовался при написании статьи.

Чтобы мой материал не получился очередной статьей про SOLID, я изменю формат подачи и последовательность объяснения принципов. Буду добавлять код небольшими инкрементами и на каждом из них указывать, какие принципы SOLID используются в том или ином случае. 

Это авторский текст (не перевод) с примерами, которые я обычно использую для объяснения принципов SOLID. Приведу только пару ссылок на Википедию про SOLID и сошлюсь на первоисточник butunclebob.com.

Принцип инверсии зависимостей

В моем идеальном мире SOLID начинается с принципа инверсии зависимостей. Википедия нам дает такое определение:

Классы должны зависеть от абстракций, а не от конкретных деталей.

A. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Wiki

Это определение совершенно.

«Совершенство достигается не тогда, когда нечего добавить, а когда нечего убрать».

Антуан де Сент-Экзюпери

Я осознанно начинаю с последнего принципа, поскольку это первое, с чем приходится сталкиваться при написании кода. Если у вас нет зависимости на абстракциях, то SOLID не будет полноценным и понять его значительно сложнее.
from abc import ABC, abstractmethod


class AbstractAuthUser(ABC):
    """Абстрактный класс, реализующий обязательные методы."""

    @abstractmethod
    def is_authenticated(self) -> bool:
        """
        Метод проверяет аутентификацию пользователя.
        Возвращает True, если аутентифицирован, и False — если не аутентифицирован
        """

    @abstractmethod
    def get_email(self) -> str:
        """Метод возвращает email пользователя"""

    @abstractmethod
    def get_department(self) -> str:
        """Метод возвращает отдел, в котором работает пользователь"""

Давайте разбираться по порядку. ABC — это класс-помощник, который указывает метакласс metaclass=ABCMeta в качестве параметров класса. 

Вариант class AbstractAuthUser(metaclass=abc.ABCMeta) тоже рабочий, но Python предлагает нам синтаксический сахар, и мы его используем. Оставим первый вариант.

Декоратор abstractmethod гарантирует, что у дочернего класса будут все методы, которые декорированы этим декоратором. Им нужно оборачивать все методы, которые будет использовать бизнес-логика. Разработчики, которые будут обращаться к классу аутентификации, могут быть уверены, что у него всегда есть методы is_authenticated, get_email, get_department, потому что они декорированы abstractmethod, а значит, обязательны для реализации в дочерних классах, унаследованных от абстрактного.

Вот так это выглядит:
from abc import ABC, abstractmethod


class AbstractAuthUser(ABC):
    """Абстрактный класс, реализующий обязательные методы."""

    @abstractmethod
    def is_authenticated(self) -> bool:
        """
        Метод проверяет аутентификацию пользователя.
        Возвращает True, если аутентифицирован, и False — если не аутентифицирован
        """

    @abstractmethod
    def get_email(self) -> str:
        """Метод возвращает email пользователя"""

    @abstractmethod
    def get_department(self) -> str:
        """Метод возвращает отдел, в котором работает пользователь"""


class AuthUserAD(AbstractAuthUser):
    """Класс аутентификации через AD"""


auth = AuthUserAD()

# Traceback (most recent call last):
#  File "/home/pavel/Projects/solid/solid.py", line 38, in <module>
#    auth = AuthUserAD()
#           ^^^^^^^^^^^^
# TypeError: Can't instantiate abstract class AuthUserAD with abstract methods get_department, get_email, is_authenticated

Давайте их объявим.
from abc import ABC, abstractmethod


class AbstractAuthUser(ABC):
    """Абстрактный класс, реализующий обязательные методы."""

    @abstractmethod
    def is_authenticated(self) -> bool:
        """
        Метод проверяет аутентификацию пользователя.
        Возвращает True, если аутентифицирован, и False — если не аутентифицирован
        """

    @abstractmethod
    def get_email(self) -> str:
        """Метод возвращает email пользователя"""

    @abstractmethod
    def get_department(self) -> str:
        """Метод возвращает отдел, в котором работает пользователь"""


class AuthUserAD(AbstractAuthUser):
    """Класс аутентификации через AD"""
    
    def is_authenticated(self) -> bool:
        raise NotImplementedError()

    def get_email(self) -> str:
        raise NotImplementedError()

    def get_department(self) -> str:
        raise NotImplementedError()


auth = AuthUserAD()

В абстрактном классе стоит указывать методы, которые будут вызываться извне. Это своего рода API класса (компонента). В свою очередь методы, которые не должны использоваться извне, не следует указывать в абстрактном классе. При реализации AuthUserAD методы, не являющиеся частью API класса, можно пометить одним подчеркиванием в начале имени. PEP8

Теперь у нас есть абстрактный класс. От него зависит класс, реализующий логику. Рекомендация, что классы должны зависеть от абстракций, реализована.

Если вы в своем коде уже используете абстрактные классы — скорее всего, в коде с SOLID все хорошо. Ниже, при описании остальных принципов, мы часто будем возвращаться к этому принципу.

Принципы открытости/закрытости

Википедия описывает два подхода к пониманию этих принципов. Рассмотрим их подробнее.

  • Принцип открытости/закрытости Мейера

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

Определение Мейера поддерживает идею наследования реализации. Реализация может быть переопределена через наследование, но спецификации интерфейса могут измениться. Существующая реализация должна быть закрыта для изменений, при этом новым реализациям не обязательно использовать существующий интерфейс*.

  • Полиморфный принцип открытости/закрытости

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

Wiki

*Что такое интерфейс — подробно разберем в принципе разделения интерфейса.

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

Но такой подход идет вразрез с принципом подстановки Лисков (о нем ниже). У себя в коде мы используем полиморфный принцип открытости/закрытости. В тексте и в примерах я описываю именно его.

Давайте предположим, что AuthUserAD — это жуткое легаси, которому лет шесть. И вариантов, чтобы кто-то дал свои комментарии, тоже нет. 

AuthUserAD регулярно DDoS-ит и роняет сервер аутентификации. Админ предлагает дешевое решение — поднять несколько инстансов Keycloak. Инстансы Keycloak будут ходить в AD по расписанию, получать список всех пользователей и брать на себя нагрузку, которая лежала на AD.

Вносить изменения в класс AuthUserAD — плохая идея. Трудно спрогнозировать риски, которые могут произойти от новых изменений. К тому же после внесения изменений его захочется переименовать, а это равносильно удалению класса. Если мы не исправим все места, где он вызывается по старому имени, то гарантированно уроним код.

Сейчас самое время вспомнить о принципе открытости/закрытости. Хорошей идеей будет не трогать класс AuthUserAD. Лучше сделать его осуждаемым и написать новый класс для работы с новой схемой аутентификации. В таком классе вместо ошибок из Keycloak будем райзить кастомные ошибки.

Сделать класс осуждаемым — что это значит

Немного подробнее о том, что я понимаю под фразой «сделать класс осуждаемым». В фреймворках встречается такое поведение: когда мы используем метод, в логи валится warning. Это говорит о том, что метод осуждаемый и перестанет поддерживаться с такой-то версии Python или с такой-то версии библиотеки. Я предлагаю тут сделать то же самое. Когда мы будем создавать инстанс осуждаемого класса, к нам в логи (и в Sentry) будет прилетать ошибка, которая указывает, что где-то используется осуждаемый класс.

Реализуем это, немного поправив метод init:
from abc import ABC, abstractmethod
import logging

logger = logging.getLogger('root')


class AbstractAuthUser(ABC):
    """Абстрактный класс, реализующий обязательные методы."""

    @abstractmethod
    def is_authenticated(self) -> bool:
        """
        Метод проверяет аутентификацию пользователя.
        Возвращает True, если аутентифицирован, и False — если не аутентифицирован
        """

    @abstractmethod
    def get_email(self) -> str:
        """Метод возвращает email пользователя"""

    @abstractmethod
    def get_department(self) -> str:
        """Метод возвращает отдел, в котором работает пользователь"""


class AuthUserAD(AbstractAuthUser):
    """Класс аутентификации через AD"""

    def __init__(self, *args, **kwargs):
        logger.error('Class AuthUserAD is deprecated. You should use AuthUserKeycloak.')
        super().__init__(*args, **kwargs)

    def is_authenticated(self) -> bool:
        raise NotImplementedError()

    def get_email(self) -> str:
        raise NotImplementedError()

    def get_department(self) -> str:
        raise NotImplementedError()


class AuthUserKeycloak(AbstractAuthUser):
    """Класс аутентификации через Keycloak"""
    
    def is_authenticated(self) -> bool:
        raise NotImplementedError()

    def get_email(self) -> str:
        raise NotImplementedError()

    def get_department(self) -> str:
        raise NotImplementedError()

Если мы где-то не заменили класс AuthUserAD на AuthUserKeycloak, у нас все продолжит работать, поскольку мы не удалили класс AuthUserAD. Он все еще присутствует в коде и работает лучше, чем прежде, поскольку нагрузка на AD уменьшится. При каждом создании инстанса AuthUserAD мы будем видеть ошибку в логах и постепенно безболезненно выпилим его из возможных мест. Принцип открытости/закрытости работает на нас.

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

Теперь у нас есть абстрактный класс AbstractAuthUser. От него зависят оба класса реализации AuthUserAD и AuthUserKeycloak. Причем нам не важны детали реализации каждого конкретного класса. Классу AuthUserKeycloak все равно, как реализован класс AuthUserAD и реализован ли он вообще. 

Также у нас абстракция не зависит от деталей: классу AbstractAuthUser не важны детали реализации дочерних классов AuthUserAD на AuthUserKeycloak, например класс с заглушками для тестов AuthTest. Более того, мы добавили новый функционал в классе AuthUserKeycloak, не меняя закрытый для изменения класс AuthUserAD.

Налицо соблюдение принципа открытости/закрытости.
class AuthTest(AbstractAuthUser):
    """Класс аутентификации для тестов с аутентифицированным пользователем"""
    
    def is_authenticated(self) -> bool:
        """Метод возвращает значение, необходимое для тестов"""
        return True

    def get_email(self) -> str:
        """Метод возвращает значение, необходимое для тестов"""
        return 'skyworker@jedi.com'

    def get_department(self) -> str:
        """Метод возвращает значение, необходимое для тестов"""
        return 'Департамент света'


class NoAuthTest(AbstractAuthUser):
    """Класс аутентификации для тестов с неаутентифицированным пользователем"""
    
    def is_authenticated(self) -> bool:
        """Метод возвращает значение, необходимое для тестов"""
        return False

    def get_email(self) -> str:
        """Метод возвращает значение, необходимое для тестов"""
        return ''

    def get_department(self) -> str:
        """Метод возвращает значение, необходимое для тестов"""
        return ''

Принцип подстановки Лисков

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

Wiki

Я считаю, что принцип подстановки Лисков — сердце SOLID и то, ради чего SOLID задумывался. Этот принцип связан с тем, что я описал выше. 

В классах, помимо обязательных методов, описанных в абстрактном классе, могут быть свои методы. В них могут отличаться сигнатуры, названия методов и реализация (как же без полиморфизма). Например, для http-запроса на сервис аутентификации могут быть реализованы разные методы для проверки токена, получения почты и названия отдела. Это не противоречит SOLID. Главное, чтобы бизнес-логика работала только с методами из абстрактного класса и сигнатуры методов, с которыми будет работать бизнес-логика, были одинаковыми.

Может показаться неочевидным, но в Python для принципа подстановки Лисков недостаточно ограничиваться методами, описанными в абстрактном классе. Если классы AuthUserAD и AuthUserKeycloak рейзят разные ошибки, это значит, что классы не удовлетворяют принципу подстановки Лисков

В контексте SOLID использование кастомных исключений — больше, чем просто хорошая практика. Это расширение понимания принципа подстановки Лисков. Мы знаем, какие ошибки могут возникнуть, и если вы напишете свои кастомные ошибки — это будет отличной идеей. То есть не пробрасывать ошибки из AD и Keycloak, а обрабатывать их в классе и рейзить свои исключения.

class AuthException(Exception):
    pass


class InvalidCredential(AuthException):
    pass


class AuthenticationServerIsNotAvailable(AuthException):
    pass

Поскольку новый класс AuthUserKeycloak реализует такие же обязательные методы, как и AuthUserAD и рейзит те же самые ошибки, то в эндпоинте (или middleware), где использовался класс AuthUserAD или его инстансы, мы можем безболезненно заменить его на новый AuthUserKeycloak. Принцип подстановки Лисков работает при такой реализации. 

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

Небольшое замечание. Декоратор abstractmethod гарантирует, что обязательные методы будут созданы, но он позволяет создать их с различными сигнатурами. А для принципа подстановки Лисков важно, чтобы сигнатуры были одинаковые или хотя бы совместимые. Без этого заменить базовый класс дочерним или один дочерний класс другим дочерним не получится.

Возможно, кто-то скажет, что принцип подстановки Лисков связан с базовым и дочерним типами. И определение в Википедии говорит об этом:

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

Но нам ничего не мешает сделать наследование таким образом:
class AuthUserAD(AbstractAuthUser):
    """Класс аутентификации через AD"""

    def __init__(self, *args, **kwargs):
        logger.error('Class AuthUserAD is deprecated. You should use AuthUserKeycloak.')
        super().__init__(*args, **kwargs)

    def is_authenticated(self) -> bool:
        raise NotImplementedError()

    def get_email(self) -> str:
        raise NotImplementedError()

    def get_department(self) -> str:
        raise NotImplementedError()


class AuthUserKeycloak(AuthUserAD):
    """Класс аутентификации через Keycloak"""
    
    def is_authenticated(self) -> bool:
        raise NotImplementedError()

Поскольку AuthUserKeycloak реализует такие же обязательные методы, как и AuthUserAD, это никак не влияет на возможность использования одного класса вместо другого. Если, например, реализация методов get_email и get_department в AuthUserKeycloak такая же, как и в AuthUserAD, такое наследование возможно.

Если же у AuthUserKeycloak своя реализация методов API класса — такое наследование не нужно. Кроме того, мы получим нежелательный побочный эффект в экземпляре метода AuthUserKeycloak — нам доступны все методы базового класса.

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

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

Принцип единственной ответственности

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

Все его поведения должны быть направлены исключительно на обеспечение этой ответственности.

Wiki

Здесь все более прозрачно. У класса AuthUserKeycloak может быть только одна причина для внесения изменений — изменения в аутентификацию или в работу с Keycloak. Если вы вносите изменения в класс для добавления новых пермишенов или для изменения способа сохранения, это уже дополнительные причины, а по рассматриваемому нами принципу причина должна быть одна. Поэтому авторизацию и работу с пермишинами необходимо вынести в отдельный класс, чтобы замена AuthUserKeycloak обратно на AuthUserAD не влияла на пермишены и на то, какие действия для пользователя разрешены или запрещены.

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

Хорошо, если для хранилища создать абстрактный класс, а от него уже наследовать дочерние классы. Например, для хранения в файле, в SQL базах данных через ORM, в NoSQL базах или другие варианты, а инстанс для нужного способа хранения использовать в AuthUserKeycloak. Я обычно использую один абстрактный класс и класс для ORM.

Он-то и используется для хранения, например StoreDB.
from abc import ABC, abstractmethod


class AbstractStore(ABC):
    """Абстрактный класс для хранения"""
    
    @abstractmethod
    def get(self, *args, **kwargs):
        pass

    @abstractmethod
    def get_multi(self, *args, **kwargs):
        pass

    @abstractmethod
    def create(self, *args, **kwargs):
        pass

    @abstractmethod
    def update(self, *args, **kwargs):
        pass

    @abstractmethod
    def delete(self, *args, **kwargs):
        pass


class StoreFile(AbstractStore):
    """Класс для хранения в файле"""
    
    def get(self, *args, **kwargs):
        raise NotImplementedError

    def get_multi(self, *args, **kwargs):
        raise NotImplementedError

    def create(self, *args, **kwargs):
        raise NotImplementedError

    def update(self, *args, **kwargs):
        raise NotImplementedError

    def delete(self, *args, **kwargs):
        raise NotImplementedError


class StoreDB(AbstractStore):  # Для ORM
    """Класс для хранения в SQL БД"""
    
    def get(self, *args, **kwargs):
        raise NotImplementedError

    def get_multi(self, *args, **kwargs):
        raise NotImplementedError

    def create(self, *args, **kwargs):
        raise NotImplementedError

    def update(self, *args, **kwargs):
        raise NotImplementedError

    def delete(self, *args, **kwargs):
        raise NotImplementedError


class StoreMongo(AbstractStore):
    """Класс для хранения в NoSQL БД"""
    
    def get(self, *args, **kwargs):
        raise NotImplementedError

    def get_multi(self, *args, **kwargs):
        raise NotImplementedError

    def create(self, *args, **kwargs):
        raise NotImplementedError

    def update(self, *args, **kwargs):
        raise NotImplementedError

    def delete(self, *args, **kwargs):
        raise NotImplementedError

Принцип разделения интерфейса

И последний незатронутый принцип. Чтобы его понять, нужно определиться с понятием интерфейса. Дело в том, что в Python нет явного понятия интерфейса, как в других языках программирования.

Если говорить своими словами, то интерфейс — это абстракция, которая позволяет определить методы без конкретной реализации. И если в классе присутствуют все методы интерфейса, можно считать, что класс реализует интерфейс.

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

Приведу пример с абстрактным классом для коллектора:
from abc import ABC, abstractmethod


class AbstractCollector(ABC):
    """Абстрактный класс коллектора"""

    def __init__(self) -> None:
        self.metrics = []
        self.prepared_metrics = []

    def collect(self) -> None:
        self.get_metrics_from_service()
        self.process_metrics()
        self.save_metrics()

    @abstractmethod
    def get_metrics_from_service(self) -> None:
        """Extract."""

    @abstractmethod
    def process_metrics(self) -> None:
        """Transform."""

    @abstractmethod
    def save_metrics(self) -> None:
        """Load."""


class AbstractStore(ABC):
    """Абстрактный класс для хранения"""
        
    @abstractmethod
    def get(self, *args, **kwargs):
        pass

    @abstractmethod
    def get_multi(self, *args, **kwargs):
        pass

    @abstractmethod
    def create(self, *args, **kwargs):
        pass

    @abstractmethod
    def update(self, *args, **kwargs):
        pass

    @abstractmethod
    def delete(self, *args, **kwargs):
        pass


class AbstractReadOnlyStore(ABC):
    """Абстрактный класс для чтения их хранилища."""
    
    @abstractmethod
    def get(self, *args, **kwargs):
        pass

    @abstractmethod
    def get_multi(self, *args, **kwargs):
        pass


class GetStoreDB:
    """Класс для получения объекта из БД"""
    
    def get(self, *args, **kwargs):
        raise NotImplementedError

    def get_multi(self, *args, **kwargs):
        raise NotImplementedError


class CreateStoreDB:
    """Класс для создания объекта в БД"""
    
    def create(self, *args, **kwargs):
        raise NotImplementedError


class UpdateStoreDB:
    """Класс для изменения объекта в БД"""
    
    def update(self, *args, **kwargs):
        raise NotImplementedError


class DeleteStoreDB:
    """Класс для удаления объекта в БД"""
    
    def delete(self, *args, **kwargs):
        raise NotImplementedError


class StoreReadOnlyDB(AbstractReadOnlyStore, GetStoreDB):
    """Класс для сбора метрик. Только чтение."""


class StoreDB(AbstractStore, GetStoreDB, CreateStoreDB, UpdateStoreDB, DeleteStoreDB):
    """Класс для хранения метрик."""

Прежде чем объяснять, что тут происходит, еще раз вспомним теорию.

Программные сущности не должны зависеть от методов, которые они не используют.

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

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

В итоге при изменении метода интерфейса не должны меняться программные сущности, которые этот метод не используют.

Wiki

В примере реализовано два абстрактных класса для работы с данными AbstractStore, AbstractReadOnlyStore и класс абстрактного коллектора AbstractCollector.

Метод коллектора get_metrics_from_service ходит в инфраструктуру и имеет доступ к критичным данным. В разных коллекторах данные собираются из различных источников (api, базы данных, файлы и др.). Но здесь выполняется только сбор данных (только безопасные методы). Методы, которые принято относить к опасным, в нем использоваться не будут. Поэтому при написании класса для сбора данных нужно использовать AbstractReadOnlyStore. Тогда потенциально опасные методы будут исключены на уровне интерфейса.

Метод коллектора save_metrics обычно выполняет только запись в базу данных. Получается, что для написания класса сохранения в базу достаточно реализовать только create и update. Эта реализация позволяет нам легко собрать такой класс: 

class StoreReadOnlyDB(CreateStoreDB, UpdateStoreDB):
    """Класс для записи метрик."""

Хотя это и будет наиболее соответствующий принципу разделения интерфейса класс, на практике мы так не делаем. А наследуемся от AbstractStore и в нем реализуем все методы CRUD. Можно сказать, что интерфейс с полным CRUD будет «толстым» для функционала сохранения.

Принцип разделения интерфейсов тесно связан с принципом единой ответственности, если класс StoreReadOnlyDB занимается только сбором статистики. И в соответствии с принципом единой ответственности мы не закладываем в него другой функционал. Также в соответствии с принципом разделения интерфейсов из абстрактного класса (интерфейса) AbstractReadOnlyStore убираем методы, которые не будем использовать.

Заключение

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

Надеюсь, что после прочтения статьи принципы SOLID стали для вас такими же очевидными, как для Роберта С. Мартина. Главная мысль, которую я хотел донести, это то, что все принципы тесно связаны друг с другом. Надеюсь, этот текст дополнил информацию из других источников и помог составить из нее законченную картину, понять, насколько тесно принципы связаны между собой. Я верю, что теперь вы легко ответите на все вопросы по SOLID на собеседовании и будете писать солидный код.

Все правила, в том числе и SOLID, можно нарушать. В небольших проектах строгое соблюдение всех принципов SOLID избыточно. Но программисты, которые пишут небольшие проекты, растут. Растут и их проекты — становятся большими, очень большими и архитектурно сложными. В таких проектах соблюдение принципов SOLID крайне желательно или обязательно.

Дополнительное чтение

beeline cloud — secure cloud provider. Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы.