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

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

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

Начать курс

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

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

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

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

Суть принципа

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

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

class UserManager {
    fun createUser(name: String, email: String) { /* ... */ }
    fun deleteUser(id: Int) { /* ... */ }
    fun sendEmail(to: String, subject: String) { /* ... */ }
    fun generateReport(userId: Int): String { /* ... */ }
}

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

Хороший пример (соблюдение SRP)

class UserRepository {
    fun createUser(name: String, email: String) { /* ... */ }
    fun deleteUser(id: Int) { /* ... */ }
}

class EmailService {
    fun sendEmail(to: String, subject: String) { /* ... */ }
}

class ReportGenerator {
    fun generateReport(userId: Int): String { /* ... */ }
}

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

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

Суть принципа

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

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

class PaymentProcessor {
    fun process(paymentType: String) {
        when (paymentType) {
            "credit_card" -> // обработка кредитной карты
            "paypal" -> // обработка PayPal
            else -> throw IllegalArgumentException()
        }
    }
}

При добавлении нового способа оплаты (например, Apple Pay) придётся модифицировать PaymentProcessor, что нарушает OCP.

Хороший пример (соблюдение OCP)

interface PaymentMethod {
    fun process(amount: Double)
}

class CreditCardPayment : PaymentMethod {
    override fun process(amount: Double) { /* ... */ }
}

class PayPalPayment : PaymentMethod {
    override fun process(amount: Double) { /* ... */ }
}

class PaymentProcessor(private val method: PaymentMethod) {
    fun processPayment(amount: Double) {
        method.process(amount)
    }
}

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

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

Суть принципа

Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы. Проще говоря, если у вас есть класс Bird и подкласс Penguin, то код, работающий с Bird, не должен ломаться при подстановке Penguin.

Плохой пример (нарушение 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 }
    override var height: Int
        get() = super.height
        set(value) { super.width = value; super.height = value }
}

Клиентский код, ожидающий Rectangle, может установить разные значения для width и height, но в Square это нарушит логику.

Хорошее решение (через абстракцию)

interface Shape {
    fun area(): Int
}

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

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

В этом примере Rectangle и Square не наследуются друг от друга, а реализуют общий интерфейс Shape. Подстановка работает корректно.

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

Суть принципа

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

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

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

class Robot : Worker {
    override fun work() { /* работает */ }
    override fun eat() { /* роботы не едят! */ }
}

Робот вынужден реализовывать метод eat(), который ему не нужен.

Хороший пример (соблюдение ISP)

interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

class Human : Workable, Eatable {
    override fun work() { /* работает */ }
    override fun eat() { /* ест */ }
}

class Robot : Workable {
    override fun work() { /* работает */ }
}

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

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

Суть принципа

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

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

class EmailNotifier {
    fun sendEmail(message:                

Блоги

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