Принципы хорошего дизайна: 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 или DbStorage — Report не изменится. Это и есть инверсия зависимостей.
Итог
- SRP — одна причина для изменения; OCP — расширяй, не меняя.
- LSP — подкласс не должен ломать ожидания; ISP — узкие интерфейсы.
- DIP — зависимости направлены на абстракции; почти каждый паттерн опирается на SOLID.