第22章:责任链模式

责任链模式是一种行为设计模式,它允许一个事件被许多处理程序之一所处理。它涉及三种类型。

  1. 客户端接受并将事件传递给处理程序协议的一个实例。事件可以是简单的、只有属性的结构,也可以是复杂的对象,如复杂的用户行为。

  2. 处理程序协议规定了具体处理程序必须实现的必要属性和方法。这可以用一个抽象的基类来代替,而允许对其进行存储属性。即使如此,它仍然不是为了直接被实例化。相反,它只是规定了具体处理者必须满足的要求。

  3. 第一个具体的处理程序实现了处理程序协议,并由客户端直接存储。在收到一个事件后,它首先尝试处理它。如果它不能处理,它就把事件转给下一个处理程序。

因此,客户端可以把所有的具体处理程序当作是一个单一的实例。在引擎盖下,每个具体处理程序决定是否处理传递给它的事件或将其传递给下一个处理程序。这发生在客户端不需要知道任何关于这个过程的事情的情况下。

如果没有任何具体的处理程序能够处理该事件,那么最后一个处理程序会简单地返回nil,什么都不做或者抛出一个错误,这取决于你的要求。

你应该在什么时候使用它?

只要你有一组处理类似事件的相关对象,但根据事件类型、属性或其他与事件相关的东西不同,就可以使用这种模式。

具体的处理程序可能完全是不同的类,或者它们可能是同一类型的类,但实例和配置不同。

例如,你可以使用这种模式来实现一个接受硬币的VendingMachine。

playground实例

打开Starter目录下的AdvancedDesignPatterns.xcworkspace,然后打开ChainOfResponsibility页面。

对于这个playground的例子,你将实现上面提到的自动售货机。为了简单起见,它将只接受美国的便士、镍币、硬币和25美分。所以不要尝试给它喂食加拿大硬币

你要考虑每枚硬币的直径和重量来确认这些硬币。以下是美国铸币局的正式规格。

好了,这就是你需要知道的一切,所以现在是时候赚点钱了!或者说,接受一些钱--毕竟你在创建一个自动售货机。或者说,接受一些钱--毕竟你在创建一个自动售货机。

在创建责任链规范类之前,你首先需要声明一些模型。在代码示例之后添加以下内容。

// 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) 
  }
}

让我们一步步来看看。

  1. 你首先为硬币创建一个新类,你将把它作为所有硬币类型的超类。

  2. 然后声明标准直径和标准重量为类的属性。你将在每个特定的硬币子类中覆盖这些属性,并在以后创建硬币验证器时使用它们。

  3. 你声明centValue和dollarValue为计算属性。你将覆盖centValue来返回每个特定硬币的正确值。因为一美元总是有100美分,所以你把美元值作为最终属性。

  4. 你创建直径和重量作为存储属性。随着硬币的老化,它们会被刮伤和磨损。因此,它们的直径和重量往往会随着时间的推移而略有减少。当你创建硬币验证器时,你会将硬币的直径和重量与标准进行比较。

  5. 您创建一个指定的初始化器,接受一个特定的硬币直径和重量。重要的是,这是一个必需的初始化器。你将用它来创建子类,在Coin.Type实例上调用它--即Coin的类型。

  6. 你最后要创建一个方便的初始化器。这将创建一个标准硬币,使用type(of: self)来获取标准直径和标准重量。这样,你就不必为每个特定的硬币子类重写这个初始化器。

接下来,添加以下内容。

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) 
  } 
}

为了检查硬币,你要把它们打印到控制台。你让Coin符合CustomStringConvertible,给它一个漂亮的描述,包括硬币的类型、直径、美元价值和重量。

接下来你需要添加具体的硬币类型。添加这段代码来做到这一点。

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 
  }
}

通过前面的代码,你用前面提供的硬币规格为Penny、Nickel、Dime和Quarter创建硬币的子类。

很好! 现在你准备好添加责任链类了。在playground的末尾添加以下内容。

// MARK: - HandlerProtocol public protocol CoinHandlerProtocol {
  var next: CoinHandlerProtocol? { get }
  func handleCoinValidation(_ unknownCoin: Coin) -> Coin? 
}

在这里,你声明了处理程序协议,它对 handleCoinValidation(_:) 和 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
  }
}

以下是你所做的事情。

  1. 你声明CoinHandler,它将是具体的处理程序。

  2. 你声明了几个属性。

  1. 最后,你要创建一个指定的初始化器,init(coinType:
    diameterVariation:weightVariation)。在此,你将self.coinType设置为coinType,并使用standardDiameter和standardWeight来创建self.diameterRange和self.weightRange。

你还需要使CoinHandler符合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
  }
}

