Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

22. Chain-of-Responsibility Pattern
Written by Joshua Greene

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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:

  1. 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.

  2. 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.

  3. 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 VendingMachine itself would be the client and would accept coin input events.
  • The handler protocol would require a handleCoinValidation(_:) method and a next property.
  • 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.

// 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)
  }
}
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)
  }
}
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 }
}
// MARK: - HandlerProtocol
public protocol CoinHandlerProtocol {
  var next: CoinHandlerProtocol? { get }
  func handleCoinValidation(_ unknownCoin: Coin) -> Coin?
}
// 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
  }
}
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
  }
}
// MARK: - Client
// 1
public class VendingMachine {

  // 2
  public let coinHandler: CoinHandler
  public var coins: [Coin] = []

  // 3
  public init(coinHandler: CoinHandler) {
    self.coinHandler = coinHandler
  }
}
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("")
}
// 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 penny = Penny()
vendingMachine.insertCoin(penny)
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
let quarter = Coin(diameter: Quarter.standardDiameter,
                   weight: Quarter.standardWeight)
vendingMachine.insertCoin(quarter)
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
let invalidDime = Coin(diameter: Quarter.standardDiameter,
                       weight: Dime.standardWeight)
vendingMachine.insertCoin(invalidDime)
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}

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.

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.

Decryption failed!
secretMessage.decrypted = passwordClient.decrypt(secretMessage.encrypted)
import Foundation

public protocol DecryptionHandlerProtocol {
  var next: DecryptionHandlerProtocol? { get }
  func decrypt(data encryptedData: Data) -> String?
}
import RNCryptor

public class DecryptionHandler {

  // MARK: - Instance Properties
  public var next: DecryptionHandlerProtocol?
  public let password: String

  public init(password: String) {
    self.password = password
  }
}
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
  }
}
private var decryptionHandler: DecryptionHandlerProtocol?
// 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
}
guard let data = Data(base64Encoded: base64EncodedString),
  let value = decryptionHandler?.decrypt(data: data) else {
    return nil
}
return value

Key points

You learned about the chain-of-responsibility pattern in this chapter. Here are its key points:

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:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now