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) — Принцип инверсии зависимостей
Теория: Модули верхнего уровня не должны зависеть от модулей нижнего уровня.