Паттерны проектирования

Шаблон проектирования обычно рассматривается как многоразовое решение часто встречающейся проблемы проектирования в объектно-ориентированном программном обеспечении.

Проектное пространство

Пространство шаблонов проектирования, представленное в таблице ниже, имеет два измерения: цель (purpose) и область применения (scope). Шаблоны целей могут быть классифицированы как creational, structural или behavioral. Порождающие шаблоны (creational) имеют дело с созданием объектов, в то время как структурные шаблоны (structural) имеют дело с составом классов или объектов. Поведенческие шаблоны (behavioral) описывают взаимодействие объектов и часто распределение ответственности между объектами.

Измерение области видимости определяет, "применяется ли шаблон в первую очередь к классам или к объектам".

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

Объектные шаблоны, с другой стороны, имеют дело с отношениями между объектами, такими как композиция и делегирование. Это означает, что связь может изменяться во время выполнения. В шаблонах объектов комбинированное поведение шаблона распределяется между несколькими объектами во время выполнения.

Scope \ Purpose Creational Structural Behavioral
Class Factory Method Adapter(class) Interpreter, Template Method
Object Abstract Factory, Builder, Prototype, Singleton Adapter(object), Bridge, Composite, Decorator, Facade, Flyweight, Proxy Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Visitor

Порождающие шаблоны

Порождающие шаблоны (creational patterns) имеют дело с созданием объектов.

Фабричный метод

Назначение

Определение интерфейса для создания объекта, в котором подклассы решают, какой класс создавать. Фабричный метод позволяет классу отложить создание экземпляра для подклассов. Фабричные методы обычно используются, когда класс не может предвидеть класс объектов, которые он должен создать.

Диаграмма

Factory Method

Пример

trait Document:
  def open(): Unit
  def close(): Unit

trait Application:
  type D <: Document
  def createDocument: D

Использование паттерна фабричный метод:

class ElectronicDocument extends Document:
  def open(): Unit = println("Open an e-doc")
  def close(): Unit = println("Close an e-doc")

object ElectronicApplication extends Application:
  type D = ElectronicDocument
  def createDocument: D = new ElectronicDocument

ElectronicApplication.createDocument.open()
// Open an e-doc

Абстрактная фабрика

Назначение

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

Допустим, необходимо создать библиотеку графического интерфейса, которая должна работать на разных платформах, таких как Microsoft Windows или Mac OSX. Чтобы поддерживать собственный внешний вид на каждой платформе, например, для графического окна, сохраняя при этом переносимость клиентского кода между платформами, клиентам должен быть доступен только интерфейс окна. Обычно окно состоит из нескольких виджетов, некоторые из которых могут зависеть от конкретной платформы. Что именно и как создается конкретное окно, абстрагируется от клиентского кода абстрактной фабрикой. Это позволяет изменять код создания объектов без изменения клиентского кода. Поскольку клиент не знает о конкретных классах, в клиенте не вводятся зависимости от платформы. Казалось бы, можно изменить реализации окна и связанных с ним виджетов и предоставить новые, если они соответствуют абстрактному интерфейсу. Обычно фабрика создает семейство продуктов, в наших настройках графического интерфейса это могут быть окна, меню и т.д. Важной проблемой является то, что мы не должны смешивать классы, зависящие от платформы, так как это может привести к ошибкам во время выполнения. Конкретные фабрики чаще всего реализуются в виде singleton-ов.

Диаграмма

Абстрактная фабрика

Пример

trait WindowFactory определяет интерфейс для фабрики. Он содержит типы aWindow и aScrollbar. Эти типы сверху ограничены trait-ми и должны быть доработаны на конкретных фабриках. Код создания экземпляра, общий для конкретных фабрик, может быть повторно использован в подклассах, если они ссылаются только на определенные абстрактные типы. Абстрактные продукты - это оба trait-а, вложенных в фабричный признак, то есть Window и Scrollbar. Любой конкретный продукт должен расширяться их. Абстрактные фабричные методы createWindow и createScrollbar скрывают фактический код создания экземпляра от клиентов.

trait WindowFactory:
  type aWindow <: Window
  type aScrollbar <: Scrollbar

  def createWindow(s: aScrollbar): aWindow
  def createScrollbar(): aScrollbar

  trait Window(s: aScrollbar)
  trait Scrollbar

