Цепочка обязанностей — это поведенческий паттерн, позволяющий передавать запрос по цепочке потенциальных обработчиков, пока один из них не обработает запрос.
Избавляет от жёсткой привязки отправителя запроса к его получателю, позволяя выстраивать цепь из различных обработчиков динамически.
Концептуальный пример
Этот пример показывает структуру паттерна Цепочка обязанностей , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest
/// Интерфейс Обработчика объявляет метод построения цепочки обработчиков. Он
/// также объявляет метод для выполнения запроса.
protocol Handler: class {
@discardableResult
func setNext(handler: Handler) -> Handler
func handle(request: String) -> String?
var nextHandler: Handler? { get set }
}
extension Handler {
func setNext(handler: Handler) -> Handler {
self.nextHandler = handler
/// Возврат обработчика отсюда позволит связать обработчики простым
/// способом, вот так:
/// monkey.setNext(handler: squirrel).setNext(handler: dog)
return handler
}
func handle(request: String) -> String? {
return nextHandler?.handle(request: request)
}
}
/// Все Конкретные Обработчики либо обрабатывают запрос, либо передают его
/// следующему обработчику в цепочке.
class MonkeyHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
if (request == "Banana") {
return "Monkey: I'll eat the " + request + ".\n"
} else {
return nextHandler?.handle(request: request)
}
}
}
class SquirrelHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
if (request == "Nut") {
return "Squirrel: I'll eat the " + request + ".\n"
} else {
return nextHandler?.handle(request: request)
}
}
}
class DogHandler: Handler {
var nextHandler: Handler?
func handle(request: String) -> String? {
if (request == "MeatBall") {
return "Dog: I'll eat the " + request + ".\n"
} else {
return nextHandler?.handle(request: request)
}
}
}
/// Обычно клиентский код приспособлен для работы с единственным обработчиком. В
/// большинстве случаев клиенту даже неизвестно, что этот обработчик является
/// частью цепочки.
class Client {
// ...
static func someClientCode(handler: Handler) {
let food = ["Nut", "Banana", "Cup of coffee"]
for item in food {
print("Client: Who wants a " + item + "?\n")
guard let result = handler.handle(request: item) else {
print(" " + item + " was left untouched.\n")
return
}
print(" " + result)
}
}
// ...
}
/// Давайте посмотрим как всё это будет работать.
class ChainOfResponsibilityConceptual: XCTestCase {
func test() {
/// Другая часть клиентского кода создает саму цепочку.
let monkey = MonkeyHandler()
let squirrel = SquirrelHandler()
let dog = DogHandler()
monkey.setNext(handler: squirrel).setNext(handler: dog)
/// Клиент должен иметь возможность отправлять запрос любому
/// обработчику, а не только первому в цепочке.
print("Chain: Monkey > Squirrel > Dog\n\n")
Client.someClientCode(handler: monkey)
print()
print("Subchain: Squirrel > Dog\n\n")
Client.someClientCode(handler: squirrel)
}
}
Output.txt: Результат выполнения
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Пример из реальной жизни
Example.swift: Пример из реальной жизни
import Foundation
import UIKit
import XCTest
protocol Handler {
var next: Handler? { get }
func handle(_ request: Request) -> LocalizedError?
}
class BaseHandler: Handler {
var next: Handler?
init(with handler: Handler? = nil) {
self.next = handler
}
func handle(_ request: Request) -> LocalizedError? {
return next?.handle(request)
}
}
class LoginHandler: BaseHandler {
override func handle(_ request: Request) -> LocalizedError? {
guard request.email?.isEmpty == false else {
return AuthError.emptyEmail
}
guard request.password?.isEmpty == false else {
return AuthError.emptyPassword
}
return next?.handle(request)
}
}
class SignUpHandler: BaseHandler {
private struct Limit {
static let passwordLength = 8
}
override func handle(_ request: Request) -> LocalizedError? {
guard request.email?.contains("@") == true else {
return AuthError.invalidEmail
}
guard (request.password?.count ?? 0) >= Limit.passwordLength else {
return AuthError.invalidPassword
}
guard request.password == request.repeatedPassword else {
return AuthError.differentPasswords
}
return next?.handle(request)
}
}
class LocationHandler: BaseHandler {
override func handle(_ request: Request) -> LocalizedError? {
guard isLocationEnabled() else {
return AuthError.locationDisabled
}
return next?.handle(request)
}
func isLocationEnabled() -> Bool {
return true /// Calls special method
}
}
class NotificationHandler: BaseHandler {
override func handle(_ request: Request) -> LocalizedError? {
guard isNotificationsEnabled() else {
return AuthError.notificationsDisabled
}
return next?.handle(request)
}
func isNotificationsEnabled() -> Bool {
return false /// Calls special method
}
}
enum AuthError: LocalizedError {
case emptyFirstName
case emptyLastName
case emptyEmail
case emptyPassword
case invalidEmail
case invalidPassword
case differentPasswords
case locationDisabled
case notificationsDisabled
var errorDescription: String? {
switch self {
case .emptyFirstName:
return "First name is empty"
case .emptyLastName:
return "Last name is empty"
case .emptyEmail:
return "Email is empty"
case .emptyPassword:
return "Password is empty"
case .invalidEmail:
return "Email is invalid"
case .invalidPassword:
return "Password is invalid"
case .differentPasswords:
return "Password and repeated password should be equal"
case .locationDisabled:
return "Please turn location services on"
case .notificationsDisabled:
return "Please turn notifications on"
}
}
}
protocol Request {
var firstName: String? { get }
var lastName: String? { get }
var email: String? { get }
var password: String? { get }
var repeatedPassword: String? { get }
}
extension Request {
/// Default implementations
var firstName: String? { return nil }
var lastName: String? { return nil }
var email: String? { return nil }
var password: String? { return nil }
var repeatedPassword: String? { return nil }
}
struct SignUpRequest: Request {
var firstName: String?
var lastName: String?
var email: String?
var password: String?
var repeatedPassword: String?
}
struct LoginRequest: Request {
var email: String?
var password: String?
}
protocol AuthHandlerSupportable: AnyObject {
var handler: Handler? { get set }
}
class BaseAuthViewController: UIViewController, AuthHandlerSupportable {
/// Base class or extensions can be used to implement a base behavior
var handler: Handler?
init(handler: Handler) {
self.handler = handler
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
class LoginViewController: BaseAuthViewController {
func loginButtonSelected() {
print("Login View Controller: User selected Login button")
let request = LoginRequest(email: "smth@gmail.com", password: "123HardPass")
if let error = handler?.handle(request) {
print("Login View Controller: something went wrong")
print("Login View Controller: Error -> " + (error.errorDescription ?? ""))
} else {
print("Login View Controller: Preconditions are successfully validated")
}
}
}
class SignUpViewController: BaseAuthViewController {
func signUpButtonSelected() {
print("SignUp View Controller: User selected SignUp button")
let request = SignUpRequest(firstName: "Vasya",
lastName: "Pupkin",
email: "vasya.pupkin@gmail.com",
password: "123HardPass",
repeatedPassword: "123HardPass")
if let error = handler?.handle(request) {
print("SignUp View Controller: something went wrong")
print("SignUp View Controller: Error -> " + (error.errorDescription ?? ""))
} else {
print("SignUp View Controller: Preconditions are successfully validated")
}
}
}
class ChainOfResponsibilityRealWorld: XCTestCase {
func testChainOfResponsibilityRealWorld() {
print("Client: Let's test Login flow!")
let loginHandler = LoginHandler(with: LocationHandler())
let loginController = LoginViewController(handler: loginHandler)
loginController.loginButtonSelected()
print("\nClient: Let's test SignUp flow!")
let signUpHandler = SignUpHandler(with: LocationHandler(with: NotificationHandler()))
let signUpController = SignUpViewController(handler: signUpHandler)
signUpController.signUpButtonSelected()
}
}
Output.txt: Результат выполнения
Client: Let's test Login flow!
Login View Controller: User selected Login button
Login View Controller: Preconditions are successfully validated
Client: Let's test SignUp flow!
SignUp View Controller: User selected SignUp button
SignUp View Controller: something went wrong
SignUp View Controller: Error -> Please turn notifications on