Skip to content

j-jeudesprit/Behavioral-patterns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 

Repository files navigation


Ссылки на конкретный паттерн:


Цепочка обязанностей — это поведенческий паттерн проектирования, который позволяет передавать запросы по цепочке обработчиков. Каждый узел цепи, сам решает, может ли он обработать или же передаст дальше.

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

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

Такая каша должна превратиться, после применения паттерна в человека-подобный вид:

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

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

class Request {
    // ...
}

protocol Handler {
    func setNext(_ next: Handler) -> Handler
    func handle(_ request: Request)
    // ...
}

class Application: Handler {
    private var next: Handler!

    func setNext(_ next: Handler) -> Handler {
        self.next = next
        return self.next
    }

    func handle(_ request: Request) {
        if ... {
            // Обрабатываем сами
            // ...
        } else {
            // Либо передаём дальше
            next.handle(request)
        }
    }

    // ...
}

class Widget: Handler {
    private var next: Handler!

    func setNext(_ next: Handler) -> Handler {
        self.next = next
        return self.next
    }

    func handle(_ request: Request) {
        if ... {
            // Обрабатываем сами
            // ...
        } else {
            // Либо передаём дальше
            next.handle(request)
        }
    }

    // ...
}

class Dialog: Handler {
    private var next: Handler!

    func setNext(_ next: Handler) -> Handler {
        self.next = next
        return self.next
    }

    func handle(_ request: Request) {
        if ... {
            // Обрабатываем сами
            // ...
        } else {
            // Либо передаём дальше
            next.handle(request)
        }
    }

    // ...
}

class Button: Handler {
    private var next: Handler!

    func setNext(_ next: Handler) -> Handler {
        self.next = next
        return self.next
    }

    func handle(_ request: Request) {
        if ... {
            // Обрабатываем сами
            // ...
        } else {
            // Либо передаём дальше
            next.handle(request)
        }
    }

    // ...
}

// Main

let app = Application()
let widget = Widget()
let button = Button()
button.setNext(widget)
let dialog = Dialog()
dialog.setNext(widget)

let someRequest = Request()
button.handle(someRequest)

Передавая таким образом некий запрос Button, произойдет проверка, а сможет ли он обработать этот запрос, если нет, то это попытается сделать Widget и так далее. Нам как клиенту, совершенно без разницы, кто из них ответит на наш запрос, главное получить на него ответ!


Команда — это поведенческий паттерн проектирования, который инкапсулирует запрос как объект, позволяя передавать их как аргументы при вызове методов, ставить запросы в очередь, вести логи и поддерживать отмену запроса.

Иногда необходимо посылать объектам запросы, ничего не зная о том, выполнение какой операции запрошено и кто является получателем. Например, в библиотеках для построения пользовательских интерфейсов встречаются такие объекты, как кнопки и меню, которые посылают запрос в ответ на действие пользователя. Но в саму библиотеку не заложена возможность обрабатывать этот запрос, так как только приложение, использующее её, располагает информацией о том, что следует сделать. Проектировщик библиотеки не владеет никакой информацией о получателе запроса и о том, какие операции тот должен выполнить. Паттерн Команда позволяет библиотечным объектам отправлять запросы неизвестным объектам приложения, преобразовав сам запрос в объект. Этот объект можно хранить и передавать, как и любой другой. В основе описываемого паттерна лежит интерфейс, в котором объявлены операции для выполнения. В простейшей своей форме этот интерфейс состоит из одной абстрактной операции execute. Конкретные классы реализующие интерфейс определяют пару «получатель действие», сохраняя получателя в переменной экземпляра, и реализуют операцию execute, так чтобы она посылала запрос. У получателя есть информация, необходимая для выполнения запроса.

// Получатели

class Application { }
class Document { }

// Команды

protocol Command {
    func execute()
}

class OpenCommand: Command {
    private var responce: Application

    init(responce: Application) {
        self.responce = responce
    }

    func execute() {
        // ...
    }

    // ...
}

class PasteCommand: Command {
    private var responce: Document

    init(responce: Document) {
        self.responce = responce
    }

    func execute() {
        // ...
    }