Ниже показано, как можно расширить нашу абстрактную фабрику с помощью конкретной фабрики. Конкретная фабрика в данном примере - это объект singleton, содержащий protected nested классы. Это конкретная реализация. Поскольку они protected, они скрыты от клиентов.

object VistaFactory extends WindowFactory:
  type aWindow = VistaWindow
  type aScrollbar = VistaScrollbar

  def createWindow(s: aScrollbar) = VistaWindow(s)
  def createScrollbar() = new VistaScrollbar

  val scrollbar: aScrollbar = new VistaScrollbar
  val window: aWindow = VistaWindow(scrollbar)

  protected class VistaWindow(s: aScrollbar) extends Window(s)
  protected class VistaScrollbar extends Scrollbar

По соображениям технического обслуживания мы можем быть не заинтересованы в том, чтобы фактический исходный код, реализующий классы продуктов, находился внутри фабрики. Используя явный тип self, можно выразить зависимость, существующую между модулем, предоставляющим классы продуктов, и любой WindowFactory. Это позволяет расширить Window и Scrollbar в модуле, где в противном случае они не были бы видны в области видимости.

trait VistaWidgets:
  self: WindowFactory =>
  protected class VistaWindow(s: aScrollbar) extends Window(s)
  protected class VistaScrollbar extends Scrollbar

Вложенность классов полезна для инкапсуляции определенных типов. Фабрика - это, по сути, просто признак пространства имен или репозиторий для конкретных продуктов. Абстрактные типы позволяют выразить взаимозависимость продуктов. Это дает статические гарантии того, что продукты с разных фабрик не могут быть смешаны. Кроме того, они расширяют возможности повторного использования в подклассах фабрики.

Строитель

Назначение

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

Более детализированное рассмотрение шаблона

Диаграмма

Builder

Пример

final case class ConnectionConfig private (
    host: String,
    port: Int,
    timeout: Int,
    connectionRetry: Int,
    user: String,
    password: String
)

object ConnectionConfig:
  def builder(): ConnectionConfigBuilder = ConnectionConfigBuilder()

  case class ConnectionConfigBuilder private (
      private val host: String,
      private val port: Int,
      private val timeout: Int,
      private val connectionRetry: Int,
      private val user: String,
      private val password: String
  ):
    def withHost(host: String): ConnectionConfigBuilder =
      copy(host = host)

    def withPort(port: Int): ConnectionConfigBuilder =
      copy(port = port)

    def withTimeout(timeout: Int): ConnectionConfigBuilder =
      copy(timeout = timeout)

    def withConnectionRetry(
        connectionRetry: Int
    ): ConnectionConfigBuilder =
      copy(connectionRetry = connectionRetry)

    def withUser(user: String): ConnectionConfigBuilder =
      copy(user = user)

    def withPassword(password: String): ConnectionConfigBuilder =
      copy(password = password)

    def build(): ConnectionConfig =
      new ConnectionConfig(
        host = host,
        port = port,
        timeout = timeout,
        connectionRetry = connectionRetry,
        user = user,
        password = password
      )
  end ConnectionConfigBuilder

  private object ConnectionConfigBuilder:
    def apply(): ConnectionConfigBuilder =
      new ConnectionConfigBuilder(
        host = "localhost",
        port = 8080,
        timeout = 10000,
        connectionRetry = 3,
        user = "root",
        password = "root"
      )
  end ConnectionConfigBuilder
end ConnectionConfig

Использование паттерна строитель:

ConnectionConfig
  .builder()
  .withHost("localhost")
  .withPort(9090)
  .withTimeout(1000)
  .withConnectionRetry(1)
  .withUser("user")
  .withPassword("12345")
  .build()
// res3: ConnectionConfig = ConnectionConfig(
//   host = "localhost",
//   port = 9090,
//   timeout = 1000,
//   connectionRetry = 1,
//   user = "user",
//   password = "12345"
// )  

Прототип

Назначение

Задаёт виды создаваемых объектов с помощью экземпляра-прототипа и создаёт новые объекты путём копирования этого прототипа. Паттерн позволяет уйти от реализации и позволяет следовать принципу "программирование через интерфейсы". В качестве возвращающего типа указывается интерфейс/абстрактный класс на вершине иерархии, а классы-наследники могут подставить туда наследника, реализующего этот тип.

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

Использование

Паттерн используется чтобы:

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

Диаграмма

Prototype

Пример

class A(var state: Int)
extension (a: A) def copy = new A(a.state)

