Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. SOLID и принципы проектирования
solid_principles

SOLID и принципы проектирования

Фундамент чистого кода: пять принципов SOLID, DRY, KISS, YAGNI. Как писать поддерживаемый и расширяемый код на Python.

SOLID и принципы проектирования

Чистый код — это не роскошь, а необходимость для поддерживаемой системы

#Почему это важно

Представьте: вы добавляете новую фичу, и ломается код в трёх других местах. Вы исправляете одно — появляется другое. Знакомо? Это признак нарушения принципов проектирования.

SOLID — это пять принципов, которые помогают писать код, который:

  • Легко понимать
  • Легко менять
  • Легко тестировать
  • Не ломается при расширениях

#1. Single Responsibility Principle (SRP)

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

#Антипаттерн

class User: def __init__(self, id, name, email): self.id = id self.name = name self.email = email def save(self, db_connection): """Сохраняет пользователя в БД""" db_connection.execute( "INSERT INTO users VALUES (?, ?, ?)", (self.id, self.name, self.email) ) def send_welcome_email(self, smtp_server): """Отправляет приветственное письмо""" message = f"Welcome, {self.name}!" smtp_server.send(self.email, message) def validate_email(self): """Проверяет корректность email""" return "@" in self.email and "." in self.email

Проблема: класс User отвечает за:

  1. Хранение данных пользователя
  2. Сохранение в БД
  3. Отправку email
  4. Валидацию

Изменение логики БД потребует изменения класса User, даже если бизнес-логика не менялась.

#Правильное решение

class User: """Модль данных — только хранение""" def __init__(self, id, name, email): self.id = id self.name = name self.email = email class UserRepository: """Работа с БД — единственная ответственность""" def __init__(self, db_connection): self._db = db_connection def save(self, user: User): self._db.execute( "INSERT INTO users VALUES (?, ?, ?)", (user.id, user.name, user.email) ) class EmailService: """Отправка email — единственная ответственность""" def __init__(self, smtp_server): self._smtp = smtp_server def send_welcome(self, user: User): message = f"Welcome, {user.name}!" self._smtp.send(user.email, message) class EmailValidator: """Валидация — единственная ответственность""" @staticmethod def is_valid(email: str) -> bool: return "@" in email and "." in email

Выгоды:

  • Можно менять БД без изменения User
  • Можно тестировать каждую часть отдельно
  • Легко добавить новую валидацию или способ отправки

#2. Open/Closed Principle (OCP)

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

#Антипаттерн

class PaymentProcessor: def process(self, payment_type: str, amount: float): if payment_type == "credit_card": self._process_credit_card(amount) elif payment_type == "paypal": self._process_paypal(amount) elif payment_type == "crypto": self._process_crypto(amount) # При добавлении нового типа придётся менять этот метод else: raise ValueError(f"Unknown payment type: {payment_type}") def _process_credit_card(self, amount): ... def _process_paypal(self, amount): ... def _process_crypto(self, amount): ...

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

#Правильное решение

from abc import ABC, abstractmethod class PaymentMethod(ABC): """Абстракция — открыта для расширения""" @abstractmethod def process(self, amount: float) -> bool: pass class CreditCardPayment(PaymentMethod): def __init__(self, card_number, cvv): self.card_number = card_number self.cvv = cvv def process(self, amount: float) -> bool: # Логика обработки кредитной карты return True class PayPalPayment(PaymentMethod): def __init__(self, email): self.email = email def process(self, amount: float) -> bool: # Логика PayPal return True class CryptoPayment(PaymentMethod): def __init__(self, wallet_address): self.wallet_address = wallet_address def process(self, amount: float) -> bool: # Логика криптовалюты return True class PaymentProcessor: """Закрыт для модификации — работает с абстракцией""" def process(self, method: PaymentMethod, amount: float) -> bool: return method.process(amount)

Выгоды:

  • Новый тип оплаты = новый класс, без изменения существующих
  • Легко тестировать каждый метод отдельно
  • Можно добавлять методы в runtime (плагины)

#3. Liskov Substitution Principle (LSP)

Принцип подстановки Барбары Лисков: объекты дочерних классов должны быть заменяемы объектами родительского класса без нарушения корректности программы.

#Антипаттерн

class Rectangle: def __init__(self, width, height): self._width = width self._height = height @property def width(self): return self._width @width.setter def width(self, value): self._width = value @property def height(self): return self._height @height.setter def height(self, value): self._height = value def area(self): return self._width * self._height class Square(Rectangle): """Нарушает LSP: меняет поведение setter'ов""" @Rectangle.width.setter def width(self, value): self._width = value self._height = value # ! @Rectangle.height.setter def height(self, value): self._width = value # ! self._height = value # Контракт: функция ожидает, что width и height независимы def resize(rectangle: Rectangle): original_area = rectangle.area() rectangle.width *= 2 rectangle.height *= 2 assert rectangle.area() == original_area * 4 # Для Rectangle: area увеличится в 4 раза ✓ # Для Square: width=2s → height=2*(2s)=4s → area=16s² ≠ 4s² ✗