    // ...
}

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

class MultiCommand: Command {
    private var list = [Command]()

    func execute() {
        for command in list {
            command.execute()
        }
    }

    func add(_ command: Command) {
        // ...
    }

    func remove(_ command: Command) {
        // ...
    }

    // ...
}

Теперь в коде мы может легко передавать любую команду, при этом мы не знаем, что это за команда и кому она адресована, да нам собственно это и не важно.

class Client {
    var command: Command!

    func action1() {
        // ...
        if ... {
            command.execute()
        }
    }

    func action2() {
        // ...
        if ... {
            command.execute()
        }
    }

    // ...
}

// Main

let client = Client()

client.command = PasteCommand(responce: Document())
client.action1()
client.action2()

client.command = OpenCommand(responce: Application())
client.action1()
client.action2()

client.command = MultiCommand()
let multi = client.command! as! MultiCommand
multi.add(OpenCommand(responce: Application()))
multi.add(PasteCommand(responce: Document()))
client.action1()
client.action2()

Интерпретатор — это поведенческий паттерн проектирования, который определяет представление его грамматики, а также интерпретатор предложений этого языка.

Если некоторая задача возникает часто, то имеет смысл представить её конкретные проявления в виде предложений на простом языке. Затем можно будет создать интерпретатор, который решает задачу, анализируя предложения этого языка.

Например, поиск строк по образцу – весьма распространенная задача. Регулярные выражения – это стандартный язык для задания образцов поиска. Вместо того чтобы программировать специализированные алгоритмы для сопоставления строк с каждым образцом, не проще ли построить алгоритм поиска так, чтобы он мог интерпретировать регулярное выражение, описывающее множество строк образцов?

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

Предположим, что они описаны следующей грамматикой:

expression ::= literal | alternation | sequence | repetition | '(' expression ')'
alternation ::= expression '|' expression
sequence ::= expression '&' expression
repetition ::= expression '*'
literal ::= 'a' | 'b' | 'c' | ... { 'a' | 'b' | 'c' | ... }*  

где expression – это начальный символ, а literal – терминальный символ, определяющий простые слова.

Паттерн Интерпретатор использует класс для представления каждого правила грамматики. Символы в правой части правила – это переменные экземпляров таких классов. Для представления приведенной выше грамматики требуется пять классов: интерфейс RegularExpression и четыре его подкласса LiteralExpression, AlternationExpression, SequenceExpression и RepetitionExpression. В последних трех подклассах определены переменные для хранения подвыражений, т.к. они представляют собой начальные, не терминальные символы.

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

представляет выражение raining & (dogs | cats) *

Отличный пример реализации на Swift


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

Составной объект, скажем список, должен предоставлять способ доступа к своим элементам, не раскрывая их внутреннюю структуру. Более того, иногда требуется обходить список по разному, в зависимости от решаемой задачи. Но вряд ли вы захотите засорять интерфейс класса List операциями для различных вариантов обхода, даже если все их можно предвидеть заранее. Кроме того, иногда нужно, что бы в один и тот же момент было определено несколько активных обходов списка.

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

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

Также вместо обычных методов в итераторах, лучше использовать операторные методы, по типу ++, -- и т.п.

Касаемо языка Swift, то для этих целей у языка из коробки существуют протоколы, реализуя которые, можно добиться той же функциональности(Sequence, Collection, IteratorProtocol, ...).


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

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

В качестве другого примера можно привести диспетчерскую в аэропорту. Самолётам при посадке или взлёте не нужно спрашивать у других самолётов, которые быть может в этот момент могут садиться или взлетать. Каждый самолёт спрашивает разрешение лишь у диспетчера. Диспетчер сам контролирует воздушное пространство близ аэропорта и не перекладывают задачу на самолёты.

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

Посредник решает эту проблему, путём введение интерфейса, который определяет обычно лишь один метод notify. Конкретные классы, (то есть посредников может быть несколько, мы не привязываемся жёстко к одному посреднику) реализуют этот интерфейс и определяют всю логику взаимодействия. Теперь каждому объекту достаточно хранить ссылку на конкретного посредника и в случаи надобности передавать оповещение посреднику. Где уже посредник, зная от кого он получил информацию, сообщит об этом нужным объектам.