Использование паттерна фабричный метод:

def a = new A(2)
println(a.state)
// 2
def aCopy = a.copy
println(a.state)
// 2

Одиночка

Назначение

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

Диаграмма

Singleton

Пример

Scala позволяет создавать одноэлементные объекты, используя ключевое слово object. По сути, одноэлементный объект создается автоматически при первом использовании, и всегда существует только один экземпляр.

object Singleton
val s = Singleton

В следующей одноэлементной реализации используется сопутствующий объект. Эта реализация актуальна только в том случае, если нам нужно иметь возможность усовершенствовать синглтон. В противном случае предпочтительнее краткое решение с использованием одноэлементного объекта (object).

class Singleton private () // private constructor

object Singleton:
  private val instance: Singleton = new Singleton
  def getInstance() = instance

val s = Singleton.getInstance()

Структурные шаблоны

Структурные шаблоны (structural patterns) имеют дело с составом классов или объектов.

Адаптер

Назначение

Преобразовать интерфейс класса в другой интерфейс, ожидаемый клиентами. Адаптер позволяет классам работать вместе, что иначе было бы невозможно из-за несовместимых интерфейсов.

Диаграмма

Adapter

Пример

Решение Scala сочетает в себе большинство преимуществ адаптера класса и адаптера объекта в одном решении.

trait Target:
  def f(): Unit

class Adaptee:
  def g(): Unit = println("g")

trait Adapter:
  self: Target with Adaptee =>
  def f(): Unit = g()

val adapter = new Adaptee with Adapter with Target
adapter.f()
// g
adapter.g()
// g

Мост

Назначение

Отделение абстракции от ее реализации, чтобы они могли различаться. Используя шаблон bridge, можно избежать постоянной привязки между абстракцией и ее реализацией. Шаблон моста является подходящим дизайном, когда у вас есть множество классов, обычно из иерархии классов, определяющих некоторые центральные абстракции, уточненные путем наследования, для каждого из которых требуются разные реализации.

Диаграмма

Bridge

Пример

Сам мост реализуется с помощью композиции и делегирования. Абстракция содержит ссылку на средство реализации. Точный тип реализации скрыт абстрактным классом, который в сочетании с композицией обеспечивает разделение. Здесь действует второй принцип GOF. Вместо включения рефакторинга иерархии классов с композицией и делегированием в Scala можно использовать явные типы self.

Корень иерархии абстракций с именем Window имеет тип self, который ссылается на корень иерархии реализации, WindowImp.

// common interface for all implementors
trait WindowImp:
  def drawLine(x: Int, y: Int): Unit

trait Window:
  self: WindowImp =>
  def drawRect(x1: Int, x2: Int, x3: Int, x4: Int): Unit =
    drawLine(x1, x2)
    drawLine(x1, x3)
    drawLine(x2, x4)
    drawLine(x3, x4)

// abstractions
trait TransientWindow:
  self: Window with WindowImp =>
  def drawCloseBox(): Unit = drawRect(4, 3, 2, 1)

trait IconWindow:
  self: Window with WindowImp =>
  def drawBorder(): Unit = drawRect(1, 2, 3, 4)
// implementors
trait WindowOSX extends WindowImp:
  def drawLine(x: Int, y: Int): Unit = println("drawing line in OSX")

trait WindowVista extends WindowImp:
  def drawLine(x: Int, y: Int): Unit = println("drawing line in Vista")
val windowOSX: Window = new Window with WindowOSX
windowOSX.drawRect(1, 2, 3, 4)
// drawing line in OSX
// drawing line in OSX
// drawing line in OSX
// drawing line in OSX

Компоновщик

Назначение

Комбинировать объекты в древовидные структуры для представления иерархий часть-целое. Composite позволяет клиентам единообразно обрабатывать отдельные объекты и композиции объектов.

Диаграмма

Composite

Пример

В листинге ниже абстрактный класс Component представляет интерфейс для общих операций, которые мы хотим выполнять с листьями или композитами, в данном случае только с одним методом с именем display. case classText и Picture — это два листовых объекта, оба они реализуют display. case class Composite содержит любое количество дочерних компонентов, то есть листьев или других составных частей. В зависимости от того, насколько тяжелыми являются наши листовые объекты, или выполняют ли они другие роли в нашем дизайне или нет, мы можем сделать дочерние переменные как неизменяемыми, так и изменяемыми.

