Паттерны проектирования - проверенные временем решения общих задач в программировании. Они разделяются на три категории:

  • Порождающие (Creational)

  • Структурные (Structural)

  • Поведенческие (Behavioral)

В этой части статьи рассмотрим порождающие и структурные паттерны.

А во второй части статьи - поведенческие.

Порождающие паттерны

1. Singleton (Одиночка)

Описание: Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.

Когда использовать: Когда нужен единственный экземпляр класса для контроля доступа к общему ресурсу.

Пример кода:

object DatabaseConnection {
  fun connect() {
    println("Подключение к базе данных")
  }
}

fun main() {
  DatabaseConnection.connect()
}

Объяснение: в Kotlin есть ключевое слово object, которое автоматически создает синглтон.

2. Factory Method (Фабричный метод)

Описание: Определяет интерфейс для создания объектов, но позволяет подклассам решать, какой класс инстанцировать.

Когда использовать: Когда заранее неизвестны типы и зависимости объектов, с которыми должен работать ваш код.

Пример кода:

interface Transport {
  fun deliver()
}

class Truck : Transport {
  override fun deliver() {
    println("Доставка грузовиком по суше")
  }
}

class Ship : Transport {
  override fun deliver() {
    println("Доставка кораблем по морю")
  }
}

abstract class Logistics {
  abstract fun createTransport(): Transport

  fun planDelivery() {
    val transport = createTransport()
    transport.deliver()
  }
}

class RoadLogistics : Logistics() {
  override fun createTransport(): Transport = Truck()
}

class SeaLogistics: Logistics() {
  override fun createTransport = Ship()
}

fun main() {
  val logistics: Logistics = RoadLogistics()
  logistics.planDelivery()
}

3. Abstract Factory (Абстрактная фабрика)

Описание: Предоставляет интерфейс для создания семейств связанных или зависимых объектов без указания их конкретных классов.

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

Пример кода:

interface Button {
  fun render()
}

interface Checkbox {
  fun render()
}

class WindowsButton : Button {
  override fun render() {
    println("Рендеринг кнопки в стиле Windows")
  }
}

class MacOSButton : Button {
  override fun render() {
    println("Рендеринг кнопки в стиле MacOS")
  }
}

class WindowsCheckbox : Checkbox {
  override fun render() {
    println("Рендеринг чекбокса в стиле Windows")
  }
}

class MacOSCheckbox : Checkbox {
  override fun render() {
    println("Рендеринг чекбокса в стиле MacOS")
  }
}

interface GUIFactory {
  fun createButton(): Button
  fun createCheckbox(): Checkbox
}

class WindowsFactory : GUIFactory {
  override fun createButton(): Button = WindowsButton()
  override fun createCheckbox(): Checkbox = WindowsCheckbox()
}

class MacOSFactory : GUIFactory {
  override fun createButton(): Button = MacOSButton()
  override fun createCheckbox(): Checkbox = MacOSCheckbox()
}

fun main() {
  val factory: GUIFactory = WindowsFactory()
  val button = factory.createButton()
  val checkbox = factory.createCheckbox()
  button.render()
  checkbox.render()
}

4. Builder (Строитель)

Описание: Разделяет создание сложного объекта от его представления, так что один и тот же процесс строительства может создавать разные представления.

Когда использовать: Когда процесс создания объекта должен быть независим от его составляющих и как они собираются.

Пример кода:

class House private constructor(
  val walls: Int,
  val doors: Int, 
  val windows: Int,
  val hasGarage: Boolean,
  val hasSwimmingPool: Boolean
) {
  data class Builder(
    var walls: Int = 0,
    var doors: Int = 0,
    var windows: Int = 0,
    var hasGarage: Boolean = false,
    var hasSwimmingPool: Boolean = false
  ) {
    fun walls(count: Int) = apply { this.walls = count }
    fun doors(count: Int) = apply { this.doors = count }
    fun windows(count: Int) = apply { this.windows = count }
    fun hasGarage(value: Boolean) = apply { this.hasGarage = value }
    fun hasSwimmingPool(value: Boolean) = apply { this.hasSwimmingPool = value }
    fun build() = House(walls, doors, windows, hasGarage, hasSwimmingPool)
  }
}

fun main() {
  val house = House.Builder()
      .walls(4)
      .doors(2)
      .windows(6)
      .hasGarage(true)
      .build()

  println("Дом с ${house.walls} стенами, ${house.doors} дверями, " + 
          "${house.windows} окнами, гараж: ${house.hasGarage}")    
}

5. Prototype (Прототип)

Описание: Позволяет копировать объекты не вдаваясь в подробности их реализации.

Когда использовать: Когда создание объекта дорогостоящее или сложно, а копирование может быть более эффективным.

Пример кода:

abstract class Shape : Cloneable {
  var x: Int = 0
  var y: Int = 0