protocol Mediator {
    func notify(_ sender: Component, with event: String)
    // ...
}

class AuthDialog: Mediator {
    func notify(_ sender: Component, with event: String) {
        // ...
    }

    // ...
}

protocol Component {
    var dialog: Mediator { get set }
    // ...
}

class Button: Component {
    var dialog: Mediator

    init(dialog: Mediator) {
        self.dialog = dialog
    }

    func click() {
        // ...
        dialog.notify(self, with: "Clicked")
        // ...
    }

    // ...
}

class TextBox: Component {
    var dialog: Mediator

    init(dialog: Mediator) {
        self.dialog = dialog
    }

    func input() {
        // ...
        dialog.notify(self, with: "Inputted")
        // ...
    }

    // ...
}

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

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

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

Одна из возможных реализаций на Swift


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

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

Представим, что наш товар это конкретный субъект и вообще говоря их может быть сколько угодно. Для этой цели мы определим интерфейс Subject, в котором будут методы подписки/отписки и оповещения. Так же определим интерфейс Observer с методом обновления, что бы любой мог стать наблюдателем. В первую очередь этот интерфейс нужен для того, что бы интерфейсу Subject не было никакой разницы, кто на него подписан. Реализую интерфейс Observer можно стать наблюдателем.

protocol Observer {
    var subject: Subject { get }
    func notify()
}

//###############################

protocol Subject {
    var observerCollection: [Observer] { get }

    func register(_ observer: Observer)
    func unregister(_ observer: Observer)
    func notifyObservers()
}

//###############################

class ConcreateSubject: Subject {
    var observerCollection = [Observer]()

    func register(_ observer: Observer) {
        observerCollection.append(observer)
    }

    func unregister(_ observer: Observer) {
        // ...
    }

    func notifyObservers() {
        for observer in observerCollection {
            observer.notify()
        }
    }

    // ...
}

//###############################

class ConcreateObserverA: Observer {
    var subject: Subject

    func notify() {
        // ...
    }

    // ...
}

class ConcreateObserverB: Observer {
    var subject: Subject

    func notify() {
        // ...
    }

    // ...
}

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


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

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

Представим, что наш товар это конкретный субъект и вообще говоря их может быть сколько угодно. Для этой цели мы определим интерфейс Subject, в котором будут методы подписки/отписки и оповещения. Так же определим интерфейс Observer с методом обновления, что бы любой мог стать наблюдателем. В первую очередь этот интерфейс нужен для того, что бы интерфейсу Subject не было никакой разницы, кто на него подписан. Реализую интерфейс Observer можно стать наблюдателем.

protocol Observer {
    var subject: Subject { get }
    func notify()
}

//###############################

protocol Subject {
    var observerCollection: [Observer] { get }

    func register(_ observer: Observer)
    func unregister(_ observer: Observer)
    func notifyObservers()
}

//###############################

class ConcreateSubject: Subject {
    var observerCollection = [Observer]()

    func register(_ observer: Observer) {
        observerCollection.append(observer)
    }

    func unregister(_ observer: Observer) {
        // ...
    }

    func notifyObservers() {
        for observer in observerCollection {
            observer.notify()
        }
    }

    // ...
}

//###############################

class ConcreateObserverA: Observer {
    var subject: Subject

    func notify() {
        // ...
    }

    // ...
}

class ConcreateObserverB: Observer {
    var subject: Subject

    func notify() {
        // ...
    }

    // ...
}

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


Состояние — это поведенческий паттерн проектирования, который позволяет варьировать своё поведение в зависимости от внутренного состояния.

Рассмотрим класс TCPConnection, с помощью которого представлено сетевое соединение. Объект этого класса может находиться в одном из нескольких состояний: Established (установлено), Listening (прослушивание), Closed (закрыто). Когда объект TCPConnection получает запросы от других объектов, то в зависимости от текущего состояния он отвечает по разному. Например, ответ на запрос Open (открыть) зависит от того, находится ли соединение в состоянии Closed или Established. Паттерн Состояние описывает, каким образом объект TCPConnection может вести себя по разному, находясь в различных состояниях.

