Цепочка обязанностей
Валидируем
Мотивы
Необходимо последовательно выполнить провероку над данными, а затем для них применить изменения. Причем порядок проверок и изменений хочется применять как угодно, в зависимости от ситуации. В этом нам на помощь приходит поведенческий паттерн «Цепочка обязанностей».
Например нам надо проверить строку с телефонным номером и отформатировать ее. Опишем по порядку какие действия мы хотим выполнить:
- Строка не должна быть пустой и не nil
- Строка должна содержать определенное количество символов
- Необходимо отформатировать строку с учетом указанного формата
Цепочка обязанностей — это поведенческий паттерн проектирования, который позволяет передавать запросы последовательно по цепочке обработчиков. Каждый последующий обработчик решает, может ли он обработать запрос сам и стоит ли передавать запрос дальше по цепи.
Playground с примером на Git-Hub
Реализация
Общий интерфейс для всех обработчиков MiddlewareProtocol:
protocol MiddlewareProtocol {
// Связываем текущий и следующий экземпляр обработчика
@discardableResult func link(with: MiddlewareProtocol) -> MiddlewareProtocol
// Этот метод необходимо перегрузить, в нем описывается вся логика
func check(value: MiddlewareItem) -> MiddlewareItem
// Вспомогательный метод для упрощения вызова следующей проверки в цепочке
func checkNext(value: MiddlewareItem) -> MiddlewareItem
}
Добавим перечисление MiddlewareItem – этот тип данных содержит само значение .value и ошибку .error если мы не прошли валидацию:
public enum MiddlewareItem {
case value(String?)
case error(String)
}
Базовая реализация обработчика MiddlewareProtocol:
class Middleware: MiddlewareProtocol {
var next: MiddlewareProtocol?
@discardableResult func link(with: MiddlewareProtocol) -> MiddlewareProtocol {
self.next = with
return with
}
func check(value: MiddlewareItem) -> MiddlewareItem {
return checkNext(value: value)
}
func checkNext(value: MiddlewareItem) -> MiddlewareItem {
guard let next = self.next else { return value }
return next.check(value: value)
}
}
Все обработчики необходимо наследовать от Middleware
Теперь напишем конкретные обработчики для валидации и изменения данных, в нашем случае это строка:
- Обработчик проверки на nil и пустую строку: CheckNilMiddleware
- Обработчик проверки на длинну строки: CheckCountMiddleware
- Обработчик который форматирует строку с учетом указанного формата: PhoneFormatterMiddleware
Реализация всех трех классов описаных выше .
Разберем подробнее
- Обработчик должен наследоваться от Middleware
- Перегружем метод check(value: MiddlewareItem) -> MiddlewareItem и описываем логику проверок, изменений над данными .value(let val).
- Если все успешно вызываем следующую проверку в нашем списке цепочек return checkNext(value: value)
- Если хотим прервать выполнение то сразу возвращаем ошибку return .error(“Example error text”).
- Если хотим подменить данные то передаем новое значение return checkNext(value: .value(“New value”))
override func check(value: MiddlewareItem) -> MiddlewareItem {
switch value {
case .value(let str):
if str == nil { // Выполняем проверку
// Валидация не прошла, возвращаем ошибку
return .error("Example error text")
}
default:
break
}
// Если все успешно вызываем следующую
// проверку в нашем списке цепочек
return checkNext(value: value)
}
Смотри комментарии к коду
Использование
Проверяем и отформатируем телефонный номер с помощью нашей реализации:
let start = Middleware()
start
.link(with: CheckNilMiddleware())
.link(with: CheckCountMiddleware(length: 11))
.link(with: PhoneFormatterMiddleware(format: "+X (XXX) XXX XX-XX"))
let result = start.check(value: .value("79634480209")))
switch result {
case .error(let error): print(error)
case .value(let result): print(result ?? "")
}
Можно добавить немного функциональной магии, добавим оператор |>
precedencegroup ForwardOperator {
associativity: left
}
infix operator |>: ForwardOperator
func |> (lhs: MiddlewareProtocol, rhs: MiddlewareProtocol) -> MiddlewareProtocol {
return lhs.link(with: rhs)
}
// после можно использовать такое написание
let start = Middleware()
start
|> NilValidation()
|> CountValidation(length: 11)
|> PhoneFormatterValidation(format: "+X (XXX) XXX XX-XX")
switch start.check(value: .value("79634481259")) {
case let .error(error): print(error)
case let .value(result): print(result ?? "")
}
Результатом выполнения будет вывод в консоль: +7 (963) 448 02-09
В заключении
Таким образом мы можем добавлять сколько угодно не зависящих друг от друга обработчиков и использовать их в зависимости от ситуации.
Где еще можно использовать:
- Редактор изображений. Добавить обработчики которые будут выполнять манипуляции над изображением, таким образом получим удобный способ применять фильтры
- Валидация при авторизации или регистрации пользователя