让我们来看看这两种方法。

  1. 在handleCoinValidation(_:)中,你首先尝试通过createCoin(from:)来创建一个硬币,这个方法是在这个方法之后定义的。如果你不能创建一个硬币,你就给下一个处理程序一个机会来尝试创建一个。

  2. 在createCoin(from:)中,你要验证传入的unknownCoin是否真的满足创建coinType所给出的特定硬币的要求。也就是说,未知硬币的直径必须在直径范围和重量范围之内。

    如果它不符合,你就打印一个错误信息并返回nil。如果它有,你就调用coinType.init(diameter:weight:),通过unknownCoin的值来创建一个新的coinType的实例。很酷,你可以像这样使用一个必要的初始化器,对吗?

你还有一个类要做! 将以下内容添加到playground的末尾。

// MARK: - Client 
// 1 
public class VendingMachine {

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

  // 3 
  public init(coinHandler: CoinHandler) { 
    self.coinHandler = coinHandler 
  }
}

以下是你所做的事情。

  1. 你为VendingMachine创建了一个新的类,它将充当客户端。

  2. 这只有两个属性:coinHandler和coins。VendingMachine不需要知道它的coinHandler实际上是一个处理程序链,而是简单地将其视为一个单一的对象。你将使用coins来保存所有有效的、被接受的硬币。

  3. 初始化器也非常简单。你只需接受一个传入的coinHandler实例。VendingMachine不需要知道CoinHandler是如何设置的,因为它只是使用它。

你还需要一个方法来实际接受硬币。在VendingMachine的closing class大括号之前添加下面的代码。

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("")
}

下面是这个的作用。

  1. 你首先尝试创建一个硬币,向coinHandler传递一个unknownCoin。如果一个有效的硬币没有被创建,你就会打印出一条信息,表明该硬币被拒绝。

  2. 如果一个有效的硬币被创建,你将打印一条成功信息并将其附加到硬币上。

  3. 然后你得到所有硬币的美元价值,并打印这个。

  4. 你最后得到所有硬币的重量并打印出来。你已经创建了一个自动售货机--但你仍然需要试一试! 将这段代码添加到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)

我们来看看这个。

  1. 在你可以实例化一个自动售货机之前,你必须先为它设置硬币处理程序对象。你可以通过为pennyHandler、nickleHandler、dimeHandler和quarterHandler创建CoinHandler的实例来实现。

  2. 然后你为处理程序挂上下一个属性。在这种情况下,pennyHandler将是第一个处理程序,其次是nickleHandler、dimeHandler,最后是quarterHandler。由于在quarterHandler之后没有任何其他处理程序,所以你把它的下一个设置为nil。

  3. 你最后通过传递pennyHandler作为coinHandler来创建vendingMachine。现在你可以在vendingMachine中插入硬币了。添加以下内容来插入一个标准硬币。

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

很好--25美分也被正确处理了! 注意到便士、五分钱和一角钱的打印语句了吗?这是预期的行为。未知的硬币在CoinHandler之间传递,直到最后,最后一个CoinHandler能够从中创建一个四分之一。

最后,添加以下内容来插入一个无效的硬币。

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}

太棒了! 自动售货机拒绝了那枚无效的硬币,这是它应该做的。

你应该注意什么?

责任链模式对那些能够非常迅速地决定是否处理一个事件的处理程序来说效果最好。要小心创建一个或多个处理程序,这些处理程序在将事件传递给下一个处理程序时很慢。

你还需要考虑如果一个事件不能被处理会怎样。你会返回nil,抛出一个错误还是做其他事情?你应该预先确定这一点,这样你就可以适当地规划你的系统。

你还应该考虑一个事件是否需要由一个以上的处理程序来处理。作为这种模式的一个变种,你可以把同一个事件转发给所有的处理程序,而不是停在第一个能处理它的处理程序上,然后返回一个响应对象的数组。

教程项目

在本章中,你将建立一个名为RWSecret的应用程序。这个应用允许用户通过尝试用户提供的几个已知密码来解密秘密信息。

