Chain of Responsibility
Chain of Responsibility passes a request through ordered handlers. Each handler can process it, reject it, or pass it on. The caller knows only the first handler.
When it helps in iOS
A checkout request needs local checks before a network call: the cart must contain items, the address must be complete, and the account must be allowed to order. Separate handlers make the order explicit and let a feature add or remove a rule.
Original example: ordered checkout checks
import Foundation
struct CheckoutRequest {
let itemCount: Int
let postalCode: String
let accountEnabled: Bool
}
enum CheckoutError: Error { case emptyCart, missingAddress, disabledAccount }
protocol CheckoutCheck {
func validate(_ request: CheckoutRequest) throws
}
struct CartCheck: CheckoutCheck {
func validate(_ request: CheckoutRequest) throws {
guard request.itemCount > 0 else { throw CheckoutError.emptyCart }
}
}
struct AddressCheck: CheckoutCheck {
func validate(_ request: CheckoutRequest) throws {
guard !request.postalCode.isEmpty else { throw CheckoutError.missingAddress }
}
}
struct AccountCheck: CheckoutCheck {
func validate(_ request: CheckoutRequest) throws {
guard request.accountEnabled else { throw CheckoutError.disabledAccount }
}
}
struct CheckoutValidationChain {
let checks: [any CheckoutCheck]
func validate(_ request: CheckoutRequest) throws {
for check in checks { try check.validate(request) }
}
}
let chain = CheckoutValidationChain(checks: [CartCheck(), AddressCheck(), AccountCheck()])The first failing check stops the chain and supplies a specific error for the UI. Unlike a single validator with a long conditional, each rule can be tested independently.
Production decisions
Keep security and price checks on the server too; these client checks improve feedback, not trust. Order rules so cheap, actionable failures appear first. If every rule must report all errors, collect failures rather than throwing on the first one. Avoid a chain for just one or two stable conditions.
Follow the request through the chain
CheckoutValidationChain stores checks in an explicit order and invokes each through the same CheckoutCheck interface. A check either returns, allowing the next one to run, or throws a specific CheckoutError, stopping the chain. The cart check comes first because an empty cart makes later address and account checks irrelevant to this purchase. The caller needs one validation call rather than knowing all three rules.
From example to an iOS screen
The checkout view model builds a request from current cart and account state when the user taps Pay. It runs the chain before showing payment UI or sending a request. Convert each error to focused feedback: scroll to the address field for missingAddress, show an empty-cart message for emptyCart, and route a disabled account to support. The server must repeat authoritative checks because device state can be stale or manipulated.
Failure modes and verification
Order affects which error the user sees first, so test it explicitly. If product requirements demand all validation errors at once, collect an array of failures instead of throwing early. If one check has to call the network, the whole interface should become async throws; avoid blocking the main thread. A chain helps when rules are individually meaningful and can change independently; a short fixed guard sequence may be clearer otherwise.