Инженерия ПО

SOLID принципы

Цели урока

  • Знать все пять букв SOLID: SRP, OCP, LSP, ISP, DIP, и проблему, которую решает каждая
  • Видеть отличие 'один класс - одна ответственность' (SRP) от 'один класс - одна функция'
  • Применять OCP через интерфейсы и наследование, не правя существующий код
  • Распознавать нарушение LSP по сигнатуре подкласса и поведению контрактов
  • Использовать DIP, чтобы тестировать бизнес-логику без БД и сети

Предварительные знания

  • Базовое ООП: классы, наследование, интерфейсы, полиморфизм
  • Опыт с любым языком, поддерживающим интерфейсы или абстрактные классы
  • Понимание зачем нужны unit-тесты

2003 год. Команда Microsoft начинает переписывать Windows Vista. 10 000 разработчиков, 50 миллионов строк кода. 2006: Microsoft переносит релиз на год. 2007: Vista выходит с многочисленными проблемами качества. Постмортем называет coupling как первопричину: изменение одного компонента ломало десятки других. Те же 5 принципов SOLID, опубликованных Robert Martin в 2000 году, описывают архитектурные решения, которые могли предотвратить этот $6 миллиардный провал.

  • **Google** SRE book описывает SOLID как основу testability: DIP и ISP - необходимые условия unit testing в микросервисной архитектуре
  • **Netflix** OSS (Hystrix, Ribbon, Eureka) построены на OCP: новые стратегии (fallback, retry) добавляются без изменения core библиотек
  • **Spring Framework** весь построен на DIP: IoC container - промышленная реализация Dependency Injection на миллионах production приложений

Robert Martin и рождение SOLID

Принципы SRP и OCP были сформулированы ещё в 1990s, LSP - Barbara Liskov в 1987. Robert Martin объединил их в единую систему в статье 'Design Principles and Design Patterns' (2000) и книге 'Agile Software Development' (2002). Акроним SOLID придумал Michael Feathers около 2004 года. Barbara Liskov получила Turing Award в 2008 году - за LSP и другие работы в области программирования. SOLID стал де-факто стандартом OOP архитектуры, однако критики (в частности, Дэн Норт) указывают что эти принципы лучше работают как heuristics, а не строгие правила.

Single Responsibility Principle

**SRP**: класс должен иметь только одну причину для изменения. Формулировка Robert Martin (Uncle Bob): 'A module should be responsible to one, and only one, actor' - где actor это группа людей (stakeholders), заинтересованная в изменении. Нарушение SRP приводит к coupling несвязанных concerns: изменение бизнес-логики ломает отчётность, изменение формата вывода требует трогать доменную логику.

**Размер не определяет SRP**: класс с 500 строками может соблюдать SRP, а класс из 30 строк - нарушать. Вопрос не в количестве методов, а в количестве акторов (stakeholders), которые могут потребовать изменений. Классический антипаттерн: `UserService` с методами `getUserById`, `sendEmail`, `validatePassword`, `generateReport`.

Класс `OrderService` содержит методы: `calculateTotal()`, `sendConfirmationEmail()`, `saveToDatabase()`, `generateInvoicePdf()`. Сколько нарушений SRP?

Open/Closed Principle

**OCP**: программные сущности должны быть открыты для расширения, но закрыты для изменения. Бертран Мейер, 1988. Практический смысл: добавление нового поведения не должно требовать модификации существующего протестированного кода. Механизм: полиморфизм через абстракции (интерфейсы, абстрактные классы).

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

Команда добавляет поддержку нового payment provider (PayPal). Какой подход соответствует OCP?

Liskov Substitution Principle

**LSP**: подтип должен быть подставим вместо базового типа без изменения корректности программы. Barbara Liskov, 1987, Turing Award 2008. Формальнее: если для объекта base типа B верно свойство P, то для объекта sub типа S (подтипа B) свойство P тоже должно быть верным. Нарушение LSP: подкласс бросает исключения там где базовый класс их не бросает, или изменяет семантику метода.

**Design by Contract**: LSP требует что подкласс не усиливает preconditions (принимает не меньший диапазон входных данных), не ослабляет postconditions (гарантирует не меньше базового класса), не бросает новых unchecked exceptions. Нарушение: `NotImplementedException` в методе интерфейса - типичный признак неверной иерархии.

Класс `ReadOnlyList` наследует `List` и переопределяет `add()` так, чтобы бросать `UnsupportedOperationException`. Это нарушает LSP?

Interface Segregation Principle

**ISP**: клиенты не должны зависеть от методов, которые они не используют. Robert Martin, ~1996. Толстые интерфейсы создают ненужные зависимости: изменение метода A, которым пользуется клиент X, вынуждает перекомпилировать клиент Y (который A не использует). В языках с duck typing (Python, Go) ISP важен семантически - даже если нет компиляции.

**ISP в микросервисах**: API контракт - тоже интерфейс. GraphQL решает ISP на уровне HTTP: клиент запрашивает только нужные поля, не весь объект (как в REST). gRPC service definitions позволяют разделить большой сервис на несколько узких, каждый с малым interface.

Интерфейс `Printer` содержит методы: `print()`, `scan()`, `fax()`, `staple()`. Простой принтер реализует только print(). Какое решение лучше?

Dependency Inversion Principle

**DIP** состоит из двух правил: 1. модули высокого уровня не должны зависеть от модулей низкого уровня - оба должны зависеть от абстракций 2. абстракции не должны зависеть от деталей - детали должны зависеть от абстракций. Инверсия: раньше BusinessLogic зависела от конкретного Database. Теперь оба зависят от интерфейса `DataStore`. Dependency Injection (DI) - паттерн реализации DIP.

**DI containers** (NestJS, Spring, Angular): автоматически разрешают зависимости через reflection и декораторы. `@Injectable()` в NestJS - декларация что класс может быть создан DI container. `@Inject()` - запрос зависимости. Container сам создаёт объекты в правильном порядке и инжектирует их.

DIP означает что нужно использовать DI framework (Spring, NestJS)

DIP - принцип проектирования, Dependency Injection - паттерн его реализации. DI можно реализовать вручную (constructor injection), без framework

DI framework автоматизирует создание объектного графа, но принцип работает и без него. Передача зависимости через конструктор - уже DIP. Framework добавляет удобство (lifecycle management, scoping), но не является обязательным условием.

PaymentService использует `new StripeGateway()` внутри метода `charge()`. Как исправить согласно DIP?

Ключевые идеи

  • **SRP**: один класс - одна причина для изменения (один актор-stakeholder). Вопрос не 'сколько методов', а 'кто требует изменений'
  • **OCP**: расширение без модификации через абстракции. Новые типы скидок/провайдеров/стратегий - новые классы, не if-else в старых
  • **LSP**: подтип подставим вместо базового без нарушения корректности. Square-is-a-Rectangle ломает LSP
  • **ISP**: клиент зависит только от нужных методов. Толстые интерфейсы создают невидимые coupling между несвязанными клиентами
  • **DIP**: оба уровня зависят от абстракции. DI через конструктор - делает зависимости видимыми и testable

Вопросы для размышления

  • В codebase найден класс `ApiController` с 2000 строк: парсинг HTTP запросов, бизнес-логика, SQL запросы, форматирование ответа, логирование, валидация. Как разбить его согласно SOLID? Какие принципы применить и в каком порядке?

Связанные уроки

  • se-01
  • se-02
  • se-03
  • se-05
  • sd-01-intro
  • mob-04
  • web-04
  • devops-04
  • ds-02-cap-theorem
  • plt-02-type-systems
SOLID принципы

0

1

Войти