#Правильное решение

class Shape(ABC): @abstractmethod def area(self) -> float: pass class Rectangle(Shape): def __init__(self, width, height): self._width = width self._height = height def area(self) -> float: return self._width * self._height class Square(Shape): def __init__(self, side): self._side = side def area(self) -> float: return self._side ** 2 # Теперь client code работает корректно с любыми Shape def print_area(shape: Shape): print(f"Area: {shape.area()}")

Выгоды:

  • Наследники не ломают ожидания клиента
  • Можно безопасно полиморфно использовать объекты
  • Тесты родительского класса работают для наследников

#4. Interface Segregation Principle (ISP)

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

#Антипаттерн

class Worker(ABC): @abstractmethod def work(self): pass @abstractmethod def eat(self): pass @abstractmethod def sleep(self): pass class HumanWorker(Worker): def work(self): ... def eat(self): ... def sleep(self): ... class RobotWorker(Worker): def work(self): ... def eat(self): raise NotImplementedError("Robots don't eat!") # ! def sleep(self): raise NotImplementedError("Robots don't sleep!") # !

Проблема: RobotWorker вынужден реализовывать ненужные методы.

#Правильное решение

class Workable(ABC): @abstractmethod def work(self): pass class Eatable(ABC): @abstractmethod def eat(self): pass class Sleepable(ABC): @abstractmethod def sleep(self): pass class HumanWorker(Workable, Eatable, Sleepable): def work(self): ... def eat(self): ... def sleep(self): ... class RobotWorker(Workable): def work(self): ... # Только нужные интерфейсы

Выгоды:

  • Классы не реализуют лишние методы
  • Изменение одного интерфейса не затрагивает другие
  • Легче понимать, что может делать объект

#Интерфейсы в Python: ABC, Protocol, duck typing

В Python есть три способа определить контракт. Выбор зависит от ситуации:

ABC — когда нужен runtime-контракт (наследование обязательно) или общая реализация:

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: ... class Circle(Shape): def area(self) -> float: return 3.14 * self._r ** 2

Protocol — структурная типизация (наследование не нужно, достаточно совпадения методов):

from typing import Protocol class Repository(Protocol): def get(self, id: int) -> dict | None: ... # Любой класс с методом get(id) -> dict подходит — без явного наследования class PostgresRepo: def get(self, id: int) -> dict | None: ...

Duck typing — контракт не объявляется, проверяется только в рантайме:

def total_area(shapes): return sum(s.area() for s in shapes) # Любой объект с методом area() total_area([Circle(5), Rectangle(3, 4)]) # ✓ total_area([NotAShape()]) # AttributeError — ошибка только в рантайме

Когда что использовать:

  • ABC — публичный API библиотеки, нужен чёткий контракт с общей логикой
  • Protocol — зависимости от внешних сервисов, репозитории, порты (естественный выбор для DIP)
  • Duck typing — внутренняя логика, где скорость разработки важнее строгости

#5. Dependency Inversion Principle (DIP)

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

#Антипаттерн

class MySQLDatabase: def connect(self): ... def execute(self, query, params=None): ... class UserService: def __init__(self): self._db = MySQLDatabase() # Прямая зависимость от конкретики def get_user(self, user_id): # 1. Зависимость от конкретной БД — нельзя заменить на PostgreSQL # 2. Нет возможности подменить для тестов return self._db.execute( "SELECT * FROM users WHERE id = %s", (user_id,) )

Проблема:

  • Невозможно протестировать UserService без реальной БД
  • Невозможно заменить MySQL на PostgreSQL без изменения UserService

#Правильное решение

from typing import Protocol class UserRepository(Protocol): """Интерфейс — порт, через который UserService работает с данными""" def get_by_id(self, user_id: int) -> dict | None: ... class UserService: def __init__(self, users: UserRepository): # Зависимость от абстракции self._users = users def get_user(self, user_id: int) -> dict | None: return self._users.get_by_id(user_id) # Реализации — вставляются снаружи class MySQLUserRepository: def __init__(self, connection): self._conn = connection def get_by_id(self, user_id: int) -> dict | None: cursor = self._conn.execute( "SELECT * FROM users WHERE id = %s", (user_id,) ) return cursor.fetchone() class InMemoryUserRepository: """Для тестов""" def __init__(self, data: dict[int, dict] | None = None): self._data = data or {} def get_by_id(self, user_id: int) -> dict | None: return self._data.get(user_id)

Dependency Injection (внедрение зависимости):

# В production — конкретная реализация repo = MySQLUserRepository(connection) service = UserService(repo) # В тестах — фейковая реализация fake_repo = InMemoryUserRepository({1: {"id": 1, "name": "Alice"}}) service = UserService(fake_repo) assert service.get_user(1) == {"id": 1, "name": "Alice"}

Выгоды:

  • Легко тестировать (фейковые репозитории без БД)
  • Легко менять реализацию (MySQL → PostgreSQL без изменения UserService)
  • Слабая связанность: UserService не знает ни SQL, ни формата запроса

