Мотивы

Необходимо последовательно выполнить провероку над данными, а затем для них применить изменения. Причем порядок проверок и изменений хочется применять как угодно, в зависимости от ситуации. В этом нам на помощь приходит поведенческий паттерн «Цепочка обязанностей».

Например нам надо проверить строку с телефонным номером и отформатировать ее. Опишем по порядку какие действия мы хотим выполнить:

  1. Строка не должна быть пустой и не nil
  2. Строка должна содержать определенное количество символов
  3. Необходимо отформатировать строку с учетом указанного формата

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

Playground с примером на Git-Hub :octocat:


Реализация

Общий интерфейс для всех обработчиков 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

Теперь напишем конкретные обработчики для валидации и изменения данных, в нашем случае это строка:

  1. Обработчик проверки на nil и пустую строку: CheckNilMiddleware
  2. Обработчик проверки на длинну строки: CheckCountMiddleware
  3. Обработчик который форматирует строку с учетом указанного формата: PhoneFormatterMiddleware

Реализация всех трех классов описаных выше :octocat:.


Разберем подробнее

  1. Обработчик должен наследоваться от Middleware
  2. Перегружем метод check(value: MiddlewareItem) -> MiddlewareItem и описываем логику проверок, изменений над данными .value(let val).
  3. Если все успешно вызываем следующую проверку в нашем списке цепочек 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


В заключении

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

Где еще можно использовать:

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

Есть вопросы? Пишите сюда – @alobanov