Chain
Published: 14 November 2017
Tagged:
Pattern iOS Swift

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


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

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

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

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



Реализация

  1. Общий интерфейс для всех обработчиков MiddlewareProtocol:
    protocol MiddlewareProtocol {
      // Связываем текущий и следующий экземпляр обработчика
      @discardableResult func link(with: MiddlewareProtocol) -> MiddlewareProtocol
      // Этот метод необходимо перегрузить, в нем описывается вся логика
      func check(value: MiddlewareItem) -> MiddlewareItem
      // Вспомогательный метод для упрощения вызова следующей проверки в цепочке
      func checkNext(value: MiddlewareItem) -> MiddlewareItem
    }
  2. Добавим перечисление MiddlewareItem -- этот тип данных содержит само значение .value и ошибку .error если мы не прошли валидацию:
    public enum MiddlewareItem {
      case value(String?)
      case error(String)
    }
  3. Базовая реализация обработчика 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

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



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

  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