trait Component:
  def display(): Unit

case class Text(text: String) extends Component:
  def display(): Unit = println(text)

case class Picture(picture: String) extends Component:
  def display(): Unit = println(picture)

case class Composite(children: List[Component]) extends Component:
  def display(): Unit = children.foreach(_.display())
val tree = Composite(List(Composite(List(Text("text1"), Picture("picture1"))), Text("text2")))
// tree: Composite = Composite(
//   children = List(
//     Composite(
//       children = List(Text(text = "text1"), Picture(picture = "picture1"))
//     ),
//     Text(text = "text2")
//   )
// )
tree.display()
// text1
// picture1
// text2
tree.children(1).display()
// text2

Ниже показан пример метода, который обходит составную структуру и изменяет все узлы Text на месте.

def changeAllText(c: Component, s: String): Component =
  c match
    case _: Text    => Text(s)
    case p: Picture => p
    case Composite(children) =>
      val newChildren = children.map(changeAllText(_, s))
      Composite(newChildren)
changeAllText(tree, "text3").display()
// text3
// picture1
// text3

Декоратор

Назначение

Динамически прикреплять дополнительные обязанности к объекту. Декораторы предоставляют гибкую альтернативу подклассам для расширения функциональности.

Диаграмма

Decorator

Пример

trait Component:
  def draw(): Unit

class TextView(var s: String) extends Component:
  def draw(): Unit = println(s"Drawing..$s")

trait EncapsulateTextView(c: TextView) extends Component:
  def draw(): Unit = c.draw()

trait BorderDecorator extends Component:
  abstract override def draw(): Unit =
    super.draw()
    drawBorder()
  def drawBorder(): Unit =
    println("Drawing border")

trait ScrollDecorator extends Component:
  abstract override def draw(): Unit =
    scrollTo()
    super.draw()
  def scrollTo(): Unit = println("Scrolling..")
val tw = new TextView("foo")
val etw1 = new EncapsulateTextView(tw) with BorderDecorator with ScrollDecorator
// Scrolling..
// Drawing..foo
// Drawing border
tw.s = "bar"
val etw2 = new EncapsulateTextView(tw) with ScrollDecorator with BorderDecorator
etw2.draw()
// Scrolling..
// Drawing..bar
// Drawing border

Фасад

Назначение

Предоставление унифицированного интерфейса набору интерфейсов в подсистеме. Facade определяет высокоуровневый интерфейс, упрощающий использование подсистемы

Диаграмма

Facade

Пример

trait Facade:
  type A <: SubSystemA
  type B <: SubSystemB
  protected val subA: A
  protected val subB: B
  def foo(): Unit = subB.foo(subA)
  protected class SubSystemA
  protected class SubSystemB:
    def foo(sub: SubSystemA): Unit = println("Calling foo")
end Facade

object FacadeA extends Facade:
  type A = SubSystemA
  type B = SubSystemB
  val subA: A = new SubSystemA
  val subB: B = new SubSystemB
end FacadeA

FacadeA.foo()
// Calling foo

Приспособленец

Назначение

Использование совместного использования для эффективной поддержки большого количества мелких объектов. Оптимизация работы с памятью путём предотвращения создания экземпляров элементов, имеющих общую сущность.

Диаграмма

Flyweight

Пример

Flyweight используется для уменьшения затрат при работе с большим количеством мелких объектов. При проектировании Flyweight необходимо разделить его свойства на внешние и внутренние. Внутренние свойства всегда неизменны, тогда как внешние могут отличаться в зависимости от места и контекста применения и должны быть вынесены за пределы приспособленца.

Flyweight дополняет шаблон Factory Method таким образом, что при обращении клиента к Factory Method для создания нового объекта ищет уже созданный объект с такими же параметрами, что и у требуемого, и возвращает его клиенту. Если такого объекта нет, то фабрика создаст новый.

trait FlyWeightFactory[T1, T2] extends Function[T1, T2]:
  import scala.collection.mutable
  private val pool = mutable.Map.empty[T1, T2]

  def createFlyWeight(intrinsic: T1): T2

  def apply(index: T1): T2 =
    pool.getOrElseUpdate(index, createFlyWeight(index))

  def update(index: T1, elem: T2): Unit =
    pool(index) = elem

end FlyWeightFactory
class Character(char: Char):
  import scala.util.Random
  private lazy val state = Random.nextInt()
  def draw(): Unit =
    println(s"drawing character - $char, state - $state")

