La Chaîne de responsabilité est un patron de conception comportemental qui permet de faire circuler une demande tout au long d’une chaîne de handlers, jusqu’à ce que l’un d’entre eux la traite.
Ce patron permet à plusieurs objets de traiter une demande sans coupler la classe du demandeur aux classes concrètes des récepteurs. La chaîne peut être assemblée dynamiquement à l’exécution à l’aide de tout handler implémentant l’interface standard des handlers.
Exemple conceptuel
Dans cet exemple, nous allons voir la structure de la Chaîne de responsabilité et répondre aux questions suivantes :
Que contiennent les classes ?
Quels rôles jouent-elles ?
Comment les éléments du patron sont-ils reliés ?
Après avoir étudié la structure du patron, vous pourrez plus facilement comprendre l’exemple qui est basé sur un cas d’utilisation réel en Swift.
Example.swift: Exemple conceptuel
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: Résultat de l’exécution
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.
Analogie du monde réel
Example.swift: Analogie du monde réel
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: Résultat de l’exécution
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