  public override fun clone(): Shape {
    return super.clone() as Shape
  }

  abstract fun draw()
}

class Rectangle(var width: Int, var height: Int) : Shape() {
  override fun clone(): Shape {
    val clone = super.clone() as Rectangle
    clone.width = this.width
    clone.height = this.height
    return clone
  }

  override fun draw() {
    println("Рисуем прямоугольник шириной $width и высотой $height")
  }
}

fun main() {
  val original = Rectangle(10, 20)
  val copy = original.clone() as Rectangle
  copy.width = 30

  original.draw()
  copy.draw()
}

Структурные паттерны

6. Adapter (Адаптер)

Описание: Позволяет объектам с несовместимыми интерфейсами работать вместе.

Когда использовать: Когда нужно использовать существующий класс, но его интерфейс не соответствует потребностям.

Пример кода:

interface RoundPeg {
  val radius: Double
}

class RoundHole(val radius: Double) {
  fun fits(peg: RoundPeg): Boolean = this.radius >= peg.radius
}

class SquarePeg(val width: Double)

class SquarePegAdapter(private val peg: SquarePeg) : RoundPeg {
  override val radius: Double
      get() = peg.width * Math.sqrt(2.0) / 2
}

fun main() {
  val hole = RoundHole(5.0)
  val smallSquarePeg = SquarePeg(5.0)
  val largeSquarePeg = SquarePeg(10.0)

  val smallPegAdapter = SquarePegAdapter(smallSquarePeg)
  val largePegAdapter = SquarePegAdapter(largeSquarePeg)

  println("Малый квадратный колышек подходит? ${hole.fits(smallPegAdapter)}")
  println("Большой квадратный колышек подходит? ${hole.fits(largePegAdapter)}")
}

7. Bridge (Мост)

Описание: Разделяет абстракцию и реализацию так, чтобы они могли изменяться независимо.

Когда использовать: Когда нужно разделить монолитный класс на несколько отдельных иерархий.

Пример кода:

interface Device {
  var volume: Int
  var isEnabled: Boolean

  fun enable()
  fun disable()
}

class Radio : Device {
  override var volume: Int = 30
  override var isEnabled: Boolean = false

  override fun enable() {
    isEnabled = true
    println("Радио включено")
  }

  override fun disable() {
    isEnabled = false
    println("Радио выключено")
  }
}

class TV : Device {
  override var volume: Int = 50
  override var isEnabled: Boolean = false

  override fun enable() {
    isEnabled = true
    println("Телевизор включен")
  }

  override fun disable() {
    isEnabled = false
    println("Телевизор выключен")
  }
}

abstract class Remote(val device: Device) {
  fun togglePower() {
    if (device.isEnabled) {
      device.disable()
    } else {
      device.enable()
    }
  }

  fun volumeUp() {
    device.volume += 10
    println("Громкость увеличена до ${device.volume}")
  }

  fun volumeDown() {
    device.volume -= 10
    println("Громкость уменьшена до ${device.volume}")
  }
}

class AdvancedRemote(device: Device) : Remote(device) {
  fun mute() {
    device.volume = 0
    println("Звук выключен")
  }
}

fun main() {
  val tv = TV()
  val remote = AdvancedRemote(tv)

  remote.togglePower()
  remote.volumeUp()
  remote.mute()
}

8. Composite (Компоновщик)

Описание: Позволяет создавать древовидные структуры объектов и работать с ними как с единичными объектами.

Когда использовать: Когда нужно представить иерархию объектов и работать с ними единообразно.

Пример кода:

interface Graphic {
  fun draw()
}

class Dot(val x: Int, val y: Int) : Graphic {
  override fun draw() {
    println("Рисуем точку на координатах ($x, $y)")
  }
}

class Circle(x: Int, y: Int, val radius: Int) : Dot(x, y) {
  override fun draw() {
    println("Рисуем круг с центром ($x, $y) и радиусом $radius")
  }
}

class CompoundGraphic : Graphic {
  private val children = mutableListOf<Graphic>()

  fun add(child: Graphic) = children.add(child)
  fun remove(child: Graphic) = children.remove(child)

  override fun draw() {
    println("Рисуем составной график")
    children.forEach { it.draw() }
  }
}

fun main() {
  val dot = Dot(1, 2)
  val circle = Circle(5, 3, 10)

  val compoundGraphic = CompoundGraphic()
  compoundGraphic.add(dot)
  compoundGraphic.add(circle)

  compoundGraphic.draw()
}

9. Decorator (Декоратор)

Описание: Динамически добавляет объектам новые обязанности.

Когда использовать: Когда нужно добавить обязанности объекту, не затрагивая другие объекты.

Пример кода:

interface DataSource {
  fun writeData(data: String)
  fun readData(): String
}

class FileDataSource(private val filename: String) : DataSource {
  private var data: String = ""

