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

Для чего нужен SOLID?

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

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

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

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

# Для чего нужен SOLID

Что такое SOLID

SOLID — это аббревиатура пяти принципов проектирования объектно-ориентированного кода, предложенных Robert C. Martin (Uncle Bob). Эти принципы помогают писать код, который:

  • Легко тестировать
  • Легко поддерживать и изменять
  • Избегает дублирования
  • Масштабируется с ростом приложения

Это практический фундамент Clean Architecture.

Пять принципов SOLID

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

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

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

Плохо (нарушение SRP)

// Класс отвечает за ВСЁ
class UserService {
    // Ответ 1: Управление пользователем
    fun createUser(name: String, email: String): User { ... }
    fun updateUser(user: User) { ... }
    fun deleteUser(id: Int) { ... }
    
    // Ответ 2: Сохранение в БД
    fun saveToDatabase(user: User) { ... }
    fun loadFromDatabase(id: Int): User { ... }
    
    // Ответ 3: Отправка email
    fun sendWelcomeEmail(user: User) { ... }
    fun sendPasswordResetEmail(user: User) { ... }
    
    // Ответ 4: Логирование
    fun log(message: String) { ... }
}

// Если поменять способ логирования — меняем UserService
// Если поменять БД с Room на Firebase — меняем UserService
// Если поменять email провайдера — меняем UserService
// Класс меняется по много причинам!

Хорошо (SRP соблюдается)

// Класс 1: Только управление пользователем
class UserService(
    private val userRepository: UserRepository,
    private val emailService: EmailService,
    private val logger: Logger
) {
    fun createUser(name: String, email: String): User {
        val user = User(name = name, email = email)
        userRepository.save(user)
        emailService.sendWelcomeEmail(user)
        logger.info("User created: $user")
        return user
    }
}

// Класс 2: Только работа с БД
class UserRepositoryImpl(
    private val userDao: UserDao
) : UserRepository {
    override fun save(user: User) = userDao.insert(user.toEntity())
    override fun load(id: Int): User = userDao.getUser(id).toDomain()
}

// Класс 3: Только отправка email
class EmailService {
    fun sendWelcomeEmail(user: User) {
        val email = Email(
            to = user.email,
            subject = "Welcome!",
            body = "Hello ${user.name}!"
        )
        emailProvider.send(email)
    }
}

// Класс 4: Только логирование
class Logger {
    fun info(message: String) { ... }
    fun error(message: String) { ... }
}

// Теперь:
// - Меняется БД → меняем только UserRepositoryImpl
// - Меняется email провайдер → меняем только EmailService
// - Меняется логирование → меняем только Logger

Преимущества SRP:

  • Легче понять код (один класс = одна ответственность)
  • Легче тестировать (мокируем только нужные зависимости)
  • Легче переиспользовать (UserRepository можно использовать где угодно)
  • Меньше side effects (изменение в одном месте не ломает другие)

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

Суть: Класс открыт для расширения, но закрыт для модификации.

Ты должен быть способен добавлять новую функциональность БЕЗ изменения существующего кода.

Плохо (нарушение OCP)

// Закрыт для расширения
class PaymentProcessor {
    fun process(payment: Payment): Boolean {
        return when (payment.method) {
            PaymentMethod.CREDIT_CARD -> processCreditCard(payment)  // Нужно менять этот класс
            PaymentMethod.PAYPAL -> processPayPal(payment)            // для каждого нового метода
            PaymentMethod.BITCOIN -> processBitcoin(payment)          // платежа
            else -> false
        }
    }
}

// Если добавить PaymentMethod.APPLE_PAY, нужно менять PaymentProcessor
// Классу приходится знать про ВСЕ способы платежей

Хорошо (OCP соблюдается)

// Интерфейс (контракт)
interface PaymentHandler {
    fun process(payment: Payment): Boolean
    fun supports(method: PaymentMethod): Boolean
}

// Реализация для кредитных карт (можно расширять)
class CreditCardPaymentHandler : PaymentHandler {
    override fun process(payment: Payment): Boolean {
        // Логика обработки кредитной карты
        return validateCard(payment.cardNumber) && chargeCard(payment)
    }
    
    override fun supports(method: PaymentMethod) = method == PaymentMethod.CREDIT_CARD
}

// Реализация для PayPal (новый способ, не меняем старый код)
class PayPalPaymentHandler : PaymentHandler {
    override fun process(payment: Payment): Boolean {
        // Логика обработки PayPal
        return authenticatePayPal(payment) && transferMoney(payment)
    }
    
    override fun supports(method: PaymentMethod) = method == PaymentMethod.PAYPAL
}

// Реализация для Apple Pay (ещё новый способ, опять не меняем)
class ApplePayHandler : PaymentHandler {
    override fun process(payment: Payment): Boolean {
        // Логика обработки Apple Pay
        return validateApplePay(payment) && completeTransaction(payment)
    }
    
    override fun supports(method: PaymentMethod) = method == PaymentMethod.APPLE_PAY
}

