SOLID принципы в Kotlin: теория и практика для чистого кода

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

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

Начать курс

Введение: Почему SOLID важен в Kotlin?

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

В этой статье мы разберем каждый из пяти принципов на конкретных примерах на Kotlin, от теории до практической реализации. Вы узнаете, как избежать типичных антипаттернов и сделать ваш код чище.

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

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

Практика в Kotlin: Часто разработчики нарушают SRP, помещая в один класс логику обработки данных, валидацию и работу с внешними сервисами.

Пример нарушения:

class UserManager {
    fun saveUser(user: User) {
        // Сохранение в базу данных
        println("User saved to DB")
    }

    fun sendEmail(user: User, message: String) {
        // Отправка email
        println("Email sent to ${user.email}")
    }

    fun validateUser(user: User): Boolean {
        // Валидация данных
        return user.name.isNotEmpty()
    }
}

Здесь UserManager отвечает за сохранение, отправку email и валидацию. Если изменится логика отправки email, придется менять этот класс, что может поломать сохранение данных.

Исправленный вариант с SRP:

class UserRepository {
    fun save(user: User) {
        println("User saved to DB")
    }
}

class EmailService {
    fun send(user: User, message: String) {
        println("Email sent to ${user.email}")
    }
}

class UserValidator {
    fun validate(user: User): Boolean {
        return user.name.isNotEmpty()
    }
}

Теперь каждый класс имеет единственную ответственность, что упрощает тестирование и модификацию кода.

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

Теория: Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы должны иметь возможность добавлять новое поведение без изменения существующего кода.

Практика в Kotlin: Используйте абстракции (интерфейсы или абстрактные классы) и полиморфизм. В Kotlin для этого отлично подходят sealed class и when выражения.

Пример нарушения:

class AreaCalculator {
    fun calculate(shape: Any): Double {
        return when (shape) {
            is Circle -> Math.PI * shape.radius * shape.radius
            is Square -> shape.side * shape.side
            else -> throw IllegalArgumentException("Unknown shape")
        }
    }
}

Чтобы добавить новую фигуру (например, Triangle), придется модифицировать метод calculate.

Исправленный вариант с OCP:

interface Shape {
    fun area(): Double
}

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

class Square(private val side: Double) : Shape {
    override fun area() = side * side
}

class Triangle(private val base: Double, private val height: Double) : Shape {
    override fun area() = 0.5 * base * height
}

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

Теперь для добавления новой фигуры достаточно создать новый класс, реализующий интерфейс Shape. Класс AreaCalculator не требует изменений.

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

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

Практика в Kotlin: Классический пример нарушения — наследование класса Rectangle от Square или наоборот, когда изменение ширины квадрата ломает логику прямоугольника.

Пример нарушения:

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

class Square : Rectangle() {
    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
        }
}

Если клиентский код ожидает Rectangle и устанавливает ширину и высоту независимо, квадрат нарушит поведение.

Исправленный вариант с LSP: Лучше использовать композицию или отдельные интерфейсы.

interface Shape {
    fun area(): Int
}

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

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

Теперь оба класса независимы и не нарушают контракт интерфейса Shape.

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

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

Практика в Kotlin: Разделяйте интерфейсы на маленькие, логически связанные группы. В Kotlin это легко сделать с помощью extension-функций, если интерфейс слишком большой.

Пример нарушения:

interface Worker {
    fun work()
    fun eat()
    fun sleep()
}

class HumanWorker : Worker {
    override fun work() { println("Working") }
    override fun eat() { println("Eating") }
    override fun sleep() { println("Sleeping") }
}

class RobotWorker : Worker {
    override fun work() { println("Working") }
    override fun eat() { throw UnsupportedOperationException() }
    override fun sleep() { throw UnsupportedOperationException() }
}

RobotWorker вынужден реализовывать методы eat и sleep, которые ему не нужны

Блоги

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