你将在这个应用程序中使用两个开源库。SwiftKeychainWrapper(http://bit.ly/SwiftKeychainWrapper) 来存储iOS钥匙链中的密码,以及RNCryptor(http://bit.ly/RNCryptor) 来执行AES,即高级加密标准的解密。

如果你不熟悉iOS钥匙串或AES解密,也没关系--这些库为你做了大量的工作 你的任务将是设置一个处理程序链来执行解密。

打开Finder并导航到你为本章下载资源的地方。然后,在Xcode中打开Starter\RWSecret\RWSecret.xcworkspace(不是.xcodeproj文件)。

这个应用程序使用CocoaPods来拉入开源库。所有的东西都已经为你包含了,所以你不需要做Pod安装。你只需要使用.xcworkspace而不是.xcodeproj文件。

建立并运行。你会看到解密屏幕。

如果你点击解密,你会看到这个打印到控制台。

Decryption failed!

这到底是怎么回事?

虽然视图已经被设置为显示秘密信息,但应用程序不知道如何解密它们!在添加这个功能之前,你需要了解一下应用程序的工作原理。在你添加这个功能之前,你首先需要了解一下这个应用程序的工作原理。

打开SecretMessage.swift,你会看到这是一个简单的模型,有两个属性:encrypted 和 decrypted。

接下来,打开DecryptViewController.swift。这是在应用程序启动时显示的视图控制器。它使用一个tableView来显示SecretMessages。向下滚动到tableView(_:didSelectRowAt:),看看当一个单元格被点击时会发生什么。

特别是要寻找这一行。

secretMessage.decrypted = passwordClient.decrypt(secretMessage.encrypted)

passwordClient作为处理解密请求的客户端,但看起来,这个方法必须总是返回nil。

打开PasswordClient.swift,向下滚动到decrypt(_:),你会发现那里有一个TODO注释。啊哈!这就是你需要实现的东西。具体来说,你需要建立一个解密处理程序链来执行解密。

为此,在PasswordClient组中创建一个名为DecryptionHandlerProtocol.swift的新文件,并将其内容改为如下。

import Foundation

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

DecryptionHandlerProtocol将充当处理程序协议。它有两个要求:next用来保持下一个解密处理程序,decrypt(data:)用来执行解密。

在PasswordClient组中创建另一个名为DecryptionHandler.swift的新文件,并将其内容改为如下。

import RNCryptor

public class DecryptionHandler {

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

  public init(password: String) { 
    self.password = password 
  }
}

DecryptionHandler将充当一个具体的处理程序。它有两个属性:根据DecryptionHandlerProtocol的要求的下一个属性,和密码,以保持要使用的解密密码。

你还需要使DecryptionHandler符合DecryptionHandler协议。在前面的代码后面添加以下内容。

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 
  }
}

该方法接受加密数据并调用RNCryptor.decrypt(data:withPassword:)来尝试解密。如果它

成功,你将返回结果文本。否则,它将提供的encryptedData传递给下一个处理程序来尝试解密。

你正在取得巨大的进展 你接下来需要在客户端添加一个对DecryptionHandlerProtocol的引用。打开PasswordClient.swift,在其他属性之后添加以下属性。

private var decryptionHandler: DecryptionHandlerProtocol?

接下来,向下滚动到setupDecryptionHandler()。这个方法在两个地方被调用:在密码的didSet中,每当有新的密码被添加或删除时都会被调用;在密码从钥匙串加载后的init()中。将此方法中的TODO注释替换为以下内容。

// 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 
}

下面是如何一步步进行的。

  1. 你首先要确保密码不是空的。否则,你就把解密处理程序设置为nil。

  2. 为第一个密码创建一个DecryptionHandler,并将其设置为current和decryptionHandler。

  3. 最后,你对其余的密码进行迭代。你为每个密码创建一个DecryptionHandler,你把它设置为current.next,然后把current也更新为next。通过这种方式,你最终建立了一个DecryptionHandler对象的链。

你最后需要实现 decrypt(_:)。把它的内容替换成以下内容。

guard let data = Data(base64Encoded: base64EncodedString), 
  let value = decryptionHandler?.decrypt(data: data) else { return nil 
} 
return value

由于decrypt(_:)需要一个字符串,你首先尝试将其转换为base-64编码的数据,然后将其传递给decryptionHandler进行解密。如果成功的话,你将返回解密后的值。否则,你将返回nil。

好样的--这就解决了责任链的实现问题! 建立并运行。在第一个单元格上点击解密。然后......你还是在控制台中看到一个解密失败!?这是怎么回事?

还记得RWSecret是如何使用钥匙串来保存密码的吗?是的,你首先需要添加正确的密码。点击右上角的 "密码 "按钮。然后,在文本框中输入密码并按下添加。

同样地,为ray和raywender添加密码。

点<Decrypt,回到解密界面,然后点解密每一个单元格,就可以看到秘密信息了!

关键点

你在本章中了解了责任链模式。下面是它的关键点。

接下来该怎么做?

使用责任链模式,你创建了一个秘密信息应用程序,使用用户提供的密码对信息进行解密。你仍然有很多功能可以添加到RWSecret中。

使用你已经从本书中学到的现有模式,这些都是可能的。你可以随意地继续尝试使用RWSecret,只要你喜欢。

当你准备好了,继续下一章,学习协调者设计模式。


上一章 目录 下一章