Основная идея этого паттерна заключается в том, чтобы ввести абстрактный класс TCPState для представления различных состояний соединения. Этот класс объявляет интерфейс, общий для всех классов, описывающих различные рабочие состояния. В подклассах TCPState реализовано поведение, специфичное для конкретного состояния. Например, в классах TCPEstablished и TCPClosed реализовано поведение, характерное для состояний Established и Closed соответственно.

Класс TCPConnection хранит у себя объект состояния (экземпляр некоторого подкласса TCPState), представляющий текущее состояние соединения, и делегирует все зависящие от состояния запросы этому объекту. TCPConnection использует свой экземпляр подкласса TCPState для выполнения операций, свойственных только данному состоянию соединения. При каждом изменении состояния соединения TCPConnection изменяет свой объект состояние. Например, когда установленное соединение закрывается, TCPConnection заменяет экземпляр класса TCPEstablished экземпляром TCPClosed.

class TCPConnection {
    private var state: TCPState = TCPClosed()

    func open() {
        state.open()

        state = TCPEstablished()
        // ...
    }

    func close() {
        state.close()
        // ...
    }

    func acknowledge() {
        state.acknowledge()
        // ...
    }

    // ...
}

//###############################

protocol TCPState {
    func open()
    func close()
    func acknowledge()
}

class TCPEstablished: TCPState {
    func open() {
        print("TCPEstablished : \(#function)")
        // ...
    }

    func close() {
        print("TCPEstablished : \(#function)")
        // ...
    }

    func acknowledge() {
        print("TCPEstablished : \(#function)")
        // ...
    }
}

class TCPListen: TCPState {
    func open() {
        print("TCPListen : \(#function)")
        // ...
    }

    func close() {
        print("TCPListen : \(#function)")
        // ...
    }

    func acknowledge() {
        print("TCPListen : \(#function)")
        // ...
    }
}

class TCPClosed: TCPState {
    func open() {
        print("TCPClosed : \(#function)")
        // ...
    }

    func close() {
        print("TCPClosed : \(#function)")
        // ...
    }

    func acknowledge() {
        print("TCPClosed : \(#function)")
        // ...
    }
}


//###############################
// Main

let connection = TCPConnection()
connection.open() // TCPClosed : open()
connection.open() // TCPEstablished : open()

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

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

В качестве конкретного примера рассмотрим навигатор. Изначально вы пишите навигатор, который умеет прокладывать маршрут из точки A в точку B, для путешествий на машине. Затем спустя какое-то время, вы добавляете такую же возможность, но уже для общественного транспорта. Спустя ещё какое-то время, добавляете возможность проложить маршрут для любителей велопрогулки. В конце концов ваш класс будет содержать тонну алгоритмов, выполняющих одну и ту же конечную задачу - проложить маршрут.

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

protocol RouteStrategy {
    func buildRoute(from a: A, to b: B)
}

class RoadStrategy: RouteStrategy {
    func buildRoute(from a: A, to b: B) {
        // ...
    }
}

class PublicTransportStrategy: RouteStrategy {
    func buildRoute(from a: A, to b: B) {
        // ...
    }
}

// ...

Достаточно хранить лишь ссылку на конкретную стратегию, которую с лёгкостью можно заменить на другую стратегию, тем самым делая контекст независимым от конкретного алгоритма.

class Navigator {
    private var strategy: RouteStrategy

    init(strategy: RouteStrategy) {
        self.strategy = strategy
    }
    
    func buildRoute(from a: A, to b: B) {
        strategy.buildRoute(from: a, to: b)
    }

    // ...
}

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

Снова затронем тему алгоритмов. Допустим вы написали некоторый алгоритм, пусть это будем шифрование. Разбили его на шаги и создали класс. Пока всё отлично и он прекрасно работает. В скором времени вы решили добавить другой тип шифрования и заметили, что шаги у вас ровно такие, как и были раньше, отличается лишь их реализация. Но при этом вы повторили некоторые части предыдущего алгоритма. Добавили новый тип шифрования, где вы также видите те же самые шаги, некоторые из них вы повторили, а какие-то изменили. Более того, код в котором используются данные алгоритмы вынужден делать проверку, а какой собственно алгоритм я сейчас использую.

