SOLID

Время чтения: 9 мин

Описание

Секция статьи "Описание"

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

  • Single Responsibility — принцип единственной ответственности.
  • Open-Closed — принцип открытости/закрытости.
  • Liskov Substitution — принцип подстановки Барбары Лисков.
  • Interface Segregation — принцип разделения интерфейса.
  • Dependency Inversion — принцип инверсии зависимостей.

Слабая связанность означает, что модуль должен иметь как можно меньше зависимостей от других.
Такой модуль легко переиспользовать и удобно тестировать.

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

SRP (Single Responsibility): Принцип единственной ответственности

Секция статьи "SRP (Single Responsibility): Принцип единственной ответственности"

Классы должны иметь одну и только одну причину для изменений.

или

Каждый класс должен отвечать только за одну операцию.

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

        
          
          class KitchenRobot:    def choose_food(self): pass    def buy_food(self): pass    def carry_food(self): pass    def choose_dish(self): pass    def prepare_ingredients(self): pass    def cook_dish(self): pass    def set_the_table(self): pass    def wash_tableware(self): pass    def clear_kitchen(self): pass    def start(self): pass
          class KitchenRobot:
    def choose_food(self): pass
    def buy_food(self): pass
    def carry_food(self): pass

    def choose_dish(self): pass
    def prepare_ingredients(self): pass
    def cook_dish(self): pass

    def set_the_table(self): pass
    def wash_tableware(self): pass
    def clear_kitchen(self): pass

    def start(self): pass

        
        
          
        
      

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

Чтобы проверить, нарушен ли принцип SRP, попробуйте описать то, чем занимается этот класс, в одном предложении. Получится что-то вроде: «Он стирает, сушит и гладит одежду». Наличие перечисления и союзов «и» — один из признаков возможного нарушения принципа единой ответственности.

Давайте избавимся от перечисления, разбив код на несколько отдельных классов.

        
          
          class FoodSupply:    def choose_food(self): pass    def buy_food(self): pass    def carry_food(self): pass    def start(self): passclass DishCook:    def choose_dish(self): pass    def prepare_ingredients(self): pass    def cook_dish(self): pass    def start(self): passclass Waiter:    def set_the_table(self): pass    def start(self): passclass Cleaning:    def wash_tableware(self): pass    def clear_kitchen(self): pass    def start(self): passclass KitchenRobot:    def start(self): pass
          class FoodSupply:
    def choose_food(self): pass
    def buy_food(self): pass
    def carry_food(self): pass
    def start(self): pass

class DishCook:
    def choose_dish(self): pass
    def prepare_ingredients(self): pass
    def cook_dish(self): pass
    def start(self): pass

class Waiter:
    def set_the_table(self): pass
    def start(self): pass

class Cleaning:
    def wash_tableware(self): pass
    def clear_kitchen(self): pass
    def start(self): pass

class KitchenRobot:
    def start(self): pass

        
        
          
        
      

Теперь закупкой еды занимается класс FoodSupply, готовкой — класс DishCook, накрыванием на стол — класс Waiter, уборку — класс Cleaning, а за запуск процесса отвечает KitchenRobot. Если в классе FoodSupply найдётся ошибка, ваши исправления не затронут работающий код, потому что он находится в другом классе.

OCP (Open-Closed): Принцип открытости/закрытости

Секция статьи "OCP (Open-Closed): Принцип открытости/закрытости"

Программные сущности должны быть открыты для расширения, но закрыты для изменения.

Идея в том, что однажды написанный класс не должен никаким образом изменяться. Если вам требуются изменения, создайте класс-наследник и пишите код в нём.

Представим у нас есть класс, который описывает работу очереди. Там есть методы получения элемента из очереди и добавление элемента в очередь. Спустя некоторое время, у нас появилась необходимость очищать очередь. Чтобы следовать принципу Open-Closed, нам надо наследовать свойства от существующего класса и добавить туда наши новые методы поведения.

        
          
          class PrimalQueue:    def get_from_queue(self): pass    def set_in_queue(self): passclass MutateQueue(PrimalQueue):    def reset_queue(self): pass
          class PrimalQueue:
    def get_from_queue(self): pass
    def set_in_queue(self): pass


class MutateQueue(PrimalQueue):
    def reset_queue(self): pass

        
        
          
        
      

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

Секция статьи "LSP (Liskov Substitution): Принцип подстановки Барбары Лисков"

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

или