object CharacterFactory extends FlyWeightFactory[Char, Character]:
  def createFlyWeight(c: Char) = new Character(c)
val f1 = CharacterFactory('a')
val f2 = CharacterFactory('b')
val f3 = CharacterFactory('a')
val f4 = new Character('a')
f1.draw()
// drawing character - a, state - 1796598386
f2.draw()
// drawing character - b, state - -1864173438
f3.draw()
// drawing character - a, state - 1796598386
f4.draw()
// drawing character - a, state - 1632455878

Обратите внимание, что f1 и f3 указывают на один и тот же общий объект-приспособленец.

Заместитель

Назначение

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

"Заместитель" хранит ссылку, которая позволяет заместителю обратиться к реальному субъекту (объект класса "Заместитель" может обращаться к объекту класса "Субъект", если интерфейсы "Реального Субъекта" и "Субъекта" одинаковы). Поскольку интерфейс "Реального Субъекта" идентичен интерфейсу "Субъекта", так, что "Заместителя" можно подставить вместо "Реального Субъекта", он контролирует доступ к "Реальному Субъекту", а также может отвечать за создание или удаление "Реального Субъекта". "Субъект" определяет общий для "Реального Субъекта" и "Заместителя" интерфейс так, что "Заместитель" может быть использован везде, где ожидается "Реальный Субъект". При необходимости запросы могут быть переадресованы "Заместителем" "Реальному Субъекту".

Виды

Диаграмма

Proxy

// "Subject"
trait IMath:
  def add(x: Double, y: Double): Double

// "Real Subject"
object Math extends IMath:
  def add(x: Double, y: Double) = x + y

// "Proxy Object"
class MathProxy extends IMath:
  private lazy val math = Math

  def add(x: Double, y: Double) = math.add(x, y)
val p: IMath = new MathProxy
p.add(4, 2)
// res20: Double = 6.0

Примеры

Протоколирующий прокси
Удалённый заместитель
Виртуальный заместитель

Отложенное вычисление Scala обеспечивает прямую языковую поддержку некоторых аспектов шаблона Proxy.

class VirtualProxy:
  lazy val expensiveOperation = List (1 to 1000000000)
Копировать-при-записи
Защищающий заместитель
Кэширующий прокси
Экранирующий прокси
Синхронизирующий прокси
"Умная" ссылка

Поведенческие шаблоны

Поведенческие шаблоны (behavioral patterns) описывают взаимодействие объектов и часто распределение ответственности между объектами.

Интерпретатор

Назначение

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

Диаграмма

Interpreter

Пример

class Context:
  import scala.collection.mutable
  val result: mutable.Stack[String] = mutable.Stack.empty[String]

trait Expression:
  def interpret(context: Context): Unit

trait OperatorExpression extends Expression:
  val left: Expression
  val right: Expression

  def interpret(context: Context): Unit =
    left.interpret(context)
    val leftValue = context.result.pop()

    right.interpret(context)
    val rightValue = context.result.pop()

    doInterpret(context, leftValue, rightValue)

  protected def doInterpret(context: Context, leftValue: String, rightValue: String): Unit

end OperatorExpression
trait EqualsExpression extends OperatorExpression:
  protected override def doInterpret(context: Context, leftValue: String, rightValue: String): Unit =
    context.result.push(if leftValue == rightValue then "true" else "false")

trait OrExpression extends OperatorExpression:
  protected override def doInterpret(context: Context, leftValue: String, rightValue: String): Unit =
    context.result.push(if leftValue == "true" || rightValue == "true" then "true" else "false")

trait MyExpression extends Expression:
  var value: String

  def interpret(context: Context): Unit =
    context.result.push(value)
val context = Context()
val input = new MyExpression() { var value = "" }

var expression = new OrExpression {
  val left: Expression = new EqualsExpression {
    val left = input
    val right = new MyExpression { var value = "4" }
  }
  val right: Expression = new EqualsExpression {
    val left = input
    val right = new MyExpression { var value = "четыре" }
  }
}

input.value = "четыре"
expression.interpret(context)
context.result.pop()
// res2: String = "true"

input.value = "44"
expression.interpret(context)
context.result.pop()
// res5: String = "false"

Шаблонный метод

Назначение

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

Диаграмма

Template Method

Пример

trait Template extends (Unit => Int):
  def subStepA(): Unit
  def subStepB: Int
  def apply: Int =
    subStepA()
    subStepB