DIP vs DI: Dependency Inversion — это принцип (зависеть от абстракций), а Dependency Injection — техника (передавать зависимости снаружи). DI — один из способов применить DIP, но не единственный (фабрики, service locator, модули).

Когда не применять: если зависимость только от одной конкретной реализации и она не будет меняться (например, утилита-хелпер), premature abstraction добавит косвенность без выгоды.

#Дополнительные принципы

#DRY (Don't Repeat Yourself)

Каждое знание должно иметь единственное, непротиворечивое представление в системе. Это не запрет на любые одинаковые строки — иногда два похожих куска кода относятся к разным бизнес-правилам и не должны объединяться. DRY про источник знания, а не про copy-paste.

# Плохо: критерий фильтрации дублируется — при изменении правила нужно менять два места def get_active_users(): return User.objects.filter(status='active', is_deleted=False) def count_active_users(): return User.objects.filter(status='active', is_deleted=False).count() # Хорошо ACTIVE_USER_FILTER = {'status': 'active', 'is_deleted': False} def get_active_users(): return User.objects.filter(**ACTIVE_USER_FILTER) def count_active_users(): return User.objects.filter(**ACTIVE_USER_FILTER).count()

#KISS (Keep It Simple, Stupid)

Избегайте излишней сложности.

# Слишком умно result = functools.reduce(lambda acc, x: acc + x['value'], filter(lambda x: x['active'], items), 0) # Просто и понятно total = sum(item['value'] for item in items if item['active'])

#YAGNI (You Aren't Gonna Need It)

Не добавляйте функциональность, пока она не понадобится.

# Не надо заранее class UserService: def get_user(self, user_id): ... def get_user_cached(self, user_id): ... # Пока не нужно def get_user_from_replica(self, user_id): ... # Пока не нужно

#Практическое задание

#Базовый уровень

Дан класс ReportGenerator:

class ReportGenerator: def generate_pdf_report(self, data, output_path): pass def generate_csv_report(self, data, output_path): pass def send_email(self, to, report_path): pass def save_to_database(self, data): pass def validate_data(self, data): pass

Задание: разделите на классы с единственной ответственностью. Каждый класс — одна причина для изменения.

Критерии: ReportGenerator не отправляет email, не сохраняет в БД и не валидирует. Валидация, генерация, хранение и отправка — отдельные компоненты.

#Middle уровень

Расширьте решение:

  1. Добавьте новый формат (HTML) без изменения класса, который оркестрирует генерацию.
  2. Напишите тест, который проверяет генерацию отчёта через фейковый репозиторий и фейковый отправитель — без реальной БД и SMTP.

Критерии: ReportService не знает о конкретных форматах. Тест проходит без внешних сервисов.

#Senior уровень

Добавьте требования:

  • PDF генерируется долго (асинхронно).
  • Повторная отправка не должна создавать дубликаты.
  • База иногда недоступна (retry с backoff).
  • Новый формат добавляется сторонним модулем (плагин).

Определите:

  • границы модулей и контракты между ними,
  • какую роль играет DIP, а какую — OCP,
  • где SOLID помогает, а где не решает проблему (например, retry — это infra, не архитектурный паттерн).

Критерии: новый формат добавляется без изменения существующих классов. Оркестратор не знает ни о форматах, ни о способах доставки.

#Возможное решение (базовый уровень)

class DataValidator: def validate(self, data: dict) -> bool: return bool(data) class ReportFormatter: def generate(self, data: dict, fmt: str, output_path: str) -> str: if fmt == "pdf": return self._generate_pdf(data, output_path) elif fmt == "csv": return self._generate_csv(data, output_path) raise ValueError(f"Unknown format: {fmt}") class ReportRepository: def save(self, report_path: str, data: dict) -> None: ... class NotificationService: def send(self, to: str, report_path: str) -> None: ... class ReportService: def __init__(self, validator, formatter, repo, notifier): self._validator = validator self._formatter = formatter self._repo = repo self._notifier = notifier def create_report(self, data, fmt, output_path, notify_to): if not self._validator.validate(data): raise ValueError("Invalid data") path = self._formatter.generate(data, fmt, output_path) self._repo.save(path, data) if notify_to: self._notifier.send(notify_to, path)

Это один из вариантов. Альтернатива: вынести валидацию в middleware, использовать стратегии вместо if/elif. Разбор компромиссов — упражнение для читателя.

#Мост к следующей теме

SOLID описывает принципы на уровне классов. Но архитектура системы — это про границы модулей и потоки данных. Следующий шаг:

  • DIP приводит к ports and adapters (hexagonal architecture) — порты абстракций, адаптеры реализаций.
  • SRP помогает определять границы доменов — каждая область знания = один модуль.
  • OCP проектирует точки расширения — плагины, event handlers, стратегии.

Об этом — в разделе «Архитектурные паттерны».

Далее: Архитектурные паттерны