// Процессор — ЗАКРЫТ для модификации, ОТКРЫТ для расширения
class PaymentProcessor(
    private val handlers: List<PaymentHandler>
) {
    fun process(payment: Payment): Boolean {
        val handler = handlers.find { it.supports(payment.method) }
        return handler?.process(payment) ?: false
    }
}

// Использование через Dependency Injection
val processor = PaymentProcessor(listOf(
    CreditCardPaymentHandler(),
    PayPalPaymentHandler(),
    ApplePayHandler()  // Новый обработчик, старый класс не менялся!
))

Преимущества OCP:

  • Добавлять новую функциональность без риска сломать старую
  • Разделение ответственности (каждый обработчик отвечает за свой способ)
  • Легче тестировать каждый обработчик отдельно
  • Меньше merge conflicts в команде

L — Liskov Substitution Principle (Принцип подстановки Лиского)

Суть: Объект подкласса должен заменяться объектом суперкласса без нарушения работы программы.

Проще: если Dog наследуется от Animal, то везде где используется Animal, можно использовать Dog.

Плохо (нарушение LSP)

// Суперкласс
open class Bird {
    open fun fly(): String = "I'm flying!"
}

// Подкласс 1: Нормальная птица
class Sparrow : Bird() {
    override fun fly(): String = "Sparrow is flying!"
}

// Подкласс 2: Пингвин НЕ может летать!
class Penguin : Bird() {
    override fun fly(): String {
        throw UnsupportedOperationException("Penguin can't fly!")  // Нарушение контракта!
    }
}

// Код, который использует Bird
fun makeBirdFly(bird: Bird) {
    println(bird.fly())  // Ожидает, что птица может летать
}

makeBirdFly(Sparrow())   // ✓ Работает
makeBirdFly(Penguin())   // ✗ Падает — нарушение LSP!

Хорошо (LSP соблюдается)

// Базовый класс
open class Bird {
    open fun eat() = "I'm eating"
}

// Летающие птицы
open class FlyingBird : Bird() {
    open fun fly(): String = "I'm flying!"
}

// Нелетающие птицы
open class NonFlyingBird : Bird() {
    open fun swim(): String = "I'm swimming!"
}

// Правильные подклассы
class Sparrow : FlyingBird() {
    override fun fly(): String = "Sparrow is flying!"
}

class Penguin : NonFlyingBird() {
    override fun swim(): String = "Penguin is swimming!"
}

// Теперь LSP соблюдается
fun makeFlyingBirdFly(bird: FlyingBird) {
    println(bird.fly())  // Все FlyingBird могут летать
}

makeFlyingBirdFly(Sparrow())  // ✓ Работает
// makeFlyingBirdFly(Penguin())  // ✗ Не компилируется — это нарушит контракт

Преимущества LSP:

  • Полиморфизм работает предсказуемо
  • Нет неожиданных исключений
  • Контракт класса соблюдается везде
  • Код безопаснее

I — Interface Segregation Principle (Принцип разделения интерфейса)

Суть: Много специфичных интерфейсов лучше, чем один большой интерфейс.

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

Плохо (нарушение ISP)

// Большой интерфейс, который ВСЁ содержит
interface Worker {
    fun work()
    fun eat()
    fun code()
    fun design()
    fun manage()
    fun sellProducts()
}

// Developer вынужден реализовать ВСЕ методы
class Developer : Worker {
    override fun work() { /* code */ }
    override fun eat() { /* eat */ }
    override fun code() { /* development */ }
    override fun design() { throw UnsupportedOperationException() }  // Не нужен
    override fun manage() { throw UnsupportedOperationException() }   // Не нужен
    override fun sellProducts() { throw UnsupportedOperationException() }  // Не нужен
}

// Designer вынужден реализовать методы программиста
class Designer : Worker {
    override fun work() { /* design */ }
    override fun eat() { /* eat */ }
    override fun code() { throw UnsupportedOperationException() }  // Не может
    override fun design() { /* design */ }
    override fun manage() { throw UnsupportedOperationException() }  // Не может
    override fun sellProducts() { throw UnsupportedOperationException() }  // Не может
}

Хорошо (ISP соблюдается)

// Много специфичных интерфейсов
interface Worker {
    fun work()
    fun eat()
}

interface Coder : Worker {
    fun code()
}

interface Designer : Worker {
    fun design()
}

interface Manager : Worker {
    fun manage()
}

interface Seller : Worker {
    fun sellProducts()
}

// Теперь Developer реализует только нужные интерфейсы
class Developer : Coder {
    override fun work() { /* development work */ }
    override fun eat() { /* eating */ }
    override fun code() { /* writing code */ }
}

// Designer реализует только нужные интерфейсы
class Designer : Designer {
    override fun work() { /* design work */ }
    override fun eat() { /* eating */ }
    override fun design() { /* creating designs */ }
}

// Manager реализует только нужные интерфейсы
class Manager : Manager, Seller {
    override fun work() { /* managing */ }
    override fun eat() { /* eating */ }
    override fun manage() { /* team management */ }
    override fun sellProducts() { /* product sales */ }
}

Преимущества ISP:

  • Класс не зависит от методов, которые не использует
  • Меньше пустых реализаций (throw UnsupportedOperationException)
  • Ясная контрактность (интерфейс = только нужные методы)
  • Легче мокировать (мокируем только нужное)

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

