SOLID принципы в Kotlin: теория и практика от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

SOLID принципы в Kotlin: от теории к практике

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

Понимание SOLID критически важно для любого Kotlin-разработчика, особенно при работе с фреймворками вроде Spring Boot или Ktor. Эти принципы лежат в основе чистой архитектуры и паттернов проектирования.

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

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

В Kotlin этот принцип особенно важен из-за поддержки функционального стиля. Рассмотрим плохой пример:

// ❌ Нарушение SRP: класс занимается и логированием, и работой с БД
class UserService {
    fun saveUser(user: User) {
        // Логирование
        println("Saving user: ${user.name}")
        // Бизнес-логика
        validateUser(user)
        // Работа с БД
        database.insert(user)
    }
}

Исправим ситуацию, разделив ответственность:

// ✅ Правильный подход: каждая ответственность в отдельном классе
class Logger {
    fun log(message: String) = println("[LOG] $message")
}

class UserValidator {
    fun validate(user: User): Boolean = user.name.isNotBlank()
}

class UserRepository {
    fun save(user: User) { /* сохранение в БД */ }
}

class UserService(
    private val logger: Logger,
    private val validator: UserValidator,
    private val repository: UserRepository
) {
    fun saveUser(user: User) {
        logger.log("Saving user: ${user.name}")
        if (validator.validate(user)) {
            repository.save(user)
        }
    }
}

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

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

В Kotlin для реализации OCP отлично подходят sealed-классы и интерфейсы:

// ❌ Нарушение OCP: при добавлении нового типа фигуры нужно менять класс
class AreaCalculator {
    fun calculate(shape: Any): Double = when (shape) {
        is Circle -> 3.14 * shape.radius * shape.radius
        is Rectangle -> shape.width * shape.height
        else -> throw IllegalArgumentException()
    }
}

Правильная реализация с использованием полиморфизма:

// ✅ Правильный подход: новые фигуры добавляются без изменения существующего кода
interface Shape {
    fun area(): Double
}

class Circle(val radius: Double) : Shape {
    override fun area() = Math.PI * radius * radius
}

class Rectangle(val width: Double, val height: Double) : Shape {
    override fun area() = width * height
}

// Теперь можно добавить треугольник без изменения AreaCalculator
class Triangle(val base: Double, val height: Double) : Shape {
    override fun area() = 0.5 * base * height
}

class AreaCalculator {
    fun calculate(shape: Shape) = shape.area()
}

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

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

Рассмотрим классический пример с прямоугольником и квадратом:

// ❌ Нарушение LSP: квадрат не может заменить прямоугольник
open class Rectangle(
    open var width: Int,
    open var height: Int
) {
    fun area() = width * height
}

class Square(side: Int) : Rectangle(side, side) {
    override var width: Int
        get() = super.width
        set(value) {
            super.width = value
            super.height = value // нарушает поведение
        }
    
    override var height: Int
        get() = super.height
        set(value) {
            super.width = value
            super.height = value
        }
}

Правильное решение — использовать интерфейс:

// ✅ Правильный подход: разделяем интерфейсы
interface Shape {
    fun area(): Int
}

class Rectangle(private var width: Int, private var height: Int) : Shape {
    fun setWidth(w: Int) { width = w }
    fun setHeight(h: Int) { height = h }
    override fun area() = width * height
}

class Square(private var side: Int) : Shape {
    fun setSide(s: Int) { side = s }
    override fun area() = side * side
}

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

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

// ❌ Нарушение ISP: "толстый" интерфейс
interface Worker {
    fun work()
    fun eat()
    fun sleep()
}

class Robot : Worker {
    override fun work() { /* работает */ }
    override fun eat() { throw UnsupportedOperationException() } // ❌
    override fun sleep() { throw UnsupportedOperationException() } // ❌
}

Решение — разделить интерфейсы:

// ✅ Правильный подход: специализированные интерфейсы
interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

class Human : Workable, Eatable, Sleepable {
    override fun work() = println("Человек работает")
    override fun eat() = println("Человек ест")
    override fun sleep() = println("Человек спит")
}

class Robot : Workable {
    override fun work() = println("Робот работает")
}

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

Суть: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей — детали должны зависеть от абстракций.

В Kotlin этот принцип часто реализуется через внедрение зависимостей (Dependency Injection):

// ❌ Нарушение DIP: высокоуровневый модуль зависит от низкоуровневого
class EmailService {
    fun sendEmail(to: String, message: String) {
        // Логика отправки через SMTP
    }
}

class NotificationService {
    private val emailService = EmailService() // ❌ жесткая связь
    
    fun notifyUser(user: User, message: String) {
        emailService.sendEmail(user.email, message)
    }
}

Правильная реализация с инверсией зависимостей:

// ✅ Правильный подход: зависимость от абстракции
interface MessageSender {                

Блоги

Рекомендаций по книгам