
Всем привет! Зачастую чтобы в чем то разобраться полезнее один раз увидеть конкретный пример чем несколько раз прочитать заумное описание.Решил написать ряд небольших статей для начинающих, в которых дать краткое описание основных паттернов проектирования и привести лаконичные примеры их использования. Данная статья, как можно догадаться из названия =), посвящена поведенческим паттернам.
Цепочка обязанностей / Chain of Responsibility
Цепочка обязанностей — это поведенческий паттерн проектирования, который позволяет передавать запросы последовательно по цепочке обработчиков. Каждый последующий обработчик решает, может ли он обработать запрос сам и стоит ли передавать запрос дальше по цепи.
Наиболее яркий пример из реальной разработки - это метод hitTest в iOS, который позволяет определить вью в иерархии, на которое было нажатие.
Приведем максимально упрощенный пример. Представим, что у нас есть простой интерфейс из скролл вью, картинки и кнопки. Мы хотим отображать подсказку об элементе в иерархии нашей вью при тапе на нем.
Давайте определим протокол обработчика события и базовый класс, реализующий данный протокол.
protocol RequestHandlerProtocol {
func handleRequest()
}
class RequestHandler: RequestHandlerProtocol {
var nextHandler: RequestHandlerProtocol?
var tooltipText: String?
func setNext(handler: RequestHandlerProtocol) {
self.nextHandler = handler
}
func handleRequest() {
if tooltipText != nil {
let objDesc = String(describing: self)
print("\(objDesc): \(tooltipText!)")
return
}
nextHandler?.handleRequest()
}
}
Как видим у базового класса есть свойство nextHandler для того, чтобы мы могли создать цепочку взаимосвязанных элементов. И свойство подсказки tooltipText.
Определим дочерние классы кнопки, картинки и скроллвью и создадим их экземпляры.
class TestButton: RequestHandler {
}
class TestImage: RequestHandler {
}
class TestScrollView: RequestHandler {
}
let button = TestButton()
let image = TestImage()
let scroll = TestScrollView()
В вызывающем коде мы можем связать классы между собой и задать каждому классу текст подсказки.
scroll.tooltipText = "This is scrollView"
button.setNext(handler: image)
image.setNext(handler: scroll)
button.handleRequest()
image.tooltipText = "This is image"
button.handleRequest()
button.tooltipText = "This is button"
button.handleRequest()
Как мы помним в методе handleRequest у нас есть проверка на то, что текст подсказки не пустой, иначе обработка запроса передается следующему элементу в иерархии.
Итератор / Iterator
Итератор — это поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления.
Разберемся на примере. Мы проектируем социальную сеть. Нам нужно дать пользователю возможность распечатать список всех своих друзей или коллег. Для этого нам нужно будет пробежаться по всем контактам юзера и выбрать либо всех друзей, либо коллег.
Но давайте предоставим возможность самой сети предоставлять нам соответствующий итератор.
Определим протоколы сети и итератора.
protocol SocialNetwork {
var users: [String] { get }
func createFriendsIterator() -> ProfileIterator
func createCoworkersIterator() -> ProfileIterator
}
protocol ProfileIterator {
func getNext()
func hasMore() -> Bool
}
Создадим класс сети.
class Ibook: SocialNetwork {
var users = ["Friend-1", "Coworker-1", "Coworker-2", "Friend-2", "Friend-3", "Coworker-3"];
func createFriendsIterator() -> ProfileIterator {
return FriendsIterator(network: self)
}
func createCoworkersIterator() -> ProfileIterator {
return CoworcersIterator(network: self)
}
}
Создадим класс итератора по друзьям.
class FriendsIterator: ProfileIterator {
let network: SocialNetwork
init(network: SocialNetwork) {
self.network = network
}
func getNext() {
for user in network.users {
if user.hasPrefix("Friend") {
print(user)
}
}
}
func hasMore() -> Bool {
var i = 0;
if i < network.users.count {
return true
}
i += 1;
return false
}
}
Как видим итератор принимает в инициализаторе сеть и затем проходиться по юзерам этой сети.
Создадим так же класс итератора по коллегам.
class CoworcersIterator: ProfileIterator {
let network: SocialNetwork
init(network: SocialNetwork) {
self.network = network
}
func getNext() {
for user in network.users {
if user.hasPrefix("Coworker") {
print(user)
}
}
}
func hasMore() -> Bool {
var i = 0;
if i < network.users.count {
return true
}
i += 1;
return false
}
}
В вызывающем коде мы можем вызвать нужный итератор и получить либо список друзей, либо список коллег.
let ibook = Ibook()
let friendsIterator = ibook.createFriendsIterator()
friendsIterator.getNext()
let coworkersIterator = ibook.createCoworkersIterator()
coworkersIterator.getNext()
Посредник / Mediator
Посредник — это поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник.
У нас есть классы лэйбл и кнопка и мы не хотим, чтобы они знали друг о друге (т.е. были сильно связаны). Мы хотим иметь возможность в любой момент заменить или кнопку или лейбл и сделать это с минимальными усилиями.
Давайте попробуем воспользоваться паттернов посредник.
Определим протокол посредника.
protocol Mediator: AnyObject {
func notify(sender: BaseComponent, event: String)
}
Определим базовый класс компонента, которыми будут наши кнопка и лейбл.
class BaseComponent {
fileprivate weak var mediator: Mediator?
init(mediator: Mediator? = nil) {
self.mediator = mediator
}
func update(mediator: Mediator) {
self.mediator = mediator
}
}
Как видим базовый класс предполагает, что он будет связан только с посредником, которого он принимает в инициализаторе и может обновить в методе update.
Создадим классы кнопки и лейбла.
class Button: BaseComponent {
func buttonPressed() {
print("buttonPressed from Button")
mediator?.notify(sender: self, event: "buttonPressed")
}
}
class Label: BaseComponent {
func showText(_ text: String) {
print("Text from label: \(text)")
}
}
Кнопка по нажатию уведомляет по посредника о данном событии и передает в методе notify себя и описание или объект события.
Рассмотрим класс посредника.
class ConcreteMediator: Mediator {
private var button: Button
private var label: Label
init(_ button: Button, _ label: Label) {
self.button = button
self.label = label
button.update(mediator: self)
label.update(mediator: self)
}
func notify(sender: BaseComponent, event: String) {
if event == "buttonPressed" {
print("Mediator reacts on buttonPressed and activate label method:")
label.showText("Button was Pressed")
}
}
}
Как видим он содержит кнопку и лейбл. При получении любого события он определяет что это за событие (в нашем случае это нажатие на кнопку) и производит определенные действия (в нашем случае вызывает метод showText у лейбла.)
Вот как все будет выглядеть в вызывающем коде.
let button = Button()
let label = Label()
let mediator = ConcreteMediator(button, label)
button.buttonPressed()
Снимок / Memento
Снимок — это поведенческий паттерн проектирования, который позволяет сохранять и восстанавливать прошлые состояния объектов, не раскрывая подробностей их реализации.
Мы пишем текстовый редактор, в котором наш текст может менять цвет и мы хотим иметь возможность после изменений вернуться к одному из предыдущих состояний. В этом нам поможет паттерн снимок.
Создадим класс нашего цветного текста.
class ColorText {
var color = ""
var text = ""
func setColor(color: String) {
self.color = color
}
func setText(text: String) {
self.text = text
}
func saveState() -> ColorTextMomento {
return ColorTextMomento(color: color, text: text, colorText: self)
}
}
Создадим снимок в виде структуры.
struct ColorTextMomento {
let color: String
let text: String
let colorText: ColorText
func restoreColorTextState() {
colorText.setText(text: text)
colorText.setColor(color: color)
}
}
Как видим снимок содержит все поля класса ColorText, а так же ссылку на экземпляр класса ColorText. В поля мы сохраняем нужные значения, а по ссылке можем восстановить их в нужном экземпляре класса.
Вот как это будет выглядеть на практике.
// Создаем экземпляр класса ColorText
let colorText = ColorText()
colorText.setText(text: "Text-1")
colorText.setColor(color: "Red-1")
print("colorText.text: \(colorText.text) colorText.color: \(colorText.color)")
// Сохраняем текущее состояние в снимок
let momento1 = colorText.saveState()
print("momento1: \(momento1)")
// Изменяем состояние текста
print("CHANGE STATE 1 to STATE 2")
colorText.setText(text: "Text-2")
colorText.setColor(color: "Green-2")
print("colorText.text: \(colorText.text) colorText.color: \(colorText.color)")
// Сохраняем текущее состояние в другой снимок
let momento2 = colorText.saveState()
print("momento2: \(momento2)")
// Восстанавливаем состояние текста из первого снимка
print("RESTORE STATE 1")
momento1.restoreColorTextState()
print("colorText.text: \(colorText.text) colorText.color: \(colorText.color)")
Состояние / State
Состояние — это поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.
Рассмотрим данный паттерн на примере простой кнопки свитчера, которая будет иметь всего два состояния вкл. и выкл.
Определим протокол состояния.
protocol TogleButtonState {
func update(button: TogleButton)
func pressed()
}
Определим класс нашей кнопки.
class TogleButton {
private var state: TogleButtonState
init(_ state: TogleButtonState) {
self.state = state
setState(state: state)
}
func setState(state:TogleButtonState) {
self.state = state
self.state.update(button: self)
}
func pressed() {
state.pressed()
}
}
Как видим у кнопки есть состояние. При нажатии на кнопку мы вызываем метод pressed у этого состояния. Т.е. сама кнопка не реагирую на нажатие, она делегирует его обработку своему состоянию.
Создадим базовый класс состояния.
class BaseTogleButtonState: TogleButtonState {
private(set) weak var button: TogleButton?
func update(button: TogleButton) {
self.button = button
}
func pressed() {}
}
Состояние содержит поле типа TogleButton?, значение которого задается в методе update.
Теперь создадим два класса состояния On и Off.
class StateON: BaseTogleButtonState {
override func pressed() {
button?.setState(state: StateOFF())
print("Button state was ON")
print("Button state is OFF")
}
}
class StateOFF: BaseTogleButtonState {
override func pressed() {
button?.setState(state: StateON())
print("Button state was OFF")
print("Button state is ON")
}
}
Как видим помимо неких действий, которые совершаются при нажатии на кнопку (в нашем случае просто вызов функции print()), происходит смена состояния на противоположное и при следующем нажатии, находясь в другом состоянии кнопка совершает другие действия. Как в реальной жизни одна и та же кнопка в одном состоянии включает свет, а другом его выключает.
let tButton = TogleButton(StateOFF())
tButton.pressed()
tButton.pressed()
tButton.pressed()
Стратегия / Strategy
Стратегия — это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы.
Мы решили написать навигатор. Мы хотим чтобы он работал в пешем режиме, при езде на велосипеде и при езде на автомобиле. Суть навигатора и все его методы и связи остаются одинаковыми в всех режимах и нам хотелось бы дублировать код.
Давайте вынесем отличающееся поведение в свойство стратегия и предоставим ему выполнять специфичные для того или иного режима действия.
Определим протокол стратегии.
protocol MakeRouteStrategy {
func makeRoute(from pointA: String, to poinB: String)
}
Как видим все виды стратегии должны уметь проложить маршрут из точки A в точку B.
Создадим нужные нам стратегии.
class WalkingStrategy: MakeRouteStrategy {
func makeRoute(from pointA: String, to poinB: String) {
print("WALKING from \(pointA) to \(poinB)");
}
}
class CyclingStrategy: MakeRouteStrategy {
func makeRoute(from pointA: String, to poinB: String) {
print("CYCLING from \(pointA) to \(poinB)");
}
}
class DrivingStrategy: MakeRouteStrategy {
func makeRoute(from pointA: String, to poinB: String) {
print("DRIVING from \(pointA) to \(poinB)");
}
}
Как видим каждая из них прокладывает маршрут, но делает это по своему (в нашем случае печатает разный текст).
Наконец, создадим наш навигатор с полем стратегия и методом создания маршрута.
class Navigator {
var strategy: MakeRouteStrategy
init(strategy: MakeRouteStrategy) {
self.strategy = strategy
}
func setStrategy(strategy: MakeRouteStrategy) {
self.strategy = strategy
}
func makeRoute(from pointA: String, to poinB: String) {
strategy.makeRoute(from: pointA, to: poinB)
}
}
В вызывающем коде мы можем задавать нашему навигатору разные стратегии и использовать его в разных режимах.
let navigator = Navigator(strategy: CyclingStrategy())
navigator.makeRoute(from: "City", to: "Vilage")
navigator.setStrategy(strategy: WalkingStrategy())
navigator.makeRoute(from: "Home", to: "School")
navigator.setStrategy(strategy: DrivingStrategy())
navigator.makeRoute(from: "Moscow", to: "St.Petersburg")
Посетитель / Visitor
Посетитель — это поведенческий паттерн проектирования, который позволяет добавлять в программу новые операции, не изменяя классы объектов, над которыми эти операции могут выполняться.
Рассмотрим пример. У нас есть массив геометрических фигур. Мы хотим пробежаться по массиву и совершить определенные действия с фигурой. Например распечатать название фигуры или ее координаты.
Мы можем каждый раз при итерации по массиву проверять, тип у каждой фигуры и в зависимости от этого производить необходимые действия, а можем воспользоваться паттернов посетитель и поручить фигуре в связке с посетителем самостоятельно выполнять действия, предусмотренные для данной фигуры.
Определим протоколы для фигуры и посетителя.
protocol Shape {
func accept(visitor: Visitor)
}
protocol Visitor {
func visitDot(_ dot: Dot)
func visitCircle(_ circle: Circle)
func visitRectangle(_ rectangle: Rectangle)
}
Фигура должна принимать посетителя, а посетитель должен определить метод visit для каждой фигур, которую он планирует посетить.
Создадим три фигуры точку, круг и прямоугольник.
class Dot: Shape {
let name = "Dot"
let coords = "100, 150"
func accept(visitor: Visitor) {
print("Dot accept visitor")
visitor.visitDot(self)
}
}
class Circle: Shape {
let name = "Circle"
let coords = "200, 200"
func accept(visitor: Visitor) {
print("Circle accept visitor")
visitor.visitCircle(self)
}
}
class Rectangle: Shape {
let name = "Rectangle"
let coords = "100, 300"
func accept(visitor: Visitor) {
print("Rectangle accept visitor")
visitor.visitRectangle(self)
}
}
Как видим каждая фигура принимает посетителя и вызывает у него метод, предназначенный для данной фигуры.
Создадим два класса посетителей. Один для печати имени фигур, другой для печати координат.
class PrintNameVisitor: Visitor {
func visitDot(_ dot: Dot) {
print("The shape name is \(dot.name)")
}
func visitCircle(_ circle: Circle) {
print("The shape name is \(circle.name)")
}
func visitRectangle(_ rectangle: Rectangle) {
print("The shape name is \(rectangle.name)")
}
}
class PrintCoordsVisitor: Visitor {
func visitDot(_ dot: Dot) {
print("\(dot.name) coords are: \(dot.coords)")
}
func visitCircle(_ circle: Circle) {
print("\(circle.name) coords are: \(circle.coords)")
}
func visitRectangle(_ rectangle: Rectangle) {
print("\(rectangle.name) coords are: \(rectangle.coords)")
}
}
Вот как мы будем использовать наши классы в вызывающем коде.
// Создаем фигуры и помещаем их в массив
let dot = Dot()
let circle = Circle()
let rect = Rectangle()
let shapes: [Shape] = [dot, circle, rect]
// Создаем посетителей
let nameVisitor = PrintNameVisitor()
let coordsVisitor = PrintCoordsVisitor()
// Пробегаемся по массиву с первым или вторым посетителем
shapes.forEach {$0.accept(visitor: nameVisitor)}
print("\n")
shapes.forEach {$0.accept(visitor: coordsVisitor)}
На этом про поведенческие паттерны все. Надеюсь, что данная статья поможет начинающим разработчикам разобраться в такой не самой простой теме как паттерну проектирования.