SOLID: принципи, 5 правил ООП і застосування в розробці

Автор: IT Sectr Опубліковано: 2026-05-11 Час читання: 10 хв

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 — акронім п'яти принципів ООП: SRP, OCP, LSP, ISP, DIP, сформульованих Robert C. Martin для створення гнучкого та підтримуваного коду.
  • SRP (Single Responsibility) — кожен клас має одну причину для зміни, одна відповідальність на модуль.
  • OCP (Open-Closed) — класи відкриті для розширення, але закриті для модифікації, реалізується через наслідування та поліморфізм.
  • LSP (Liskov Substitution) — об'єкти підкласів повинні замінювати об'єкти базового класу без зміни коректності програми.
  • ISP (Interface Segregation) — клієнти не повинні залежати від інтерфейсів, які вони не використовують, інтерфейси повинні бути вузькими та специфічними.
  • DIP (Dependency Inversion) — модулі верхнього рівня не залежать від модулів нижнього рівня, обидва залежать від абстракцій.

Що таке SOLID? Огляд п'яти принципів

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.

SRP: Принцип єдиної відповідальності

Single Responsibility Principle (SRP) — принцип єдиної відповідальності. Формулювання: «У класу повинна бути тільки одна причина для зміни». Це означає, що кожен модуль або клас відповідає рівно за одну функціональність або одну сутність предметної області. Якщо клас керує і користувачами, і відправкою email — у нього дві причини для зміни, що порушує SRP.

Згідно з Robert C. Martin, 2002, SRP — найважливіший і одночасно найбільш порушуваний принцип. У мобільній розробці SRP часто порушують в Activity/Fragment, поєднуючи UI-логіку, навігацію, роботу з мережею та бізнес-логіку. Рішення — винести кожен шар в окремий клас: ViewModel для UI-логіки, Repository для даних, NavController для навігації.

Приклад SRP: поділ UserManager

Розглянемо клас UserManager, який і завантажує профіль, і зберігає налаштування, і надсилає email. Це три різні відповідальності, кожну з яких потрібно виділити в окремий клас: UserProfileRepository (завантаження), UserSettingsStorage (збереження) та EmailService (відправка). Клієнтський код (ViewModel) використовує всі три через Dependency Injection, а кожен клас легко тестується ізольовано та змінюється без впливу на інші.

kotlin
// ❌ Порушення 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 та мікросервісна архітектура

Принцип SRP застосовний не тільки до класів, але й до архітектури рівня сервісу. Кожен мікросервіс відповідає за одну доменну сутність: UserService — тільки користувачі, PaymentService — тільки платежі, NotificationService — тільки сповіщення. Це дозволяє масштабувати, деплоїти та тестувати сервіси незалежно. У мобільному додатку SRP на рівні мікросервісів проявляється в розділенні API-клієнтів за доменами.

OCP: Принцип відкритості/закритості

Open-Closed Principle (OCP) — класи повинні бути відкриті для розширення (можна додати нову поведінку) і закриті для модифікації (існуючий код не змінюється). Досягається через поліморфізм, абстрактні класи та інтерфейси. Замість того щоб додавати if-else в існуючий метод, створюється нова реалізація інтерфейсу.

Згідно з Clean Coder Blog, 2014, OCP найбільш ефективний у зв'язці з патерном Strategy. Наприклад, якщо додаток підтримує різні способи оплати (Google Pay, Apple Pay, PayPal), не потрібно додавати switch-case у платіжний процесор. Кожен спосіб оплати реалізує загальний інтерфейс PaymentGateway, і нова платіжна система додається як новий клас без зміни існуючих.

kotlin
// ✅ 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
}

LSP: Принцип підстановки Лісков

Liskov Substitution Principle (LSP) — принцип підстановки Барбари Лісков. Якщо S — підтип T, то об'єкти T можна замінити об'єктами S без зміни властивостей програми. Формально: функція, що використовує базовий клас, повинна коректно працювати з будь-яким його підкласом. Якщо підклас викидає виняток там, де базовий клас не викидає — LSP порушено.

Згідно з Robert C. Martin, 2002, LSP — найскладніший для розуміння принцип SOLID. Класичний приклад порушення — клас Square, що наслідує Rectangle. Якщо setWidth для Square встановлює і ширину, і висоту, то клієнтський код, який очікує поведінку Rectangle, отримає неочікуваний результат. У мобільній розробці LSP часто порушується при наслідуванні ViewModel, коли дочірня ViewModel додає обов'язкові залежності.

kotlin
// ❌ Порушення 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) }
}

ISP: Принцип розділення інтерфейсів