Суть: Зависимости должны быть на интерфейсы, а не на конкретные реализации.

Высокоуровневые модули не должны зависеть от низкоуровневых. Обе должны зависеть от абстракций.

Плохо (нарушение DIP)

// ViewModel зависит от конкретной реализации
class UserViewModel(
    private val userRepositoryImpl: UserRepositoryImpl  // Конкретная реализация!
) {
    fun loadUser(id: Int) {
        val user = userRepositoryImpl.getUser(id)  // Привязана к конкретной реализации
    }
}

// Если захотим поменять Repository на другую реализацию
// (например, с Room на Firebase), нужно менять ViewModel

Хорошо (DIP соблюдается)

// Интерфейс (абстракция)
interface UserRepository {
    fun getUser(id: Int): User
}

// Реализация 1
class UserRepositoryRoom(val userDao: UserDao) : UserRepository {
    override fun getUser(id: Int) = userDao.getUser(id).toDomain()
}

// Реализация 2
class UserRepositoryFirebase(val firestore: FirebaseFirestore) : UserRepository {
    override fun getUser(id: Int) = firestore.getUser(id).toDomain()
}

// ViewModel зависит от интерфейса, не от реализации
class UserViewModel(
    private val userRepository: UserRepository  // Интерфейс!
) {
    fun loadUser(id: Int) {
        val user = userRepository.getUser(id)
    }
}

// Через Dependency Injection выбираем реализацию
val viewModel1 = UserViewModel(UserRepositoryRoom(userDao))
val viewModel2 = UserViewModel(UserRepositoryFirebase(firestore))
// Одна ViewModel работает с обеими реализациями!

Преимущества DIP:

  • Легко менять реализацию (подменяем зависимость)
  • Легко тестировать (передаём mock)
  • Слабая связанность между компонентами
  • Гибкость и масштабируемость

Как SOLID помогает в реальной разработке

Сценарий 1: Добавить новую функцию

Без SOLID:

// Нужно менять существующий код
class UserService {
    fun process() {
        // 500 строк кода
        // Нужно добавить новую функцию
        // Риск сломать существующее
    }
}

С SOLID (OCP):

// Создаём новый класс, не трогаем старое
class NewFeatureService : UserProcessing {
    override fun process() { }
}
// Старый код не меняется, новый добавляется

Сценарий 2: Unit тестирование

Без SOLID:

class UserServiceTest {
    @Test
    fun testCreateUser() {
        // Нужно инициализировать Room, Retrofit, Firebase, email service...
        val service = UserService(database, api, firebase, emailService)
        // Много зависимостей, сложно изолировать тест
    }
}

С SOLID (DIP, SRP):

class UserServiceTest {
    @Test
    fun testCreateUser() {
        val mockRepository = mockk<UserRepository>()
        val mockEmailService = mockk<EmailService>()
        val service = UserService(mockRepository, mockEmailService)
        // Мокируем только нужное, тест быстрый и чистый
    }
}

Сценарий 3: Командная разработка

Без SOLID:

Фронтенд разработчик меняет UserService
Бэкенд разработчик меняет UserService
Конфликт в git!
Риск потерять код

С SOLID (SRP, OCP):

Фронтенд разработчик работает с ViewModelLayer
Бэкенд разработчик работает с RepositoryLayer
Нет конфликтов!
Можно работать параллельно

Практический пример: News App

// S — Single Responsibility
class NewsViewModel(val newsRepository: NewsRepository)  // Только UI logic
class NewsRepository(val api: NewsApi, val db: NewsDao)  // Только data management
class NewsApi(val httpClient: HttpClient)  // Только network requests

// O — Open/Closed
interface NewsSource {
    suspend fun getNews(): List<News>
}
class RemoteNewsSource(val api: NewsApi) : NewsSource { }
class LocalNewsSource(val db: NewsDao) : NewsSource { }
// Новый источник — новый класс, старый не меняется

// L — Liskov Substitution
val sources: List<NewsSource> = listOf(
    RemoteNewsSource(api),
    LocalNewsSource(db)
)
sources.forEach { it.getNews() }  // Все работают одинаково

// I — Interface Segregation
interface NewsLoader { suspend fun load(): List<News> }
interface NewsFilter { fun filter(category: String): List<News> }
interface NewsSorter { fun sort(order: SortOrder): List<News> }
// Не один большой интерфейс, а много специфичных

// D — Dependency Inversion
class NewsViewModel(val repository: NewsRepository)  // Зависит от интерфейса
// В тестах передаём mock
val viewModel = NewsViewModel(mockk<NewsRepository>())

Вывод

SOLID принципы — это не dogma, а практические инструменты для написания кода, который:

  • Легче разуметь — каждый класс делает одно
  • Легче менять — изменения локализованы
  • Легче тестировать — зависимости инъектируются
  • Легче масштабировать — новая функциональность = новые классы
  • Меньше bugs — меньше side effects и неожиданных изменений

В контексте Android и Kotlin, SOLID вместе с Clean Architecture создают мощную комбинацию для построения масштабируемых приложений.