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

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

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

Начать курс

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

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

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

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

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

// ❌ Нарушение SRP
class UserManager {
    fun registerUser(email: String, password: String) {
        if (isValidEmail(email) && isValidPassword(password)) {
            saveToDatabase(email, password)
            sendWelcomeEmail(email)
        }
    }

    private fun isValidEmail(email: String): Boolean = email.contains("@")
    private fun isValidPassword(password: String): Boolean = password.length >= 6
    private fun saveToDatabase(email: String, password: String) { /* ... */ }
    private fun sendWelcomeEmail(email: String) { /* ... */ }
}

Рефакторинг с соблюдением SRP:

// ✅ Соблюдение SRP
class UserValidator {
    fun isValidEmail(email: String): Boolean = email.contains("@")
    fun isValidPassword(password: String): Boolean = password.length >= 6
}

class UserRepository {
    fun save(email: String, password: String) { /* Сохранение в БД */ }
}

class EmailService {
    fun sendWelcomeEmail(email: String) { /* Отправка email */ }
}

class UserRegistrationService(
    private val validator: UserValidator,
    private val repository: UserRepository,
    private val emailService: EmailService
) {
    fun registerUser(email: String, password: String) {
        if (validator.isValidEmail(email) && validator.isValidPassword(password)) {
            repository.save(email, password)
            emailService.sendWelcomeEmail(email)
        }
    }
}

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

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

Практика в Kotlin: Используем интерфейсы и полиморфизм. Вместо if-else или when для обработки разных типов, создадим иерархию.

// ❌ Нарушение OCP
class DiscountCalculator {
    fun calculateDiscount(customerType: String, amount: Double): Double {
        return when (customerType) {
            "Regular" -> amount * 0.05
            "Premium" -> amount * 0.10
            "VIP" -> amount * 0.20
            else -> 0.0
        }
    }
    // Чтобы добавить "Gold", нужно изменить этот метод!
}
// ✅ Соблюдение OCP
interface DiscountStrategy {
    fun applyDiscount(amount: Double): Double
}

class RegularDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double): Double = amount * 0.05
}

class PremiumDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double): Double = amount * 0.10
}

class VIPDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double): Double = amount * 0.20
}

// Новый тип добавляется без изменения существующего кода
class GoldDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double): Double = amount * 0.15
}

class DiscountCalculator {
    fun calculateDiscount(strategy: DiscountStrategy, amount: Double): Double {
        return strategy.applyDiscount(amount)
    }
}

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

Теория: Функции, которые используют базовый тип, должны иметь возможность использовать подтипы, не зная об этом. Подкласс не должен нарушать контракт базового класса.

Практика в Kotlin: Классический пример с фигурами. Квадрат не должен наследоваться от Прямоугольника, если это нарушает логику.

// ❌ Нарушение LSP
open class Rectangle {
    open var width: Int = 0
    open var height: Int = 0

    fun area(): Int = 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
        }
}

fun printArea(rectangle: Rectangle) {
    rectangle.width = 5
    rectangle.height = 10
    println("Ожидаемая площадь: 50, реальная: ${rectangle.area()}")
    // Для Square: ширина=10, высота=10, площадь=100 — неверно!
}
// ✅ Соблюдение LSP
interface Shape {
    fun area(): Int
}

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

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

fun printArea(shape: Shape) {
    println("Площадь: ${shape.area()}")
}

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

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

Практика в Kotlin: Разделим интерфейс для работы с документами.

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

class Robot : Worker {
    override fun work() { /* Работает */ }
    override fun eat() { /* Робот не ест! */ }
    override fun sleep() { /* Робот не спит! */ }
}
// ✅ Соблюдение ISP
interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

class Human : Workable, Eatable, Sleepable {
    override fun work() { /* Работает */ }
    override fun eat() { /* Ест */ }
    override fun sleep() { /* Спит */ }
}

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

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

Теория: Модули верхнего уровня не должны зависеть от модулей нижнего уровня.

Блоги

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