스위프트로 작성된 책임 연쇄
책임 연쇄 패턴은 핸들러 중 하나가 요청을 처리할 때까지 핸들러들의 체인(사슬)을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴입니다.
이 패턴은 발신자 클래스를 수신자들의 구상 클래스들에 연결하지 않고도 여러 객체가 요청을 처리할 수 있도록 합니다. 체인은 표준 핸들러 인터페이스를 따르는 모든 핸들러와 런타임 때 동적으로 구성될 수 있습니다.
복잡도:
인기도:
사용 예시들: 책임 연쇄 패턴은 스위프트 코드에 매우 일반적이며, 당신의 코드가 필터, 이벤터 체인 등과 같은 객체 체인과 함께 작동할 때 특히 유용합니다.
식별: 패턴의 모든 객체는 공통 인터페이스를 따르며, 다른 객체들의 같은 메서드들을 간접적으로 호출하는 한 객체 그룹의 행동 메서드들이 있습니다.
개념적인 예시
이 예시는 책임 연쇄 패턴의 구조를 보여주고 다음 질문에 중점을 둡니다:
- 패턴은 어떤 클래스들로 구성되어 있나요?
- 이 클래스들은 어떤 역할을 하나요?
- 패턴의 요소들은 어떻게 서로 연관되어 있나요?
이 패턴의 구조를 배우면 실제 스위프트 사용 사례를 기반으로 하는 다음 예시를 더욱 쉽게 이해할 수 있을 것입니다.
Example.swift: 개념적인 예시
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: AnyObject { @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: 실행 결과
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