Цепочка обязанностей

Назначение

Поведенческий шаблон проектирования, предназначенный для организации в системе уровней ответственности. Избегайте связывания отправителя запроса с его получателем, предоставляя более чем одному объекту возможность обработать запрос. Цепляйте принимающие объекты и передайте запрос по цепочке, пока объект не обработает его.

Идея шаблона состоит в том, чтобы отделить отправителей и получателей сообщения. Шаблон позволяет определить точного получателя сообщения во время выполнения.

Диаграмма

Chain of Responsibility

Пример

trait Handler[T]:
  var successor: Option[Handler[T]] = None

  def handleRequest(r: T): Unit =
    if handlingCriteria(r) then doThis(r)
    else successor.foreach(_.handleRequest(r))

  def doThis(v: T): Unit = ()
  def handlingCriteria(request: T): Boolean = false

end Handler
class Sensor extends Handler[Int]:
  var value = 0
  def changeValue(v: Int): Unit =
    value = v
    handleRequest(value)

class Display1 extends Handler[Int]:
  def show(v: Int): Unit = println(s"Display1 prints $v")
  override def doThis(v: Int): Unit = show(v)
  override def handlingCriteria(v: Int): Boolean = v < 4

Другое решение, специфичный для шаблона код хранится в отдельном trait.

class Display2:
  def show(v: Int): Unit = println(s"Display2 prints $v")

trait Display2Handler extends Display2 with Handler[Int]:
  override def doThis(v: Int): Unit = show(v)
  override def handlingCriteria(v: Int): Boolean = v >= 4
val sensor = new Sensor
val display1 = new Display1
val display2 = new Display2 with Display2Handler
sensor.successor = Some(display1)
display1.successor = Some(display2)
sensor.changeValue(2)
// Display1 prints 2
sensor.changeValue(4)
// Display2 prints 4

Команда

Назначение

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

Диаграмма

Command

Пример

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

class Button(val click: () => Unit)
val button = new Button(() => println("click!"))
button.click()
// click!

Итератор

Назначение

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

Диаграмма

Iterator

Пример

trait Iterator[A]:
  def first: A

  def next: A

  def isDone: Boolean

  def currentItem: A

Посредник

Назначение

Определение объекта, который инкапсулирует способ взаимодействия набора объектов. Медиатор способствует слабой связи, не позволяя объектам явно ссылаться друг на друга, и позволяет независимо изменять их взаимодействие.

Диаграмма

Mediator

Пример

Классы ListBox и EntryField — наши классы-коллеги, оба — Widget-ы. DialogDirector содержит вложенный трейт ListBoxDir, который перехватывает каждый раз, когда щелкают наш список. При нажатии на него вызывается listBoxChanged, что приводит к установке текста в нашем поле ввода с текущим выбором списка. Это простой пример взаимодействия объектов. Обратите внимание, что коллеги совершенно не знают о посреднике и, следовательно, о самой схеме.

// Widgets
class ListBox:
  def getSelection: String = "selected"
  def click(): Unit = ()

class EntryField:
  def setText(s: String): Unit = println(s)

class DialogDirector:
  protected trait ListBoxDir extends ListBox:
    abstract override def click(): Unit =
      super.click()
      listBoxChanged()

  // Colleagues
  val listBox: ListBox = new ListBox with ListBoxDir
  val entryField: EntryField = new EntryField

  // Directing methods
  def showDialog(): Unit = ()

  // called when listBox is clicked via advice
  def listBoxChanged(): Unit = entryField.setText(listBox.getSelection)

end DialogDirector
val dialog = new DialogDirector
val listBox = dialog.listBox
val entryField = dialog.entryField
listBox.click()
// selected

Хранитель

Назначение

Не нарушая инкапсуляцию, захватить и вывести наружу внутреннее состояние объекта, чтобы объект можно было позже восстановить в это состояние.

Диаграмма

Memento

Пример

trait Originator:
  def createMemento: Memento
  def setMemento(m: Memento): Unit

trait Memento:
  def getState: Originator
  def setState(originator: Originator): Unit

Наблюдатель

Назначение

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

Шаблон также известен как публикация/подписка, что указывает на структуру шаблона: объект играет роль издателя, любое количество объектов может подписаться на издателя, тем самым получая уведомление всякий раз, когда в издателе происходит определенное событие. Обычно издатель передает свой экземпляр в уведомлении. Это позволяет подписчику запрашивать у издателя любую соответствующую информацию, например, чтобы иметь возможность синхронизировать состояние.

