Принципы хорошего дизайна: SOLID кратко

Пять принципов SOLID — фундамент, на котором стоят почти все паттерны. Разбираем каждый коротко и с примером.

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

S — Single Responsibility

Принцип единственной ответственности: у класса должна быть одна причина для изменения. Если класс и считает зарплату, и форматирует отчёт, и пишет в БД — три причины меняться, три источника багов. Разделяйте.

O — Open/Closed

Открыт для расширения, закрыт для изменения. Новое поведение добавляйте новым кодом, а не правкой проверенного. Классический инструмент — полиморфизм вместо if/elif по типу.

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        ...


class Circle(Shape):
    def __init__(self, r):
        self.r = r

    def area(self):
        return 3.14 * self.r * self.r


class Square(Shape):
    def __init__(self, a):
        self.a = a

    def area(self):
        return self.a * self.a


# Добавить новую фигуру = новый класс, total() менять НЕ нужно (OCP)
def total(shapes):
    return sum(s.area() for s in shapes)


print(total([Circle(1), Square(2)]))

Вывод:

7.140000000000001

Чтобы добавить треугольник, мы напишем новый класс Triangle, а функцию total трогать не придётся — она закрыта для изменения, но открыта для расширения новыми фигурами.

L — Liskov Substitution

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

I — Interface Segregation

Лучше много узких интерфейсов, чем один «толстый». Не заставляйте класс реализовывать методы, которые ему не нужны. Робот не должен реализовывать eat() только потому, что это есть в общем интерфейсе Worker.

D — Dependency Inversion

Зависьте от абстракций, а не от конкретных классов. Высокоуровневый модуль не должен знать про конкретную базу данных — он работает с интерфейсом Storage, а конкретику подставляют снаружи.

from abc import ABC, abstractmethod


class Storage(ABC):
    @abstractmethod
    def save(self, data):
        ...


class MemoryStorage(Storage):
    def save(self, data):
        print(f"Сохранено в память: {data}")


class Report:
    def __init__(self, storage: Storage):  # зависим от абстракции
        self.storage = storage

    def run(self):
        self.storage.save("итоги квартала")


Report(MemoryStorage()).run()

Вывод:

Сохранено в память: итоги квартала

Класс Report не знает, куда именно сохраняются данные. Завтра подставим FileStorage или DbStorageReport не изменится. Это и есть инверсия зависимостей.

Итог

  • SRP — одна причина для изменения; OCP — расширяй, не меняя.
  • LSP — подкласс не должен ломать ожидания; ISP — узкие интерфейсы.
  • DIP — зависимости направлены на абстракции; почти каждый паттерн опирается на SOLID.
Проверьте себя
1. Какой принцип нарушает класс, который и считает данные, и рисует UI, и пишет в файл?
AOpen/Closed
BSingle Responsibility
CLiskov Substitution
DDependency Inversion
2. Что предписывает принцип Dependency Inversion?
AЗависеть от конкретных реализаций ради скорости
BЗависеть от абстракций, а не от конкретных классов
CИнвертировать порядок вызова методов
DНе использовать наследование
3. Принцип Open/Closed чаще всего реализуют через…
AБольшой блок if/elif по типу объекта
BПолиморфизм и новые классы
CГлобальные переменные
DКопирование кода
Поддержать проект