Паттерн Шаблонный метод решает эти проблемы, путём создание абстрактного класса, определяющего шаги нашего алгоритма и так называемого шаблонного метода, в котором вы запускаете все шаги алгоритма. Соответсвующие подклассы переопределяют нужные шаги алгоритма. При этом вызывающий код не должен заботиться о том, какой алгоритм он использует сейчас, он просто вызывает шаблонный метод, который запускает алгоритм.

class AbstractClass {
    final func temlateMethod() {
        // ...
        step1()
        // ...
        step2()
        // ...
        step3()
        // ...
    }

    func step1() {
        print("Hello from AbstractClass : \(#function)")
        // ...
    }

    func step2() {
        fatalError("not defaut implementation")
    }

    func step3() {
        print("Hello from AbstractClass : \(#function)")
        // ...
    }
}


class ConcreteClass1:  AbstractClass {
    override func step2() {
        print("Hello from ConcreteClass1 : \(#function)")
        // ...
    }
}

class ConcreteClass2:  AbstractClass {
    override func step2() {
        print("Hello from ConcreteClass1 : \(#function)")
        // ...
    }

    override func step3() {
        print("Hello from ConcreteClass1 : \(#function)")
        // ...
    }
}

Теперь мы с лёгкость можем переопределись нужные нам шаги алгоритма, при этом клиентский код не зависит от конкретного типа алгоритма.

// Main

var conreteClass: AbstractClass = ConcreteClass1()
conreteClass.temlateMethod()
/*   
Hello from AbstractClass : step1()
Hello from ConcreteClass1 : step2()
Hello from AbstractClass : step3()  
*/

conreteClass = ConcreteClass2()
conreteClass.temlateMethod()
/*  
Hello from AbstractClass : step1()
Hello from ConcreteClass2 : step2()
Hello from ConcreteClass2 : step3()
*/

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

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

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

protocol Visitor {
    func visitConcreteElement(_ elem: ConcreteElementA)
    func visitConcreteElement(_ elem: ConcreteElementB)
    func visitConcreteElement(_ elem: ConcreteElementC)
}

Для простоты мы добавляем лишь один метод visitConcreteElement, где ConcreteElementA, ConcreteElementB, ConcreteElementC, суть ссылки на объекты соответствующих классов, куда мы хотим добавить эту новую функцию.

Определим соответсвующие классы реализующие этот интерфейс.

class ConcreteVisitor1: Visitor {
    func visitConcreteElement(_ elem: ConcreteElementA) {
        // ...
    }

    func visitConcreteElement(_ elem: ConcreteElementB) {
        // ...
    }

    func visitConcreteElement(_ elem: ConcreteElementC) {
        // ...
    }
}

class ConcreteVisitor2: Visitor {
    func visitConcreteElement(_ elem: ConcreteElementA) {
        // ...
    }

    func visitConcreteElement(_ elem: ConcreteElementB) {
        // ...
    }

    func visitConcreteElement(_ elem: ConcreteElementC) {
        // ...
    }
}

Классов будет столько, сколько различных реализаций новой функции нам нужно.

Несмотря на то, что сами функции мы отделили от класса, всё же небольшое изменение нам нужно произвести. Но оно будет совсем незаметным.

protocol Element {
    func accept(_ with: Visitor)
}

class ConcreteElementA: Element {
    func accept(_ with: Visitor) {
        with.visitConcreteElement(self)
    }

    // ...
}

class ConcreteElementB: Element {
    func accept(_ with: Visitor) {
        with.visitConcreteElement(self)
    }

    // ...
}

class ConcreteElementC: Element {
    func accept(_ with: Visitor) {
        with.visitConcreteElement(self)
    }

    // ...
}

Мы лишь определили интерфейс, который содержит один единственный метод, метод вызова новой функциональности.

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

Releases

No releases published

Packages

No packages published