Interface Segregation Principle (ISP) — клієнти не повинні залежати від інтерфейсів, які вони не використовують. Замість одного «жирного» інтерфейсу створюється кілька вузьких спеціалізованих. Якщо клас реалізує інтерфейс, але частина методів викидає виняток UnsupportedOperationException або залишає порожніми — це явна ознака порушення ISP.

Згідно з DigitalOcean, 2024, ISP особливо актуальний у мобільній розробці при проектуванні ViewModel та Repository. Замість одного інтерфейсу UserRepository з усіма CRUD-методами краще створити QueryUserRepository (тільки читання) та CommandUserRepository (запис). Тоді читаючий клієнт (елемент UI) залежить тільки від Query-інтерфейсу і не знає про методи запису.

kotlin
// ❌ Жирний інтерфейс — клієнт змушений реалізовувати непотрібні методи
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) }

DIP: Принцип інверсії залежностей

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-контейнер.

kotlin
// ✅ 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 у мобільній розробці

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 для впровадження залежностей.

ПринципПроблема без ньогоРішення в мобільному проекті
SRPActivity на 1000+ рядківViewModel + UseCase + Repository
OCPswitch-case за типом платежуStrategy: PaymentGateway інтерфейс
LSPБаг при заміні BaseViewModelПеревірка контракту підкласів
ISPUnsupportedOperationExceptionReader / Writer розділення
DIPViewModel створює Retrofit вручнуHilt / Koin DI контейнер

Типові помилки при застосуванні SOLID

Помилки SOLID найчастіше пов'язані з надмірним ускладненням коду. Перша — слідування принципам буквально без урахування контексту. Розділення одного класу UserService на 10 інтерфейсів і 15 класів заради «чистого» ISP — це оверінжиніринг. SOLID — інструмент, а не мета. Друга помилка — плутанина між SRP та «один метод = одна відповідальність». Клас може мати кілька методів, якщо всі вони відносяться до однієї зони відповідальності.

Згідно з Simple Thread, 2024, третя помилка — ігнорування LSP при наслідуванні ViewModel в Android. Якщо базова ViewModel очікує LiveData, а дочірня використовує StateFlow — клієнтський код, підписаний на LiveData, не отримає оновлень. Четверта — порушення DIP заради тестування: RepositoryImpl безпосередньо створює екземпляр OkHttpClient, що робить модульне тестування неможливим.

Правило золотої середини: застосовуйте SOLID, коли це вирішує реальну проблему (часті зміни, складність тестування, дублювання). Для простих CRUD-екранів суворе слідування всім п'яти принципам надлишкове. Для бізнес-логіки, фінансових розрахунків та API-взаємодій SOLID обов'язковий.

Зв'язок SOLID та Clean Architecture

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 простими словами?

SOLID — п'ять правил написання коду, щоб його було легко змінювати, тестувати та розуміти. Кожна буква — один принцип: не пиши великі класи (SRP), не змінюй існуючий код — додавай новий (OCP), не ломай поведінку нащадків (LSP) та інші.

Який принцип SOLID найважливіший?

SRP (Single Responsibility) вважається найважливішим, тому що його порушення веде до God-класів — величезних класів, які складно тестувати та змінювати. Однак без DIP (Dependency Inversion) код залишається жорстко зв'язаним, що також критично.

Чи обов'язковий SOLID для мобільної розробки?

Не обов'язковий, але вкрай бажаний для комерційних проектів з довгим життєвим циклом. Для простих додатків (один екран, немає бізнес-логіки) SOLID може бути надлишковим. Для проектів на 50+ екранів і 3+ розробників SOLID — необхідний мінімум.

Що буде, якщо не дотримуватися SOLID?

Наслідки: класи стають «жирними» (1000+ рядків), зміна в одному місці ламає три інших, неможливо написати модульні тести, додавання нової функції займає тижні замість днів. З часом код перетворюється на «Big Ball of Mud» — заплутаний і крихкий.

Як перевірити, чи дотримується SOLID у проекті?

Ознаки дотримання: кожен клас менше 200 рядків, зміна функції не зачіпає 5+ файлів, тести пишуться без мокування 10 залежностей, новий розробник розуміє структуру за день. Інструменти на зразок SonarQube та detekt допомагають виявити порушення SRP та DIP.

Підсумки

  • SOLID — п'ять принципів ООП (SRP, OCP, LSP, ISP, DIP) для створення гнучкого та підтримуваного коду
  • SRP — кожна сутність відповідає за одне завдання, вирішує проблему God-класів
  • OCP — розширення через поліморфізм, а не модифікацію існуючого коду
  • LSP — нащадки не повинні ламати поведінку базового класу
  • ISP — вузькі інтерфейси замість універсальних «швейцарських ножів»
  • DIP — залежність від абстракцій, впровадження через Hilt/Koin в Android
  • SOLID обов'язковий для Clean Architecture та комерційних мобільних проектів

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також