Chain of Responsibility is behavioral design pattern that allows passing request along the chain of potential handlers until one of them handles request.
The pattern allows multiple objects to handle the request without coupling sender class to the concrete classes of the receivers. The chain can be composed dynamically at runtime with any handler that follows a standard handler interface.
Conceptual Example
This example illustrates the structure of the Chain of Responsibility design pattern and focuses on the following questions:
What classes does it consist of?
What roles do these classes play?
In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world Swift use case.
Example.swift: Conceptual example
import XCTest
/// The Handler interface declares a method for building the chain of handlers.
/// It also declares a method for executing a request.
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
/// Returning a handler from here will let us link handlers in a
/// convenient way like this:
/// monkey.setNext(handler: squirrel).setNext(handler: dog)
return handler
}
func handle(request: String) -> String? {
return nextHandler?.handle(request: request)
}
}
/// All Concrete Handlers either handle a request or pass it to the next handler
/// in the chain.
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)
}
}
}
/// The client code is usually suited to work with a single handler. In most
/// cases, it is not even aware that the handler is part of a chain.
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)
}
}
// ...
}
/// Let's see how it all works together.
class ChainOfResponsibilityConceptual: XCTestCase {
func test() {
/// The other part of the client code constructs the actual chain.
let monkey = MonkeyHandler()
let squirrel = SquirrelHandler()
let dog = DogHandler()
monkey.setNext(handler: squirrel).setNext(handler: dog)
/// The client should be able to send a request to any handler, not just
/// the first one in the chain.
print("Chain: Monkey > Squirrel > Dog\n\n")
Client.someClientCode(handler: monkey)
print()
print("Subchain: Squirrel > Dog\n\n")
Client.someClientCode(handler: squirrel)
}
}
Output.txt: Execution result
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.
Real World Example
Example.swift: Real world example
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: Execution result
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