SOLID — пять принципов объектно-ориентированного программирования, сформулированные Robert C. Martin (Uncle Bob) в начале 2000-х годов. По данным DigitalOcean, 2024, SOLID расшифровывается как Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation и Dependency Inversion. Эти принципы составляют основу Clean Architecture и применяются в Android-разработке (MVP, MVVM, Clean Architecture) и iOS (VIPER, TCA).
Главное
SOLID — мнемонический акроним, обозначающий пять принципов объектно-ориентированного проектирования. Термин введён Robert C. Martin в статье «Design Principles and Design Patterns» (2000), а позже популяризирован в книге «Agile Software Development: Principles, Patterns, and Practices» (2002). SOLID не является фреймворком или библиотекой — это набор практик, которые делают код менее связанным, более тестируемым и удобным для изменений.
По данным Clean Coder Blog, 2014, каждый принцип SOLID решает определённую проблему проектирования: SRP борется с God-классами, OCP — с каскадными изменениями, LSP — с некорректным наследованием, ISP — с жирными интерфейсами, DIP — с жёсткой связанностью. Вместе они формируют фундамент Clean Architecture, который используется в Android-проектах с MVP, MVVM и MVI.
Single Responsibility Principle (SRP) — принцип единственной ответственности. Формулировка: «У класса должна быть только одна причина для изменения». Это означает, что каждый модуль или класс отвечает ровно за одну функциональность или одну сущность предметной области. Если класс управляет и пользователями, и отправкой email — у него две причины для изменения, что нарушает SRP.
По данным Robert C. Martin, 2002, SRP — самый важный и одновременно самый нарушаемый принцип. В мобильной разработке SRP часто нарушают в Activity/Fragment, совмещая UI-логику, навигацию, работу с сетью и бизнес-логику. Решение — вынести каждый слой в отдельный класс: ViewModel для UI-логики, Repository для данных, NavController для навигации.
Рассмотрим класс UserManager, который и загружает профиль, и сохраняет настройки, и отправляет email. Это три разные ответственности, каждую из которых нужно выделить в отдельный класс: UserProfileRepository (загрузка), UserSettingsStorage (сохранение) и EmailService (отправка). Клиентский код (ViewModel) использует все три через Dependency Injection, а каждый класс легко тестируется изолированно и меняется без влияния на другие.
// ❌ Нарушение SRP: Activity знает про сеть, БД и UI
class ProfileActivity : AppCompatActivity() {
fun loadProfile() {
api.getUser() // Сетевой вызов
db.saveUser() // Работа с БД
updateUI() // Обновление UI
}
}
// ✅ SRP соблюдён: слои разделены
class ProfileViewModel : ViewModel() {
private val repo = UserRepository()
fun loadProfile() { repo.getUser() }
}
Признаки нарушения SRP: класс содержит более 200 строк, имеет методы из разных предметных областей, часто изменяется по разным причинам. Для Android-разработки правило простое: Activity отвечает только за жизненный цикл экрана, ViewModel — за состояние UI, Repository — за источники данных.
Принцип SRP применим не только к классам, но и к архитектуре уровня сервиса. Каждый микросервис отвечает за одну доменную сущность: UserService — только пользователи, PaymentService — только платежи, NotificationService — только уведомления. Это позволяет масштабировать, деплоить и тестировать сервисы независимо. В мобильном приложении SRP на уровне микросервисов проявляется в разделении API-клиентов по доменам.
Open-Closed Principle (OCP) — принцип открытости / закрытости. Классы должны быть открыты для расширения (можно добавить новое поведение) и закрыты для модификации (существующий код не меняется). Достигается через полиморфизм, абстрактные классы и интерфейсы. Вместо того чтобы добавлять if-else в существующий метод, создаётся новая реализация интерфейса.
По данным Clean Coder Blog, 2014, OCP наиболее эффективен в связке с паттерном Strategy. Например, если приложение поддерживает разные способы оплаты (Google Pay, Apple Pay, PayPal), не нужно добавлять switch-case в платёжный процессор. Каждый способ оплаты реализует общий интерфейс PaymentGateway, и новая платёжная система добавляется как новый класс без изменения существующих.
// ✅ OCP: открыт для расширения, закрыт для модификации
interface PaymentGateway {
fun processPayment(amount: Double): Boolean
}
class GooglePayGateway : PaymentGateway {
override fun processPayment(amount: Double) = true
}
// Новая платёжная система — без изменения существующего кода
class ApplePayGateway : PaymentGateway {
override fun processPayment(amount: Double) = true
}
Liskov Substitution Principle (LSP) — принцип подстановки Барбары Лисков. Если S — подтип T, то объекты T можно заменить объектами S без изменения свойств программы. Формально: функция, использующая базовый класс, должна корректно работать с любым его подклассом. Если подкласс выбрасывает исключение там, где базовый класс не выбрасывает — LSP нарушен.
По данным Robert C. Martin, 2002, LSP — самый сложный для понимания принцип SOLID. Классический пример нарушения — класс Square (квадрат), наследующий от Rectangle (прямоугольник). Если setWidth для Square устанавливает и ширину, и высоту, то клиентский код, ожидающий поведение Rectangle, получит неожиданный результат. В мобильной разработке LSP часто нарушается при наследовании ViewModel, когда дочерняя ViewModel добавляет обязательные зависимости.
// ❌ Нарушение LSP: Square ломает поведение Rectangle
open class Rectangle(open var width: Int, open var height: Int)
class Square(side: Int) : Rectangle(side, side) {
override var width
get() = super.width
set(value) { super.setBoth(value, value) }
}
Interface Segregation Principle (ISP) — принцип разделения интерфейсов. Клиенты не должны зависеть от интерфейсов, которые они не используют. Вместо одного «жирного» интерфейса создаётся несколько узких специализированных. Если класс реализует интерфейс, но часть методов выбрасывает исключение UnsupportedOperationException или оставляет пустыми — это явный признак нарушения ISP.
По данным DigitalOcean, 2024, ISP особенно актуален в мобильной разработке при проектировании ViewModel и Repository. Вместо одного интерфейса UserRepository со всеми CRUD-методами лучше создать QueryUserRepository (только чтение) и CommandUserRepository (запись). Тогда читающий клиент (элемент UI) зависит только от Query-интерфейса и не знает про методы записи.
// ❌ Жирный интерфейс — клиент вынужден реализовывать ненужные методы
interface UserOperations {
fun getUser(id: String): User
fun saveUser(user: User)
fun deleteUser(id: String)
fun exportUsers(): File
}
// ✅ ISP: разделённые интерфейсы
interface UserReader { fun getUser(id: String): User }
interface UserWriter { fun saveUser(user: User) }
interface UserDeleter { fun deleteUser(id: String) }
Dependency Inversion Principle (DIP) — принцип инверсии зависимостей. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба уровня должны зависеть от абстракций (интерфейсов). Абстракции не должны зависеть от деталей — детали зависят от абстракций. Это не «Dependency Injection» (внедрение зависимостей), хотя DI — частый способ реализации DIP.
По данным Robert C. Martin, 2019, DIP — основа Clean Architecture. ViewModel (верхний уровень) не должен напрямую создавать экземпляр RetrofitApi (деталь). Вместо этого ViewModel зависит от интерфейса UserRepository, а конкретная реализация UserRepositoryImpl с Retrofit передаётся через конструктор. В Android DIP реализуется через Hilt/Dagger или Koin: все зависимости предоставляются через DI-контейнер.
// ✅ DIP: Module зависит от абстракции, а не от детали
class UserRepositoryImpl(
private val api: UserApi, // Зависим от интерфейса
private val db: UserDao // Зависим от интерфейса
) : UserRepository {
override suspend fun getUser(id: String): User {
return api.fetchUser(id)
}
}
// Hilt DI: детали связываются через модуль DI
@Module
object NetworkModule {
@Provides
fun provideUserApi(retrofit: Retrofit): UserApi =
retrofit.create(UserApi::class.java)
}
SOLID в мобильной разработке применяется на всех уровнях: от архитектуры приложения до отдельных классов. В Android-проектах Clean Architecture делит код на три слоя: domain (бизнес-логика — независит от фреймворков), data (репозитории, API, БД) и presentation (UI, ViewModel). Domain-слой использует принципы SOLID: use case (SRP), интерфейсы репозиториев (DIP), entity-классы (OCP + LSP).
По данным Android Developers Guide, 2025, SRP в Android проявляется в разделении ViewModel, Repository и Mapper. OCP — при добавлении новых источников данных через интерфейс DataSource. LSP — в единообразной обработке Result от разных репозиториев. ISP — в CQRS-подходе (разделение Read/Write репозиториев). DIP — через Hilt/Koin для внедрения зависимостей.
| Принцип | Проблема без него | Решение в мобильном проекте |
|---|---|---|
| SRP | Activity на 1000+ строк | ViewModel + UseCase + Repository |
| OCP | switch-case по типу платежа | Strategy: PaymentGateway интерфейс |
| LSP | Баг при замене BaseViewModel | Проверка контракта подклассов |
| ISP | UnsupportedOperationException | Reader / Writer разделение |
| DIP | ViewModel создаёт Retrofit вручную | Hilt / Koin DI контейнер |
Ошибки SOLID чаще всего связаны с избыточным усложнением кода. Первая — следование принципам буквально без учёта контекста. Разделение одного класса UserService на 10 интерфейсов и 15 классов ради «чистого» ISP — это оверинжиниринг. SOLID — инструмент, а не цель. Вторая ошибка — путаница между SRP и «один метод = одна ответственность». Класс может иметь несколько методов, если все они относятся к одной зоне ответственности.
По данным Simple Thread, 2024, третья ошибка — игнорирование LSP при наследовании ViewModel в Android. Если базовая ViewModel ожидает LiveData, а дочерняя использует StateFlow — клиентский код, подписанный на LiveData, не получит обновлений. Четвёртая — нарушение DIP ради тестирования: RepositoryImpl напрямую создаёт экземпляр OkHttpClient, что делает модульное тестирование невозможным.
Правило золотой середины: применяй SOLID, когда это решает реальную проблему (частые изменения, сложность тестирования, дублирование). Для простых CRUD-экранов строгое следование всем пяти принципам избыточно. Для бизнес-логики, финансовых расчётов и API-взаимодействий SOLID обязателен.
Clean Architecture (Robert C. Martin, 2012) — прямое применение SOLID на уровне слоёв приложения. SRP определяет границы use case (каждый use case — один класс). OCP реализуется через интерфейсы репозиториев (Data Layer может меняться без изменения Domain). ISP даёт разделение Use Case на input/output boundary. DIP — направление зависимостей внутрь Domain-слоя. LSP гарантирует, что любая реализация репозитория заменяема без поломки use case.
Часто задаваемые вопросы
SOLID — пять правил написания кода, чтобы его было легко менять, тестировать и понимать. Каждая буква — один принцип: не пиши большие классы (SRP), не меняй существующий код — добавляй новый (OCP), не ломай поведение наследников (LSP) и другие.
SRP (Single Responsibility) считается самым важным, потому что его нарушение ведёт к God-классам — огромным классам, которые сложно тестировать и изменять. Однако без DIP (Dependency Inversion) код остаётся жёстко связанным, что тоже критично.
Не обязателен, но крайне желателен для коммерческих проектов с длинным жизненным циклом. Для простых приложений (один экран, нет бизнес-логики) SOLID может быть избыточным. Для проектов на 50+ экранов и 3+ разработчиков SOLID — необходимый минимум.
Последствия: классы становятся «жирными» (1000+ строк), изменение в одном месте ломает три других, невозможно написать модульные тесты, добавление новой фичи занимает недели вместо дней. Со временем код превращается в «Big Ball of Mud» — запутанный и хрупкий.
Признаки соблюдения: каждый класс меньше 200 строк, изменение фичи не затрагивает 5+ файлов, тесты пишутся без мокирования 10 зависимостей, новый разработчик понимает структуру за день. Инструменты вроде SonarQube и detekt помогают выявить нарушения SRP и DIP.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также