SOLID на уровне сервисов
О том, как микросервисная архитектура структурно продолжает идеи хорошего объектно-ориентированного проектирования. SOLID, как помощник для молодых разработчиков в микросервисной архитектуре.
Принцип единственной ответственности в формулировке Роберта Мартина гласит, что у класса должна быть одна причина для изменения. На уровне микросервиса формулировка остаётся той же — меняется только масштаб. Сервис должен инкапсулировать одну бизнес-возможность (capability) или один ограниченный контекст (bounded context в терминах DDD), и причиной его изменения должно быть изменение требований к этой конкретной возможности.
Ошибка, которую совершают команды на этом этапе, симметрична классической ошибке проектирования классов. Класс UserManager, который содержит методы register(), sendNotification(), generateReport() и validatePayment(), — это не класс, а свалка. Сервис user-service, который занимается регистрацией, отправкой уведомлений, формированием отчётов и валидацией платежей, — это распределённый монолит, замаскированный под микросервис. Признак нарушения SRP в обоих случаях один: при изменении одного аспекта приходится трогать код, отвечающий за другие
Разные циклы изменения = разные сервисы:
• Частота обновлений
• Разные команды
• Разные требования к масштабируемости
• Разные метрики успеха
Open/Closed Principle и расширяемость через API
Принцип открытости/закрытости требует, чтобы сущность была открыта для расширения, но закрыта для модификации. В ООП это достигается через полиморфизм и абстракции: добавление нового поведения не требует изменения существующего кода.
В микросервисах OCP реализуется через стабильные контракты API и расширение через композицию сервисов. Если в систему добавляется новый тип отчёта по простоям оборудования, это не должно приводить к изменению сервиса сбора телеметрии — должен появиться новый сервис-потребитель, подписывающийся на события или вызывающий стабильный API. Контракт OpenAPI или gRPC-схема выполняет ту же роль, что интерфейс или абстрактный класс в ООП: точка расширения, через которую можно добавлять новое поведение, не ломая существующее.
Версионирование API — это, по сути, механизм поддержания OCP в условиях, когда контракт всё-таки должен эволюционировать. Параллельная работа /v1/ и /v2/ эндпоинтов аналогична паттерну Adapter, позволяющему старым клиентам продолжать работать с новой реализацией.
- Используйте семантическое версионирование: v1, v2, v3
- Поддерживайте несколько версий параллельно
- Документируйте контракт в OpenAPI/gRPC
- Не добавляйте обязательные поля в ответы (backwards incompatible) Liskov Substitution Principle и совместимость версий
Liskov Substitution Principle и совместимость версий
LSP требует, чтобы подтипы были взаимозаменяемы со своими базовыми типами без нарушения корректности программы. На уровне микросервисов это превращается в требование совместимости версий и реплик: любая инстанция сервиса, отвечающая на запросы по определённому контракту, должна вести себя предсказуемо одинаково.
Это требование становится критическим при blue-green деплое, канареечных релизах и горизонтальном масштабировании. Если новая версия сервиса возвращает в ответе поле в другом формате, теряет какие-то поля или меняет семантику кодов ошибок — это нарушение LSP. Клиенты ожидают, что любой инстанс за балансировщиком ведёт себя как «базовый класс», и нарушение этого ожидания приводит к плавающим багам, которые сложно воспроизвести.
Практический совет:
- Тестирование контракта (Contract Testing) перед деплоем
- Проведи сравнение реальные responses старой и новой версии
- Мониторинг ошибок после релиза
- Держи несколько версий сервиса в зависимостях
Interface Segregation Principle и тонкие API
ISP утверждает, что клиенты не должны зависеть от методов, которыми они не пользуют. На уровне микросервисов это превращается в принцип проектирования специализированных API под конкретных потребителей и в паттерн Backend for Frontend (BFF).
Толстый сервис, экспонирующий один большой API со всеми возможными операциями для всех возможных клиентов, страдает ровно теми же проблемами, что класс с интерфейсом на сорок методов. Любое изменение, нужное одному клиенту, потенциально влияет на всех остальных. Поэтому хорошо спроектированный микросервис либо предоставляет несколько узких API под разные сценарии использования, либо за ним стоит BFF-слой, агрегирующий и адаптирующий ответы под конкретного потребителя — мобильное приложение, веб-дашборд оператора, система отчётности.
Практический совет:
- Один BFF на клиент (mobile, web, integrations)
- BFF агрегирует и трансформирует данные
- Базовый сервис предоставляет узкий API
- GraphQL часто лучше REST для ISP
Dependency Inversion Principle и асинхронная коммуникация
Принцип инверсии зависимостей требует, чтобы модули верхнего уровня не зависели от модулей нижнего уровня — оба должны зависеть от абстракций. В микросервисной архитектуре эту роль абстракций играют брокеры сообщений, схемы событий и контракты API.
Когда сервис A отправляет событие в Kafka, он не знает и не должен знать, какие сервисы его обработают — может быть, ни одного, может быть, десять. Сервис A зависит от абстракции «топик с событиями определённой схемы», а не от конкретных потребителей. Это и есть DIP в чистом виде, реализованный на уровне инфраструктуры.
REST-вызов в этом смысле — более слабая форма DIP, потому что вызывающий всё-таки знает про вызываемого. Поэтому для систем с высокой связанностью предпочтительна именно событийно-ориентированная архитектура: она обеспечивает максимальную инверсию зависимостей.
Практический совет:
- Используй события для cross-service коммуникации
- REST только для запросов, которые требуют instant response
- Schema Registry для версионирования событий
- Idempotency keys для retry безопасности
- Dead Letter Queues для обработки ошибок
- Мониторь задержки обработки событий
Вместо вывода
Применение SOLID не гарантирует успех, но его нарушение практически гарантирует проблемы