← Назад к вопросам

Что значит каждый принцип в SOLID?

1.3 Junior🔥 222 комментариев
#Архитектура и паттерны

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Принципы SOLID в объектно-ориентированном программировании

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

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

Каждый класс должен иметь одну и только одну причину для изменения.

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

Нарушение SRP в Android:

// ПЛОХО: Класс делает слишком много.
class UserRepository {
    fun saveUser(user: User) {
        // 1. Логика валидации
        // 2. Преобразование в JSON
        // 3. Сохранение в SharedPreferences
        // 4. Отправка события в Analytics
    }
}

Соблюдение SRP:

// ХОРОШО: Ответственности разделены.
class UserValidator { /* ... */ }
class UserToJsonConverter { /* ... */ }
class SharedPrefsManager { /* ... */ }
class AnalyticsTracker { /* ... */ }

class UserRepository(
    private val validator: UserValidator,
    private val prefsManager: SharedPrefsManager
) {
    fun saveUser(user: User) {
        validator.validate(user)
        prefsManager.saveUser(user)
    }
}

Вывод: Класс становится менее "хрупким", его проще тестировать и повторно использовать.

2. Open/Closed Principle (OCP) — Принцип открытости/закрытости

Программные сущности должны быть открыты для расширения, но закрыты для модификации.

Вы можете добавлять новое поведение, не изменяя существующий, уже протестированный и рабочий код. В Android это часто достигается через абстракции (интерфейсы) и полиморфизм.

Пример с OCP:

// Абстракция
interface DiscountStrategy {
    fun calculate(price: Double): Double
}

// Закрытые для модификации, готовые реализации
class RegularDiscount : DiscountStrategy {
    override fun calculate(price: Double) = price * 0.9
}

class PremiumDiscount : DiscountStrategy {
    override fun calculate(price: Double) = price * 0.7
}

// Класс открыт для расширения новыми стратегиями
class PriceCalculator(private val strategy: DiscountStrategy) {
    fun calculateFinalPrice(price: Double): Double {
        return strategy.calculate(price)
    }
}

// Использование: легко добавить BlackFridayDiscount, не трогая PriceCalculator.

Этот принцип лежит в основе многих архитектурных паттернов, таких как Clean Architecture и MVVM, где слои зависят от абстракций.

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения корректности программы.

Наследующий класс должен дополнять, а не изменять или нарушать поведение базового класса. Клиентский код, работающий с базовым классом, не должен "ломаться" или знать о конкретном подтипе.

Классическое нарушение LSP (прямоугольник-квадрат):

open class Rectangle {
    open var width: Int = 0
    open var height: Int = 0
    fun area() = width * height
}

class Square : Rectangle() {
    override var width: Int
        get() = super.width
        set(value) {
            super.width = value
            super.height = value // Нарушение! Изменяет поведение сеттера.
        }
    // Аналогично для height
}

// Клиентский код ожидает, что ширина и высота независимы.
fun resize(rectangle: Rectangle) {
    rectangle.width = 5
    rectangle.height = 4
    assert(rectangle.area() == 20) // Для Square это утверждение НЕ сработает!
}

В Android-контексте: если Fragment является подтипом Activity (что не так), то замена одного на другой привела бы к крашу. LSP гарантирует, что, например, все реализации интерфейса RecyclerView.Adapter будут вести себя предсказуемо.

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов

Клиенты не должны зависеть от методов, которые они не используют.

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

Нарушение ISP:

interface MultiFunctionDevice {
    fun print(document: Document)
    fun scan(document: Document)
    fun fax(document: Document)
}

class SimplePrinter : MultiFunctionDevice {
    override fun print(document: Document) { /* реализация */ }
    override fun scan(document: Document) {
        throw NotImplementedError("Мой принтер не умеет сканировать!") // ПЛОХО
    }
    override fun fax(document: Document) { /* аналогично */ }
}

Соблюдение ISP:

interface Printer { fun print(document: Document) }
interface Scanner { fun scan(document: Document) }
interface FaxMachine { fun fax(document: Document) }

class SimplePrinter : Printer {
    override fun print(document: Document) { /* только печать */ }
}

class AllInOneMachine : Printer, Scanner, FaxMachine {
    // Реализует все необходимые методы
}

В Android так построены многие API, например, отдельные интерфейсы для разных callback'ов View.OnClickListener, View.OnLongClickListener.

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Это ключевой принцип для достижения слабой связанности (loose coupling). Вместо создания зависимостей внутри класса, их нужно "внедрять" извне (Dependency Injection).

Нарушение DIP:

class OrderProcessor {
    // Прямая зависимость от конкретной реализации (детали)
    private val paymentGateway = PayPalGateway()

    fun process(order: Order) {
        paymentGateway.charge(order.total)
    }
}

Соблюдение DIP:

// Абстракция
interface PaymentGateway {
    fun charge(amount: Double)
}

// Детали, зависящие от абстракции
class PayPalGateway : PaymentGateway { /* ... */ }
class StripeGateway : PaymentGateway { /* ... */ }

// Модуль верхнего уровня зависит от абстракции
class OrderProcessor(private val paymentGateway: PaymentGateway) {
    fun process(order: Order) {
        paymentGateway.charge(order.total)
    }
}

// Зависимость инжектируется извне (например, через Dagger/Hilt)
val processor = OrderProcessor(StripeGateway())

Это основа тестируемости: в юнит-тестах вы можете внедрить Mock-объект, реализующий PaymentGateway.

Итог для Android-разработчика

Применение SOLID в Android ведет к:

  • Упрощению тестирования (Unit Tests).
  • Повышению переиспользуемости компонентов.
  • Снижению риска появления багов при изменениях.
  • Более чистой и понятной архитектуре (использованию MVVM, Clean Arch).
  • Эффективному использованию DI-фреймворков (Dagger, Hilt, Koin).

Пренебрежение этими принципами приводит к появлению "God-Object", "Spaghetti Code", который невозможно поддерживать, особенно в долгосрочных проектах с большой командой. SOLID — это не строгие правила, а руководства для создания устойчивого к изменениям кода.

Что значит каждый принцип в SOLID? | PrepBro