Перейти к содержанию

SOLID

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

Принцип единственной ответственности (SRP)

У компонента должна быть одна связная ответственность и одна основная причина для изменения. Это принцип о границах изменений, а не призыв превращать каждую функцию в отдельный класс.

Android ViewModel может координировать логику экрана, но не должна одновременно разбирать сетевой JSON, выполнять SQL и форматировать Android-ресурсы. Детали транспорта и хранения относятся к слою данных. Добавляйте use case или mapper, когда он представляет переиспользуемую логику или устраняет реальную сложность, а не автоматически.

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

Принцип открытости/закрытости (OCP)

Стабильный код должен позволять добавлять ожидаемые варианты поведения без постоянного переписывания основной логики. Композиция, стратегии и полиморфизм могут заменить растущее условие, когда поведение действительно различается:

interface PricePolicy {
    fun price(order: Order): Money
}

class Checkout(private val policy: PricePolicy) {
    fun total(order: Order): Money = policy.price(order)
}

OCP не означает, что существующий код нельзя менять. Точки расширения для гипотетических требований добавляют сложность; выделяйте абстракцию, когда вариант уже известен или повторяется.

Принцип подстановки Лисков (LSP)

Каждая реализация базового типа должна сохранять его контракт, чтобы вызывающему коду не требовались проверки конкретной реализации. Подтип не должен усиливать предусловия, ослаблять гарантии результата или добавлять неожиданные ошибки и побочные эффекты.

Если UserRepository.user(id) обещает пользователя или документированную доменную ошибку, одна реализация не должна молча возвращать устаревшие данные, а другая - бросать недокументированное исключение. Для нескольких реализаций полезны общие контрактные тесты.

LSP относится не только к наследованию классов, но и к интерфейсам.

Принцип разделения интерфейсов (ISP)

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

Например, экран только для чтения может зависеть от ObserveOrders, а код синхронизации - от SyncOrders. Не разделяйте интерфейсы только ради малого размера: методы, которые меняются вместе и нужны одним клиентам, могут оставаться вместе.

Принцип инверсии зависимостей (DIP)

Высокоуровневая политика не должна напрямую зависеть от низкоуровневых деталей; они встречаются на границе, определённой политикой. ViewModel может зависеть от контракта UserRepository уровня приложения, а реализация в слое данных - использовать Room и Retrofit.

Внедрение через конструктор делает зависимости явными, а Hilt или ручной DI собирает реализации. DI - техника связывания объектов, сама по себе она не обеспечивает DIP. Интерфейс не нужен, если нет осмысленной границы или альтернативного поведения.

Практичное применение SOLID

Используйте SOLID, чтобы объяснить конкретное изменение: какая ответственность смешана, какой контракт нарушен или какая зависимость мешает тестированию. Выбирайте простейший дизайн, защищающий наблюдаемую вариативность. Дополнительные слои, интерфейсы с одним методом и сквозные use case могут усложнить навигацию, не упрощая изменения.

Связанные темы: ООП, Паттерны проектирования, Основы архитектуры и Основы DI.