Если П является подтипом Т, то любые объекты типа Т, присутствующие в программе, могут заменяться объектами типа П без негативных последствий для функциональности программы.

Этот принцип указывает, что наследники должны уметь всё то, что умеют их родители. Вы можете быть уверены, что код, который успешно работает с классом File, будет корректно работать с его «ребёнком» PdfFileи «внуком» EncryptedPdfFile.

Пример такой реализаций, можно посмотреть в примере к принципу Open-Closed.

ISP (Interface Segregation): Принцип разделения интерфейса

Секция статьи "ISP (Interface Segregation): Принцип разделения интерфейса"

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

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

Для примера рассмотрим абстрактный класс Bird, который обязывает реализовать методы fly(), eat() и build_nest(). Создадим на его основе несколько птиц:

        
          
          from abc import ABC, abstractmethodclass Bird(ABC):    @abstractmethod    def fly(self):        pass    @abstractmethod    def build_nest(self):        pass    @abstractmethod    def eat(self):        passclass Eagle(Bird):    def fly(self):        """ Лететь быстро и высоко """    def build_nest(self):        """ Затаскивание веток на скалу """    def eat(self):        """ Поедание вкусных мясных кусочков """class Colibri(Bird):    def fly(self):        """ Лететь, выписывая «восьмёрки» """    def build_nest(self):        """ построить гнездо из травинок и пуха """    def eat(self):        """ Пить нектар """
          from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def fly(self):
        pass

    @abstractmethod
    def build_nest(self):
        pass

    @abstractmethod
    def eat(self):
        pass

class Eagle(Bird):
    def fly(self):
        """ Лететь быстро и высоко """

    def build_nest(self):
        """ Затаскивание веток на скалу """

    def eat(self):
        """ Поедание вкусных мясных кусочков """

class Colibri(Bird):
    def fly(self):
        """ Лететь, выписывая «восьмёрки» """

    def build_nest(self):
        """ построить гнездо из травинок и пуха """

    def eat(self):
        """ Пить нектар """

        
        
          
        
      

Пока всё идёт хорошо. Теперь попробуем добавить Пингвина, который не умеет летать.

        
          
          class Pinguin(Bird):    def build_nest(self):        """ Строить гнездо из камней """    def eat(self):        """ Ловить рыбу """
          class Pinguin(Bird):
    def build_nest(self):
        """ Строить гнездо из камней """

    def eat(self):
        """ Ловить рыбу """

        
        
          
        
      

При создании экземпляра Pinguin вы получите ошибку TypeError: Can't instantiate abstract class Pinguin with abstract methods fly.Абстрактный класс заставляет вас реализовывать ненужный пингвину метод. Чтобы избавиться от этой проблемы, разбейте класс на Bird, FlyingBird и NestingBird, распределив методы между ними. Теперь реализация пингвина может выглядеть так:

        
          
          class Pinguin(NestingBird, Bird):    ...
          class Pinguin(NestingBird, Bird):
    ...

        
        
          
        
      

DIP (Dependency Inversion): Принцип инверсии зависимостей

Секция статьи "DIP (Dependency Inversion): Принцип инверсии зависимостей"

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

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

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

        
          
          class Endpoint:    def __init__(self):        self.value = Cache().get_value(key="key")class Cache:    def __init__(self):        ...    def get_value(self, key: str):        return ...
          class Endpoint:
    def __init__(self):
        self.value = Cache().get_value(key="key")

class Cache:
    def __init__(self):
        ...

    def get_value(self, key: str):
        return ...

        
        
          
        
      

Высокоуровневый класс Endpoint создаёт экземпляр класса Cache. У такого кода есть несколько проблем:

  • Вы не можете заменить Cache на DummyCache или другую реализацию, не изменив класс.
  • Вы не сможете протестировать свой код, не выполнив код из связанных классов.

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

        
          
          from abc import ABC, abstractmethodclass AbstractCache(ABC):    @abstractmethod    def get_value(self, key: str):        ...class Endpoint:    def __init__(self, specific_cache: AbstractCache):        self.value = specific_cache.get_value(key="key")class Cache(AbstractCache):    def __init__(self):        ...    def get_value(self, key: str):        return ...
          from abc import ABC, abstractmethod

class AbstractCache(ABC):
    @abstractmethod
    def get_value(self, key: str):
        ...

class Endpoint:
    def __init__(self, specific_cache: AbstractCache):
        self.value = specific_cache.get_value(key="key")

class Cache(AbstractCache):
    def __init__(self):
        ...

    def get_value(self, key: str):
        return ...