  override fun writeData(data: String) {
    this.data = data
    println("Данные записаны в файл $filename")
  }

  override fun readData(): String {
    println("Чтение данных из файла $filename")
    return data
  }
}

open class DataSourceDecorator(private val wrappee: DataSource) : DataSource {
  override fun writeData(data: String) {
    wrappee.writeData(data)
  }
  override fun readData(): String = wrappee.readData()
}

class EncryptionDecorator(wrappee: DataSource) : DataSourceDecorator(wrappee) {
  override fun writeData(data: String) {
    val encryptedData = "encrypted($data)"
    super.writeData(encryptedData)
  }

  override fun readData(): String {
    val data = super.readData()
    return "decrypted($data)"
  }
}

fun main() {
  val source = FileDataSource("somefile.dat")
  val encryptedSource = EncryptionDecorator(source)

  encryptedSource.writeData("важные данные")
  println(encryptedSource.readData())
}

10. Facade (Фасад)

Описание: Предоставляет унифицированный интерфейс к набору интерфейсов в системе.

Когда использовать: Когда нужно упростить взаимодействие с комплексной системой.

Пример кода:

class CPU {
  fun jump(position: Long) = println("CPU переходит к $position")
  fun execute() = println("CPU выполняет инструкции")
}

class Memory {
  fun load(position: Long, data: String) = 
      println("Память загружает данные '$data' на позицию $position")
}

class HardDrive {
  fun read(lba: Long, size: Int): String {
    println("Жесткий диск читает $size байт с позиции $lba")
    return "данные"
  }
}

class ComputerFacade {
  private val cpu = CPU()
  private val memory = Memory()
  private val hardDrive = HardDrive()

  fun start() {
    val bootData = hardDrive.read(0, 1024)
    memory.load(0, bootData)
    cpu.jump(0)
    cpu.execute()
  }
}

fun main() {
  val computer = ComputerFacade()
  computer.start()
}

11. Flyweight (Приспособленец)

Описание: Позволяет вместить большее количество объектов, используя разделение общего состояния между ними.

Когда использовать: Когда приложение должно эффективно подддерживать множество мелких объектов.

Пример кода:

data class TreeType(val name: String, val color: string, val texture: String)

class Tree(val x: Int, val y: Int, val type: TreeType) {
  fun draw() = println("Рисуем дерево '${type.name}' на позиции ($x, $y)")
}

object TreeFactory {
  private val treeTypes = mutableMapOf<String, TreeType>()

  fun getTreeType(name: String, color: String, texture: String): TreeType {
    val key = "$name-$color-$texture"
    return treeTypes.getOrPut(key) { TreeType(name, color, texture) }
  }
}

class Forest {
  private val trees = mutableListOf<Tree>()

  fun plantTree(x: Int, y: Int, name: String, color: String, texture: String) {
    val type = TreeFactory.getTreeType(name, color, texture)
    val tree = Tree(x, y, type)
    trees.add(tree)
  }

  fun draw() = trees.forEach { it.draw() }
}

fun main() {
  val forest = Forest()
  forest.plantTree(1, 2, "Дуб", "Зеленый", "Грубая")
  forest.plantTree(3, 4, "Дуб", "Зеленый", "Грубая")
  forest.plantTree(5, 6, "Береза", "Белый", "Гладкая")

  forest.draw()
}

12. Proxy (Заместитель)

Описание: Предоставляет суррогатный объект, контролирующий доступ к другому объекту.

Когда использовать: Когда нужен более функциональный или изолированный доступ к объекту.

Пример кода:

interface Image {
  fun display()
}

class RealImage(private val filename: String) : Image {
  init {
    loadFromDisk()
  }

  private fun loadFromDisk() = println("Загрузка $filename с диска")

  override fun display() = println("Отображение $filename")
}

class ProxyImage(private val filename: String) : Image {
  private var realImage: RealImage? = null

  override fun display() {
    if (realImage == null) {
      realImage = RealImage(filename)
    }
    realImage?.display()
  }
}

fun main() {
  val image = ProxyImage("test_image.jpg")
  image.display() // загрузка произойдет здесь
  image.display() // загрузка не произойдет
}

В следующей части статьи рассмотрим поведенческие паттерны.

Комментарии (1)


  1. e_Hector
    20.11.2024 18:40

    В Kotlin нет места билдерам.

    Идиоматично, использовать в таком случае комбинацию дефолтных и именованных параметров

    class House(
        val walls: Int = 0,
        val doors: Int = 0,
        val windows: Int = 0,
        val hasGarage: Boolean = false,
        val hasSwimmingPool: Boolean = false
    )
    
    fun main() {
        val house = House(
            walls = 4,
            doors = 2,
            windows = 6,
            hasGarage = true,
        )
    
      println("Дом с ${house.walls} стенами, ${house.doors} дверями, " + 
              "${house.windows} окнами, гараж: ${house.hasGarage}") 
    }