Диаграмма

Observer

Пример

trait Subject[T]:
  self: T =>

  import scala.collection.mutable
  private val observers: mutable.ListBuffer[T => Unit] =
    mutable.ListBuffer.empty[T => Unit]

  def subscribe(obs: T => Unit): Unit =
    observers.addOne(obs)

  def unsubscribe(obs: T => Unit): Unit =
    observers.subtractOne(obs)

  protected def publish(): Unit = observers.foreach(obs => obs(self))
trait Sensor(val label: String):
  var value: Double = _
  def changeValue(v: Double): Unit = value = v

// Pattern specific code
trait SensorSubject extends Sensor with Subject[Sensor]:
  override def changeValue(v: Double): Unit =
    super.changeValue(v)
  publish()

class Display(label: String):
  def notify(s: Sensor): Unit =
    println(s"$label ${s.label} ${s.value}")
val s1: SensorSubject = new Sensor("s1") with SensorSubject
val d1: Display = new Display("d1")
val d2: Display = new Display("d2")
s1.subscribe(d1.notify)
s1.subscribe(d2.notify)
s1.changeValue(10)

Состояние

Назначение

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

Диаграмма

State

Пример

class Context:
  private var currentState: State = State1

  def operation(): Unit = currentState.operation()

  trait State:
    def operation(): Unit

  private object State1 extends State:
    def operation(): Unit =
      println("State1")
  
  currentState = State2

  private object State2 extends State:
    def operation(): Unit =
      println("State2")
  currentState = State1

end Context
val c = new Context
c.operation()
// State1
c.operation()
// State1
c.operation()
// State1

Стратегия

Назначение

Определение семейства алгоритмов, инкапсулирование каждого из них и создание их взаимозаменяемыми. Стратегия позволяет алгоритму изменяться независимо от клиентов, которые его используют.

Диаграмма

Strategy

Пример

Strategy определяет интерфейс алгоритма. Context поддерживает ссылку на текущий объект Strategy и перенаправляет запросы от клиентов конкретному алгоритму.

object FileMatcher:
  private val filesHere: Seq[String] =
    Seq(
      "example.txt",
      "file.txt.png",
      "1txt"
    )

  // Strategy selection
  def filesContaining(query: String): Seq[String] =
    filesMatching(_.contains(query)) // inline strategy

  def filesRegex(query: String): Seq[String] =
    filesMatching(matchRegex(query)) // using a method

  def filesEnding(query: String): Seq[String] =
    filesMatching(new FilesEnding(query).matchEnding) // lifting a method

  // matcher is a strategy
  private def filesMatching(matcher: String => Boolean): Seq[String] =
    for
      file <- filesHere
      if matcher(file)
    yield file

  // Strategies
  private def matchRegex(query: String): String => Boolean =
    (s: String) => s.matches(query)

  private class FilesEnding(query: String):
    def matchEnding(s: String): Boolean = s.endsWith(query)
val query = ".txt"
// query: String = ".txt"
FileMatcher.filesContaining(query)
// res24: Seq[String] = List("example.txt", "file.txt.png")
FileMatcher.filesRegex(query)
// res25: Seq[String] = List("1txt")
FileMatcher.filesEnding(query)
// res26: Seq[String] = List("example.txt")

Посетитель

Назначение

Представление операции, которая должна быть выполнена над элементами структуры объекта. Visitor позволяет определить новую операцию без изменения классов элементов, над которыми она работает.

Диаграмма

Visitor

Пример

trait Expr
case class Num(n: Int) extends Expr
case class Sum(l: Expr, r: Expr) extends Expr

def prettyPrint(e: Expr): Unit =
  e match
    case Num(n) => print(n)
    case Sum(l, r) =>
      prettyPrint(l)
      print(" + ")
      prettyPrint(r)

def eval(e: Expr): Int =
  e match
    case Num(n)    => n
    case Sum(l, r) => eval(l) + eval(r)
val e1 = Sum(Sum(Num(1), Num(2)), Num(3))
// e1: Sum = Sum(l = Sum(l = Num(n = 1), r = Num(n = 2)), r = Num(n = 3))
prettyPrint(e1)
// 1 + 2 + 3
print(eval(e1))
// 6

Ссылки: