22.
Chain-of-Responsibility Pattern
Written by Joshua Greene
The chain-of-responsibility pattern is a behavioral design pattern that allows an event to be processed by one of many handlers. It involves three types:
-
The client accepts and passes events to an instance of a handler protocol. Events may be simple, property-only structs or complex objects, such as intricate user actions.
-
The handler protocol defines required properties and methods that concrete handlers must implement. This may be substituted for an abstract, base class instead allowing for stored properties on it. Even then, it’s still not meant to be instantiated directly. Rather, it only defines requirements that concrete handlers must fulfill.
-
The first concrete handler implements the handler protocol, and it’s stored directly by the client. Upon receiving an event, it first attempts to handle it. If it’s not able to do so, it passes the event on to its next handler.
Thereby, the client can treat all of the concrete handlers as if they were a single instance. Under the hood, each concrete handler determines whether or not to handle an event passed to it or pass it on to the next handler. This happens without the client needing to know anything about the process!
If there aren’t any concrete handlers capable of handling the event, the last handler simply returns nil, does nothing or throws an error depending on your requirements.
When should you use it?
Use this pattern whenever you have a group of related objects that handle similar events but vary based on event type, attributes or anything else related to the event.
Concrete handlers may be different classes entirely or they may be the same class type but different instances and configurations.
For example, you can use this pattern to implement a VendingMachine that accepts coins:
- The
VendingMachineitself would be the client and would accept coin input events. - The handler protocol would require a
handleCoinValidation(_:)method and anextproperty. - The concrete handlers would be coin validators. They would determine whether an unknown coin was valid based on certain criteria, such as a coin’s weight and diameter, and use this to create a known coin type, such as a
Penny.
Playground example
Open AdvancedDesignPatterns.xcworkspace in the Starter directory, and then open the ChainOfResponsibility page.
For this playground example, you’ll implement the VendingMachine mentioned above. For simplicity, it will only accept U.S. pennies, nickels, dimes and quarters. So don’t try feeding it Canadian coins!
You’ll consider each coin’s diameter and weight to validate said coins. Here are the official specifications per the United States Mint:
Ok, that’s all you need to know, so it’s time to make some money! Or rather, accept some money — you’re creating a vending machine, after all.
Before creating the chain-of-responsibility specific classes, you first need to declare a few models. Add the following right after Code Example:
// MARK: - Models
// 1
public class Coin {
// 2
public class var standardDiameter: Double {
return 0
}
public class var standardWeight: Double {
return 0
}
// 3
public var centValue: Int { return 0 }
public final var dollarValue: Double {
return Double(centValue) / 100
}
// 4
public final let diameter: Double
public final let weight: Double
// 5
public required init(diameter: Double, weight: Double) {
self.diameter = diameter
self.weight = weight
}
// 6
public convenience init() {
let diameter = type(of: self).standardDiameter
let weight = type(of: self).standardWeight
self.init(diameter: diameter, weight: weight)
}
}
Let’s go over this step by step:
-
You first create a new class for
Coin, which you’ll use as the superclass for all coin types. -
You then declare
standardDiameterandstandardWeightas class properties. You’ll override these within each specific coin subclass, and you’ll use them later when you create the coin validators. -
You declare
centValueanddollarValueas computed properties. You’ll overridecentValueto return the correct value for each specific coin. Since there’s always 100 cents to a dollar, you makedollarValueafinalproperty. -
You create
diameterandweightas stored properties. As coins age, they get dinged and worn down. Consequently, their diameters and weights tend to decrease slightly over time. You’ll compare a coin’s diameter and weight against the standards later when you create the coin validators. -
You create a designated initializer that accepts a specific coin’s diameter and weight. It’s important that this is a
requiredinitializer: You’ll use this to create subclasses by calling it on aCoin.Typeinstance - i.e. a type ofCoin. -
You lastly create a convenience initializer. This creates a standard coin using
type(of: self)to get thestandardDiameterandstandardWeight. This way, you won’t have to override this initializer for each specific coin subclass.
Next, add the following:
extension Coin: CustomStringConvertible {
public var description: String {
return String(format:
"%@ {diameter: %0.3f, dollarValue: $%0.2f, weight: %0.3f}",
"\(type(of: self))", diameter, dollarValue, weight)
}
}
To inspect coins, you’ll print them to the console. You make Coin conform to CustomStringConvertible to give it a nice description that includes the coin’s type, diameter, dollarValue and weight.
You next need to add concrete coin types. Add this code to do so:
public class Penny: Coin {
public override class var standardDiameter: Double {
return 19.05
}
public override class var standardWeight: Double {
return 2.5
}
public override var centValue: Int { return 1 }
}
public class Nickel: Coin {
public override class var standardDiameter: Double {
return 21.21
}
public override class var standardWeight: Double {
return 5.0
}
public override var centValue: Int { return 5 }
}
public class Dime: Coin {
public override class var standardDiameter: Double {
return 17.91
}
public override class var standardWeight: Double {
return 2.268
}
public override var centValue: Int { return 10 }
}
public class Quarter: Coin {
public override class var standardDiameter: Double {
return 24.26
}
public override class var standardWeight: Double {
return 5.670
}
public override var centValue: Int { return 25 }
}
With the previous code, you create subclasses of Coin for Penny, Nickel, Dime and Quarter using the coin specifications provided earlier.
Great! You’re now ready to add the chain-of-responsibility classes. Add the following to the end of the playground:
// MARK: - HandlerProtocol
public protocol CoinHandlerProtocol {
var next: CoinHandlerProtocol? { get }
func handleCoinValidation(_ unknownCoin: Coin) -> Coin?
}
Here, you declare the handler protocol, which has requirements for handleCoinValidation(_:) and a next property.
Add this code next:
// MARK: - Concrete Handler
// 1
public class CoinHandler {
// 2
public var next: CoinHandlerProtocol?
public let coinType: Coin.Type
public let diameterRange: ClosedRange<Double>
public let weightRange: ClosedRange<Double>
// 3
public init(coinType: Coin.Type,
diameterVariation: Double = 0.05,
weightVariation: Double = 0.05) {
self.coinType = coinType
let standardDiameter = coinType.standardDiameter
self.diameterRange =
(1-diameterVariation)*standardDiameter ...
(1+diameterVariation)*standardDiameter
let standardWeight = coinType.standardWeight
self.weightRange =
(1-weightVariation)*standardWeight ...
(1+weightVariation)*standardWeight
}
}
Here’s what you’ve done:
-
You declare
CoinHandler, which will be the concrete handler. -
You declare several properties:
-
nextwill hold onto the nextCoinHandler. -
coinTypewill be the specificCointhis instance will create. Consequently, you won’t need to create specific coin validators forPenny,Nickel,DimeandQuarter. -
diameterRangeandweightRangewill be the valid range for this specific coin.
- You lastly create an designated initializer,
init(coinType: diameterVariation:weightVariation). Within this, you setself.coinTypetocoinType, and you usestandardDiameterandstandardWeightto createself.diameterRangeandself.weightRange.
You also need to make CoinHandler conform to CoinHandlerProtocol:
extension CoinHandler: CoinHandlerProtocol {
// 1
public func handleCoinValidation(_ unknownCoin: Coin) ->
Coin? {
guard let coin = createCoin(from: unknownCoin) else {
return next?.handleCoinValidation(unknownCoin)
}
return coin
}
// 2
private func createCoin(from unknownCoin: Coin) -> Coin? {
print("Attempt to create \(coinType)")
guard diameterRange.contains(unknownCoin.diameter) else {
print("Invalid diameter")
return nil
}
guard weightRange.contains(unknownCoin.weight) else {
print("Invalid weight")
return nil
}
let coin = coinType.init(diameter: unknownCoin.diameter,
weight: unknownCoin.weight)
print("Created \(coin)")
return coin
}
}
Let’s go over these two methods:
-
Within
handleCoinValidation(_:), you first attempt to create aCoinviacreateCoin(from:)that is defined after this method. If you can’t create aCoin, you give thenexthandler a chance to attempt to create one. -
Within
createCoin(from:), you validate that the passed-inunknownCoinactually meets the requirements to create the specific coin given bycoinType. Namely, theunknownCoinmust have adiameterthat falls within thediameterRangeandweightRange.If it doesn’t, you print an error message and return
nil. If it does, you callcoinType.init(diameter:weight:)passing the values fromunknownCointo create a new instance of thecoinType. Pretty cool how you can use arequiredinitializer like that, right?
You’ve got just one more class to go! Add the following to the end of the playground:
// MARK: - Client
// 1
public class VendingMachine {
// 2
public let coinHandler: CoinHandler
public var coins: [Coin] = []
// 3
public init(coinHandler: CoinHandler) {
self.coinHandler = coinHandler
}
}
Here’s what you’ve done:
-
You create a new class for
VendingMachine, which will act as the client. -
This has just two properties:
coinHandlerandcoins.VendingMachinedoesn’t need to know that itscoinHandleris actually a chain of handlers, but instead it simply treats this as a single object. You’ll usecoinsto hold onto all of the valid, accepted coins. -
The initializer is also very simple: You simply accept a passed-in
coinHandlerinstance.VendingMachinedoesn’t need to how aCoinHandleris set up, as it simply uses it.
You also need a method to actually accept coins. Add this next code right before the closing class curly brace for VendingMachine:
public func insertCoin(_ unknownCoin: Coin) {
// 1
guard let coin = coinHandler.handleCoinValidation(unknownCoin)
else {
print("Coin rejected: \(unknownCoin)")
return
}
// 2
print("Coin Accepted: \(coin)")
coins.append(coin)
// 3
let dollarValue = coins.reduce(0, { $0 + $1.dollarValue })
print("")
print("Coins Total Value: $\(dollarValue)")
// 4
let weight = coins.reduce(0, { $0 + $1.weight })
print("Coins Total Weight: \(weight) g")
print("")
}
Here’s what this does:
-
You first attempt to create a
Coinby passing anunknownCointocoinHandler. If a valid coin isn’t created, you print out a message indicating that the coin was rejected. -
If a valid
Coinis created, you print a success message and append it tocoins. -
You then get the
dollarValuefor all of thecoinsand print this. -
You lastly get the
weightfor all of thecoinsand print this, too.
You’ve created a vending machine — But you still need to try it out!
Add this code to the end of the playground:
// MARK: - Example
// 1
let pennyHandler = CoinHandler(coinType: Penny.self)
let nickleHandler = CoinHandler(coinType: Nickel.self)
let dimeHandler = CoinHandler(coinType: Dime.self)
let quarterHandler = CoinHandler(coinType: Quarter.self)
// 2
pennyHandler.next = nickleHandler
nickleHandler.next = dimeHandler
dimeHandler.next = quarterHandler
// 3
let vendingMachine = VendingMachine(coinHandler: pennyHandler)
Let’s go over this:
-
Before you can instantiate a
VendingMachine, you must first set up thecoinHandlerobjects for it. You do so by creating instances ofCoinHandlerforpennyHandler,nickleHandler,dimeHandlerandquarterHandler. -
You then hook up the
nextproperties for the handlers. In this case,pennyHandlerwill be the first handler, followed bynickleHandler,dimeHandlerand lastlyquarterHandlerin the chain. Since there aren’t any other handlers afterquarterHandler, you leave itsnextset tonil. -
You lastly create
vendingMachineby passingpennyHandleras thecoinHandler.
You can now insert coins in the vendingMachine! Add the following to insert a standard Penny:
let penny = Penny()
vendingMachine.insertCoin(penny)
You should see the following printed to the console:
Attempt to create Penny
Created Penny {diameter: 0.750,
dollarValue: $0.01, weight: 2.500}
Accepted Coin: Penny {diameter: 0.750,
dollarValue: $0.01, weight: 2.500}
Coins Total Value: $0.01
Coins Total Weight: 2.5 g
Awesome — the penny was handled correctly. However, this one was easy: It was a standard penny, after all!
Add the following code next to create an unknown Coin matching the criteria for a Quarter:
let quarter = Coin(diameter: Quarter.standardDiameter,
weight: Quarter.standardWeight)
vendingMachine.insertCoin(quarter)
You should then see this in the console:
Attempt to create Penny
Invalid diameter
Attempt to create Nickel
Invalid diameter
Attempt to create Dime
Invalid diameter
Attempt to create Quarter
Created Quarter {diameter: 0.955,
dollarValue: $0.25, weight: 5.670}
Accepted Coin: Quarter {diameter: 0.955,
dollarValue: $0.25, weight: 5.670}
Coins Total Value: $0.26
Coins Total Weight: 8.17 g
Great — the quarter was also handled correctly! Notice the print statements for penny, nickel and dime, too? This is expected behavior: The unknown coin was passed from CoinHandler to CoinHandler until, finally, the last one was able to create a Quarter from it.
Lastly, add the following to insert an invalid coin:
let invalidDime = Coin(diameter: Quarter.standardDiameter,
weight: Dime.standardWeight)
vendingMachine.insertCoin(invalidDime)
You should then see this printed to the console:
Attempt to create Penny
Invalid diameter
Attempt to create Nickel
Invalid diameter
Attempt to create Dime
Invalid diameter
Attempt to create Quarter
Invalid weight
Coin rejected: Coin {diameter: 0.955,
dollarValue: $0.00, weight: 2.268}
Fantastic! VendingMachine rejected that invalid coin just as it should.
What should you be careful about?
The chain-of-responsibility pattern works best for handlers that can determine very quickly whether or not to handle an event. Be careful about creating one or more handlers that are slow to pass an event to the next handler.
You also need to consider what happens if an event can’t be handled. Will you return nil, throw an error or do something else? You should identify this upfront, so you can plan your system appropriately.
You should also consider whether or not an event needs to be processed by more than one handler. As a variation on this pattern, you can forward the same event to all handlers, instead of stopping at the first one that can handle it, and then return an array of response objects.
Tutorial project
You’ll build an app called RWSecret in this chapter. This app allows users to decrypt secret messages by attempting several known passwords provided by the user.
You’ll use two open-source libraries in this app: SwiftKeychainWrapper (http://bit.ly/SwiftKeychainWrapper) to store passwords within the iOS keychain, and RNCryptor (http://bit.ly/RNCryptor) to perform AES, or Advanced Encryption Standard, decryption.
It’s OK if you’re not familiar with the iOS keychain or AES decryption — these libraries do the heavy lifting for you! Your task will be to set up a handler chain to perform decryption.
Open Finder and navigate to where you downloaded the resources for this chapter. Then, open Starter\RWSecret\RWSecret.xcworkspace (not the .xcodeproj file) in Xcode.
This app uses CocoaPods to pull in the open-source libraries. Everything has already been included for you, so you don’t need to do pod install. You simply need to use the .xcworkspace instead of the .xcodeproj file.
Build and run. You’ll see the Decrypt screen:
If you Tap to decrypt, you’ll see this printed to the console:
Decryption failed!
What’s up with that?
While the view has already been set up to display secret messages, the app doesn’t know how to decrypt them! Before you can add this functionality, you first need to know a bit about how the app works.
Open SecretMessage.swift, and you’ll see this is a simple model with two properties, encrypted and decrypted:
-
encryptedholds onto to the encrypted form of the message. This is set viainit(encrypted:), so it will always have a value. -
decryptedis set whenever the message is decrypted. This is initially set tonil, asSecretMessagedoesn’t know how to perform decryption.
Next, open DecryptViewController.swift. This is the view controller that’s shown whenever the app is launched. It uses a tableView to display SecretMessages. Scroll down to tableView(_:didSelectRowAt:) to see what happens when a cell is tapped.
Specifically, look for this line:
secretMessage.decrypted = passwordClient.decrypt(secretMessage.encrypted)
passwordClient acts as the client for handling decryption requests, but seemingly, this method must always be returning nil.
Open PasswordClient.swift, scroll down to decrypt(_:), and you’ll find there’s a TODO comment there. Ah ha! This is what you need to implement. Specifically, you need to set up a chain of decryption handlers to perform decryption.
To do so, create a new file called DecryptionHandlerProtocol.swift within the PasswordClient group and replace its contents with the following:
import Foundation
public protocol DecryptionHandlerProtocol {
var next: DecryptionHandlerProtocol? { get }
func decrypt(data encryptedData: Data) -> String?
}
DecryptionHandlerProtocol will act as the handler protocol. It has two requirements: next to hold onto the next decryption handler, and decrypt(data:) to perform decryption.
Create another new file called DecryptionHandler.swift within the PasswordClient group and replace its contents with the following:
import RNCryptor
public class DecryptionHandler {
// MARK: - Instance Properties
public var next: DecryptionHandlerProtocol?
public let password: String
public init(password: String) {
self.password = password
}
}
DecryptionHandler will act as a concrete handler. This has two properties: next per the DecryptionHandlerProtocol requirement, and password to hold onto the decryption password to use.
You also need to make DecryptionHandler conform to DecryptionHandler Protocol. Add the following right after the previous code:
extension DecryptionHandler: DecryptionHandlerProtocol {
public func decrypt(data encryptedData: Data) -> String? {
guard let data = try? RNCryptor.decrypt(
data: encryptedData,
withPassword: password),
let text = String(data: data, encoding: .utf8) else {
return next?.decrypt(data: encryptedData)
}
return text
}
}
This method accepts encryptedData and calls RNCryptor.decrypt(data:withPassword:) to attempt the decryption. If it’s successful, you return the resulting text. Otherwise, it passes the provided encryptedData on to the next handler to attempt decryption.
You’re making great progress! You next need to add a reference to the DecryptionHandlerProtocol on the client. Open PasswordClient.swift and add the following property, right after the others:
private var decryptionHandler: DecryptionHandlerProtocol?
Next, scroll down to setupDecryptionHandler(). This method is called in two places: in didSet for passwords, which is called whenever a new password is added or removed, and in init() after passwords have been loaded from the keychain. Replace the TODO comment within this method with the following:
// 1
guard passwords.count > 0 else {
decryptionHandler = nil
return
}
// 2
var current = DecryptionHandler(password: passwords.first!)
decryptionHandler = current
// 3
for i in 1 ..< passwords.count {
let next = DecryptionHandler(password: passwords[i])
current.next = next
current = next
}
Here’s how this works step by step:
-
You first ensure that
passwordsisn’t empty. Otherwise, you setdecryptionHandlertonil. -
You create a
DecryptionHandlerfor the first password, and you set this to bothcurrentanddecryptionHandler. -
You lastly iterate through the remaining
passwords. You create aDecryptionHandlerfor each, which you set ascurrent.nextand then updatecurrenttonextas well. In this manner, you ultimately set up a chain ofDecryptionHandlerobjects.
You lastly need to implement decrypt(_:). Replace the contents of it with the following:
guard let data = Data(base64Encoded: base64EncodedString),
let value = decryptionHandler?.decrypt(data: data) else {
return nil
}
return value
Since decrypt(_:) takes a String, you first attempt to convert this into base-64 encoded data and then pass this to the decryptionHandler for decryption. If this is successful, you return the resulting decrypted value. Otherwise, you return nil.
Great job — that takes care of the chain-of-responsibility implementation! Build and run. Tap to decrypt on the first cell. And then… you still see a Decryption failed! in the console!? What gives?
Remember how RWSecret uses the keychain to hold onto passwords? Yep, you first need to add the correct passwords. Tap on the Passwords button in the top right corner. Then, type password into the text field and press add.
Likewise, add passwords for ray and raywender.
Tap < Decrypt to return to the decryption screen and then Tap to Decrypt each cell to reveal the secret messages!
Key points
You learned about the chain-of-responsibility pattern in this chapter. Here are its key points:
-
The chain-of-responsibility pattern allows an event to be processed by one of many handlers. It involves three types: a client, handler protocol, and concrete handlers.
-
The client accepts events and passes them onto its handler protocol instance; the handler protocol defines required methods and properties each concrete handler much implement; and each concrete handler can accept an event and in turn either handle it or pass it onto the next handler.
-
This pattern thereby defines a group of related handlers, which vary based on the type of event each can handle. If you need to handle new types of events, you simply create a new concrete handler.
Where to go from here?
Using the chain-of-responsibility pattern, you created a secret message app that decrypts messages using passwords provided by the user. There’s still a lot of functionality that you can add to RWSecret:
-
You can add the ability to input and encrypt secret messages, instead of just decrypting them.
-
You can add the capability to send secret messages to other users.
-
You can support several types of decryption instead of only AES.
Each of these is possible using the existing patterns that you’ve already learned from this book. Feel free to continue experimenting with RWSecret as much as you like.
When you’re ready, continue onto the next chapter to learn about the coordinator design pattern.