Паттерны проектирования в Kotlin с примерами: продвинутый уровень
На начальном уровне разработчики осваивают классические паттерны: Singleton, Factory, Observer. Однако при построении сложных, масштабируемых систем требуется более тонкая архитектурная настройка. В этой статье мы разберём четыре продвинутых паттерна проектирования на Kotlin: Компоновщик, Посетитель, Состояние и Мост. Каждый паттерн будет проиллюстрирован практическим примером кода и пояснением, когда его стоит применять.
1. Паттерн Компоновщик (Composite)
Паттерн Компоновщик позволяет клиентам единообразно работать с отдельными объектами и их группами. Это идеальное решение для построения иерархических структур, таких как файловые системы, меню интерфейсов или графические сцены.
В Kotlin мы часто используем sealed классы для представления узлов дерева. Рассмотрим пример построения структуры "Файлы и Папки":
sealed class FileSystemNode(open val name: String) {
data class File(override val name: String, val size: Long) : FileSystemNode(name)
data class Directory(
override val name: String,
val children: MutableList<FileSystemNode> = mutableListOf()
) : FileSystemNode(name) {
fun add(node: FileSystemNode) {
children.add(node)
}
fun remove(node: FileSystemNode) {
children.remove(node)
}
fun getTotalSize(): Long = children.sumOf {
when (it) {
is File -> it.size
is Directory -> it.getTotalSize()
}
}
}
}
// Использование
fun main() {
val root = FileSystemNode.Directory("root")
root.add(FileSystemNode.File("readme.txt", 1024))
val subDir = FileSystemNode.Directory("src")
subDir.add(FileSystemNode.File("main.kt", 4096))
root.add(subDir)
println("Общий размер: ${root.getTotalSize()} байт") // 5120
}
Плюсы: упрощает клиентский код, поддерживает рекурсивные операции. Минусы: может усложнить систему, если дерево слишком глубокое.
2. Паттерн Посетитель (Visitor)
Посетитель позволяет добавлять новые операции к объектам без изменения их классов. Это особенно полезно, когда у вас есть стабильная иерархия классов, но часто меняющиеся алгоритмы обработки.
В Kotlin отлично работает с sealed классами. Реализуем пример вычисления площади для фигур:
sealed class Shape
class Circle(val radius: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
interface ShapeVisitor<T> {
fun visit(circle: Circle): T
fun visit(rectangle: Rectangle): T
}
class AreaCalculator : ShapeVisitor<Double> {
override fun visit(circle: Circle) = Math.PI * circle.radius * circle.radius
override fun visit(rectangle: Rectangle) = rectangle.width * rectangle.height
}
// Использование
fun <T> Shape.accept(visitor: ShapeVisitor<T>): T = when (this) {
is Circle -> visitor.visit(this)
is Rectangle -> visitor.visit(this)
}
fun main() {
val shapes = listOf(Circle(5.0), Rectangle(3.0, 4.0))
val calculator = AreaCalculator()
shapes.forEach { println("Площадь: ${it.accept(calculator)}") }
}
Когда применять: если структура объектов редко меняется, а операции над ними — часто. В Kotlin можно использовать when для простых случаев, но Visitor остаётся незаменимым при сложной логике с множеством типов.
3. Паттерн Состояние (State)
Паттерн Состояние позволяет объекту менять своё поведение в зависимости от внутреннего состояния. Это элегантная альтернатива громоздким if-else или switch-case.
В Kotlin мы можем использовать sealed классы для представления состояний. Пример — заказ в интернет-магазине:
sealed class OrderState {
object New : OrderState()
object Paid : OrderState()
object Shipped : OrderState()
object Delivered : OrderState()
}
class Order {
var state: OrderState = OrderState.New
private set
fun pay() {
state = when (state) {
is OrderState.New -> {
println("Оплата прошла успешно")
OrderState.Paid
}
else -> {
println("Невозможно оплатить: текущее состояние $state")
state
}
}
}
fun ship() {
state = when (state) {
is OrderState.Paid -> {
println("Заказ отправлен")
OrderState.Shipped
}
else -> {
println("Невозможно отправить: текущее состояние $state")
state
}
}
}
fun deliver() {
state = when (state) {
is OrderState.Shipped -> {
println("Заказ доставлен")
OrderState.Delivered
}
else -> {
println("Невозможно подтвердить доставку: текущее состояние $state")
state
}
}
}
}
fun main() {
val order = Order()
order.pay() // Оплата прошла успешно
order.ship() // Заказ отправлен
order.deliver() // Заказ доставлен
order.pay() // Невозможно оплатить: текущее состояние Delivered
}
Преимущество: код становится читаемым и легко расширяемым — добавление нового состояния не требует изменения существующих методов.
4. Паттерн Мост (Bridge)
Мост отделяет абстракцию от реализации, позволяя изменять их независимо. Это мощный инструмент для борьбы с "взрывом классов" при комбинаторике.
Рассмотрим пример: система уведомлений с разными платформами и типами сообщений.
// Реализация (Implementation)
interface MessageSender {
fun send(message: String)
}
class EmailSender : MessageSender {
override fun send(message: String) = println("Email: $message")
}
class SmsSender : MessageSender {
override fun send(message: String) = println("SMS: $message")
}
// Абстракция (Abstraction)
abstract class Notification(protected val sender: MessageSender) {
abstract fun notifyUser(message: String)
}
class UrgentNotification(sender: MessageSender) : Notification(sender) {
override fun notifyUser(message: String) {
sender.send("[СРОЧНО] $message")
}
}
class RegularNotification(sender: MessageSender) : Notification(sender) {
override fun notifyUser(message: String) {
sender.send("[ОБЫЧНОЕ] $message")
}
}
// Использование
fun main() {
val emailUrgent = UrgentNotification(EmailSender())
val smsRegular = RegularNotification(SmsSender())
emailUrgent.notifyUser("