Swift2-设计模式-全-

Swift2 设计模式(全)

原文:zh.annas-archive.org/md5/d90ca5c5521879522503c8072a25d7d7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书将帮助您理解何时以及如何使用由四人帮(GoF)描述的 23 种模式,利用苹果提供的新语言:Swift。

本书的主要思想是使其成为实现特定模式的参考书。这就是为什么我将本书分为三个类别:创建型、结构型和行为模式。对于每个类别,您都会找到一个具有共同结构的章节:角色、UML 类图、参与者、协作、说明和 Swift 实现。

这种结构是找到您可能自问的答案的简单方法。在这本书中,我将首先向您介绍五种创建型模式,接着是七种结构型模式,最后以由 GoF 定义的十一种行为模式作为总结。

本书涵盖内容

第一章, 创建型模式,为您介绍了创建型模式类别中的五种模式:原型、工厂方法、单例、抽象工厂和建造者模式。

第二章, 结构型模式 – 装饰者、代理和桥接,为您介绍了结构型模式,并帮助您探索装饰者、代理和桥接模式。

第三章, 结构型模式 – 组合和享元,教您如何使用组合和享元模式处理多个对象的架构。

第四章, 结构型模式 – 适配器和外观,教您如何利用适配器模式将原本未设计为一起工作的两种类型结合起来,然后您将学习外观模式如何帮助您简化一组复杂系统的接口。

第五章, 行为模式 – 策略、状态和模板方法,为您介绍了行为模式。在本章中,我们将讨论策略、状态和模板方法模式。

第六章, 行为模式 – 责任链和命令,为您介绍了两种其他关注将请求传递给适当对象以执行操作的行为模式。

第七章, 行为模式 – 迭代器、中介者和观察者,提供了一种在保持对象独立性和匿名性的同时实现对象间通信的方法。

第八章,行为模式 – 访问者、解释器和备忘录,以 GoF 定义的 23 个模式的发现和实现作为结尾。

您需要这本书的内容

这本书的唯一要求是在您的 Mac 计算机上安装 Xcode 7。所有提供的代码都是用 Swift 2 编写的,编译并测试过。您不需要其他软件来跟随本书中提供的示例。本书中的所有示例都是使用 OSX 命令行工具项目或游乐场文件编写的。

这本书适合谁

这本书旨在面向初学者以及想要在软件工程行业迈出下一步的高级开发者。

这本书将帮助您了解底层程序员和具有设计模式知识的程序员的区别。您可以使用 Swift 在 Xcode 中应用这些知识来设计可扩展和灵活的 iOS 和 Mac 应用或游戏。由于设计模式并非 Swift 的专属,但可以被任何其他语言使用,因此这是任何开发人员和软件架构师都必须拥有的知识。

规范

在这本书中,您将找到许多不同的文本样式,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词如下所示:“AbstractExpression”:它定义了一个对所有抽象语法树节点都通用的interpret()方法。

代码块如下所示:

class WalkMoveStrategy:IMoveStrategy{
  func performMove() {
    print("I am walking")
  }
}

当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

class WalkMoveStrategy:IMoveStrategy{
  func performMove() {
 print("I am walking")
  }
}

任何控制台输出都如下所示:

Printing checkPoint....
Level: 0   Weapon: gun   Points: 1200 
Level: 1   Weapon: tommy gun   Points: 2250 
Level: 2   Weapon: bazooka   Points: 2400 
Level: 4   Weapon: knife   Points: 3000 
Total Points: 8850

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“点击左侧的显示测试导航器按钮。”

注意

警告或重要注意事项将以这样的框显示。

小贴士

小技巧和技巧如下所示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中受益的书籍。

如要向我们发送一般性反馈,请简单地发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南,网址为 www.packtpub.com/authors

客户支持

现在,您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从您在 www.packtpub.com 的账户下载示例代码文件,适用于您购买的所有 Packt 出版的书籍。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从 [www.packtpub.com/sites/default/files/downloads/Swift 2 Design patterns_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/Swift 2 Design patterns_ColorImages.pdf) 下载此文件。

错误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中找到错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问 www.packtpub.com/submit-errata,选择您的书,点击 错误提交表单 链接,并输入您的错误详情来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误部分下的现有错误列表中。

要查看之前提交的错误,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书的名称。所需信息将出现在 错误 部分。

盗版

在互联网上,版权材料的盗版是所有媒体持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过 <copyright@packtpub.com> 与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

电子书,折扣优惠等

您知道吗,Packt 提供每本书的电子书版本,包括 PDF 和 ePub 文件。您可以在 www.PacktPub.com 升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。如有更多详情,请联系我们 <customercare@packtpub.com>

www.PacktPub.com,您还可以阅读一系列免费的技术文章,注册各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

问题

如果您在这本书的任何方面遇到问题,您可以联系我们的邮箱questions@packtpub.com,我们将尽力解决问题。

第一章:创建型模式

创建型模式旨在处理软件设计中的对象创建机制。使用这些模式后,系统变得独立于对象的创建方式,这意味着它独立于具体类的实例化方式。

这些模式封装了具体类的使用,并倾向于在对象之间的关系中使用接口,从而允许更好地抽象全局系统概念。

因此,如果我们分析单例模式,这是一个旨在仅实例化类的一个实例的模式,我们发现控制对这一实例唯一访问的机制完全封装在类中,这意味着这对消耗类实例的客户是完全透明的。

在本章中,我们将向您介绍五种创建型模式,并讨论如何使用 Swift 来应用它们:

  • 原型模式

  • 工厂方法模式

  • 单例模式

  • 抽象工厂模式

  • 构建者模式

这些模式的目标在以下表格中描述:

模式 目标
原型模式 此模式允许您通过复制称为原型的现有对象来创建新对象。此模式具有克隆能力。
工厂方法模式 此模式向您介绍一个抽象方法,允许您通过告诉其子类关于对象有效创建的信息来创建对象。
单例模式 此模式确保一个类只有一个实例。此类提供了一个唯一的访问点,返回此实例。
抽象工厂模式 此模式允许您创建一个对象,该对象通过隐藏创建这些对象所需的具体类来按家族分组。
构建者模式 此模式允许您将复杂对象的创建与其实现分离。这允许客户端创建具有不同表示的复杂对象。

原型模式

我们首先介绍的是原型模式;我们将看到如何使用它来加速实例的创建。我们将看到如何使用它来复制现有实例,最终,我们将看到如何根据我们的需求修改新实例。

角色

原型模式通过复制称为原型的现有对象来创建新对象,并且它们具有克隆能力。

此模式在以下用例中使用:

  • 当您需要创建一个实例,而不知道类的层次结构时

  • 当您需要创建动态加载的类实例时

  • 当您需要一个简单的对象系统,而不包括工厂类的并行层次结构时

设计

以下图显示了原型模式的通用类:

设计

参与者

参与者到这个模式如下:

  • Client:这个类包含了一个对象列表,这些对象是AbstractPrototype抽象类的实例。Client类需要克隆这些原型,而无需了解它们的内部结构和子类层次结构。

  • AbstractPrototype:这是一个可以自我复制的抽象类。这个类包含一个名为clone()的克隆方法。

  • ConcretePrototype1ConcretePrototype2:这些是继承自AbstractPrototype类的具体类。它们定义了一个原型,并且都有一个名为clone()的克隆方法。

协作

客户端请求一个或多个原型进行自我克隆。

插图

一个简单且真实的例子是来自Blizzard魔兽世界的创造者)的著名游戏炉石传说。在这款策略卡牌游戏中,当你使用“法力”来使用法术、武器或将随从放在场上时,有一个特殊的随从具有克隆特定卡牌的能力。当玩家使用这张卡牌时,它会选择玩家想要克隆的随从,并且这张卡牌成为所选卡牌的精确副本。以下卡片代表具有这种行为的“炉石传说”卡牌:

插图

实现

以下代码展示了使用 Swift 实现该模式的代码示例:

import UIKit

class AbstractCard {
  var name: String?
  var mana: Int?
  var attack: Int?
  var defense: Int?

  init(name:String?, mana:Int?, attack:Int?, defense:Int?) {
    self.name = name
    self.attack = attack
    self.defense = defense
    self.mana = mana
  }

  func clone() -> AbstractCard {
    return AbstractCard(name: self.name, mana: self.mana, attack: self.attack, defense: self.defense)
  }
}

class Card: AbstractCard {

  override init(name:String?, mana:Int?, attack:Int?, defense:Int? ) {
    super.init(name: name,mana: mana,attack: attack,defense: defense)

  }
}

注意

AbstractPrototype类是我们的AbstractCard类,我们在其中实现了一种使用clone()方法返回自身副本的方式。

用法

以下代码模拟了客户端如何与实现原型模式的Card对象交互:

// Simulate our client

// This is the card that we will copy
let raidLeader = Card(name: "Raid Leader", mana: 3, attack: 2, defense: 2)

// Now we use our faceless Manipulator card to clone the raidleader
let facelessManipulator = raidLeader.clone()

print("\(facelessManipulator.name, facelessManipulator.mana, facelessManipulator.attack, facelessManipulator.defense)")

由于代码是写在 Playground 文件中的,你应该将其视为你将放入Client类中的代码。

首先,我们实例化一个新的卡牌名为Raid Leader。这是一个具体原型类。假设你有一张“无面操纵者”卡牌,并且你想用它来克隆Raid Leader卡牌,那么你只需使用raidLeader.clone()方法,这将返回一个具有与Raid Leader完全相同属性的新的实例。

通过检查 Playground 文件右侧的细节,你会看到facelessManipulator常量与raidLeader(第 39 行)具有完全相同的属性,如下面的截图所示:

用法

工厂方法模式

我们的第二个模式是一个非常著名的模式。它介绍了著名的概念:“面向接口编程,而不是面向实现。”实例化是在工厂类中完成的,该类依赖于我们需要的类型以及需要返回给客户端的类型。

角色

工厂方法模式是软件设计中用得最多的模式之一。这个模式的目的在于抽象对象的创建。工厂方法允许一个类将实例化推迟到子类。

你会时不时地看到我们提到“面向接口编程”。这正是这个模式所做的事情。在 Swift 中,你将使用“协议”而不是类本身来代替接口。

此模式在以下用例中使用:

  • 一个类只知道与其有某些关系的抽象类或接口的对象

  • 一个类希望其子类实例化对象以利用多态机制

设计

以下图展示了工厂方法模式的通用类:

设计

参与者

参与该模式的参与者如下:

  • 产品接口:此类包含我们产品的定义。在这里我们将定义什么是卡片。

  • 抽象产品:此抽象类实现了我们卡片的签名和一些方法。你会看到我们保留了原型模式,这允许我们最终克隆一个卡片。这些类定义了产品的属性。

  • 具体产品:此类定义了我们的产品;在我们的例子中,Raid Leader卡片是一个具体产品,例如Faceless Manipulator卡片。

  • 具体创建者:此类实现了我们的工厂方法。

示例

在我们之前的模式中,你会看到以下行:

let raidLeader = Card(name: "Raid Leader", mana: 3, attack: 2, defense: 2)

这里,我们直接编写实现。我们需要一种创建一些卡片的方法,但不知道确切如何构建卡片;我们只能告诉创建raidLeaderFaceless Manipulator卡片。在这个时候,客户端不想知道Raid Leader卡片需要三个法力值,所以它提供了两个攻击点和两个防御点。

实现方式

工厂方法模式的具体实现如下:

import UIKit
import Foundation

//Define what a card is
protocol Card {
  var name: String? {get set}
  var attack: Int? {get set}
  var defense: Int? {get set}
  var mana: Int? {get set}
  func clone() -> Card
  func toString() -> String
}

// AbstractCard
// implements the signature and some properties
class AbstractCard: NSObject, Card {
  private var _name: String?
  private var _mana: Int?
  private var _attack: Int?
  private var _defense: Int?

  init(name: String?, mana: Int?, attack: Int?, defense: Int?) {
    self._name = name
    self._attack = attack
    self._defense = defense
    self._mana = mana
    super.init()
  }

  override init(){
    super.init()
  }

  //property name
  var name: String?{
    get{ return _name }
    set{ _name = newValue }
  }

  //property mana
  var mana: Int? {
    get{ return _mana }
    set{ _mana = newValue }
  }

  //property attack
  var attack: Int? {
    get{ return _attack }
    set{ _attack = newValue }
  }

  //property attack
  var defense: Int? {
    get{ return _defense }
    set{ _defense = newValue }
  }

  func clone() -> Card {
    return AbstractCard(name: self.name, mana: self.mana, attack: self.attack, defense: self.defense)
  }

  func toString() -> String{
    return ("\(self.name, self.mana, self.attack,self.defense)")
  }
}

enum CardType {
  case FacelessManipulator, RaidLeader
}

// our Factory Class
// Depending what we need, this class return an instance of the 
// appropriate object.
class CardFactory{
  class func createCard(cardtype: CardType) -> Card?{

    switch cardtype {
    case .FacelessManipulator:
      return FacelessManipulatorCard()
    case .RaidLeader:
      return RaidLeaderCard()
    default:
      return nil
    }
  }
}

//Concrete Card "Raid Leader"
//This is the full definition of the Raid Leader Card
class RaidLeaderCard: AbstractCard {
  override init()
  {
    super.init()
    self._mana = 3
    self._attack = 2
    self._defense = 2
    self._name = "Raid Leader"
  }
}

//Concrete Card "Faceless Manipulator"
//this is the full definition of the FacelessManipulator Card.
class FacelessManipulatorCard: AbstractCard {
  override init()
  {
    super.init()
    self._mana = 5
    self._attack = 3
    self._defense = 3
    self._name = "Faceless Manipulator"

  }
}

用法

为了模拟客户端使用工厂方法模式,我们可以将卡片创建编写如下:

//simulate our client

var c = CardFactory.createCard(.FacelessManipulator)
c?.toString()

注意

为了模拟我们的客户端,我们只需告诉CardFactory方法我们想要一个FacelessManipulator卡片。

要做到这一点,我们使用createCard方法(我们的工厂方法),此方法将委派请求的卡片的实例化。

变量c的类型是Card而不是FacelessManipulator

单例模式

这种模式无疑是每个开发者首先学习的模式。它通常与工厂或抽象工厂类一起使用,以确保只有一个类的实例。

角色

单例模式确保一个类只有一个实例,并提供一个全局访问点,此时它返回该类的实例。

在某些情况下,拥有只有一个实例的类可能很有用;例如,在抽象工厂的情况下,拥有多个实例是没有用的。

设计

以下图展示了单例模式的通用 UML 类图。使用 Swift 编写单例模式有许多方法。

这里,我们使用最简单的方法来做这件事。使用 Swift,你会发现我们可以通过类常量来改变我们应用它的方式:

设计

参与者

这个模式只有一个参与者:Singleton类。

这个类提供了一个返回类唯一实例的方法。该机制锁定其他实例的创建。它是在 Swift 1.2 中引入的。我们现在可以使用类常量。

使用 Swift 1.2,我们将使用类常量来为我们提供一种确保实例唯一创建的方法。

类常量定义如下:

static let myVariable = myObject()

协作

每个客户端都可以通过调用Instance方法来访问Singleton类的唯一实例。

使用 Swift,我们将考虑的方法是使用类常量来访问我们的Singleton类的唯一实例,我们将称之为sharedInstance

示例

您正在开发您的卡牌游戏,并且需要管理当前游戏的所有数据。在我们的游戏中,我们有两个玩家;每个玩家都有一个牌组、法力储备、名字等等。我们有一个棋盘(我们放置卡牌的桌子)和一个游戏状态(谁正在玩)。为了管理所有这些信息,您需要一个BoardManager类。这个类将是一个单例类,因为我们不会同时有几个棋盘(我们只允许同时进行一个游戏)。单例模式可以是一个有趣的概念,可以在这里使用,以确保我们访问正确的数据。

实现

以下方法支持懒初始化,并且由于let的定义,它是线程安全的:

import UIKit

class BoardGameManager {

  static let sharedInstance = BoardGameManager()
  init() {
    println("Singleton initialized");
  }

}

用法

要使用我们的单例对象,每个客户端都将使用以下代码来访问它:

let boardManager = BoardGameManager.sharedInstance

boardManager变量包含我们单例对象中所有可用的成员,并且只初始化一次。

这种模式在以下情况下使用:

  • 我们必须只有一个类的实例

  • 这个实例必须从众所周知的访问点对客户端可访问。

抽象工厂模式

我们已经向您介绍了一个在设计模式中非常流行的概念:工厂。工厂是处理相关对象实例化的类,而不需要子类化。我们已经看到的工厂方法模式隐藏了对象实例化的类名。抽象工厂模式更完整,因为它创建相关或依赖对象的系列。

角色

抽象工厂模式旨在构建分组在家族中的对象,而不需要知道创建对象所需的具体类。

这种模式通常在以下领域使用:

  • 使用产品的系统需要保持独立于这些产品如何分组和实例化

  • 一个系统可以有多个可以演化的产品系列

设计

以下图表示抽象工厂模式的通用结构。您将看到产品和家庭是如何解耦的:

设计

参与者

抽象工厂模式有很多参与者:

  • Abstract Factory:这个抽象类定义了创建我们产品的不同方法的签名。

  • ConcreteFactory1ConcreteFactory2:这些是我们实现每个产品家族方法的具体类。通过了解家族和产品,工厂能够为该家族创建一个产品实例。

  • IProductAIProductB:这些是我们定义的接口,它们定义了独立于其家族的产品。家族在其具体子类中引入。

  • ProductAProductB:这些是分别实现 IProductAIProductB 的具体类。

协作

Client 类使用一个具体工厂的一个实例,通过抽象工厂的接口创建产品。

插图

我们的公司专门从事制造手表。我们的手表由两部分组成:表带和表盘。我们的手表有两种尺寸,因此我们必须根据我们手表的尺寸调整表带和表盘的制造。

为了简化我们手表制造的管理,方向团队决定使用一家专门生产适应我们手表 38 毫米型号产品的制造商,以及另一家其产品适应我们手表 42 毫米型号的制造商。

这些制造商中的每一个都将构建适应手表尺寸的表盘和表带。

实现

为了实现我们的模式,我们首先需要确定我们的参与者。两个制造商代表 ConcreteFactory1ConcreteFactory2 类。这两个工厂实现了 AbstractFactory 方法,这告诉我们我们可以创建一个表带或表盘。当然,具体工厂将创建适应该工厂生产的表尺寸的表盘。

我们的 ConcreteProductAConcreteProductB 类是表带和表盘;每个产品都实现了它们各自的 IProductAIProductB 接口,如下面的代码所示:

import UIKit

//Our interfaces
protocol IWatchBand {
  var color: UIColor{get set}
  var size: BandSize{get set}
  var type: BandType{get set}
  init(size: BandSize)
}

protocol IWatchDial {
  var material: MaterialType{get set}
  var size: WatchSize{get set}
  init(size: WatchSize)
}

//Enums
enum MaterialType: String {
  case Aluminium = "Aluminium",
  StainlessSteel = "Stainless Steel",
  Gold = "Gold"
}

enum BandType: String {
  case Milanese = "Milanese",
  Classic = "Classic",
  Leather = "Leather",
  Modern = "Modern",
  LinkBracelet = "LinkBracelet",
  SportBand = "SportBand"
}

enum WatchSize: String {
  case _38mm = "38mm", _42mm = "42mm"
}

enum BandSize: String {
  case SM = "SM", ML = "ML"
}

//prepare our Bands components
class MilaneseBand: IWatchBand {
  var color = UIColor.yellowColor()
  var size: BandSize
  var type = BandType.Milanese
  required init(size _size: BandSize) {
    size = _size
  }
 }

class Classic: IWatchBand {
  var color = UIColor.yellowColor()
  var size: BandSize
  var type = BandType.Classic
  required init(size _size: BandSize) {
    size = _size
  }
}
class Leather:IWatchBand{
  var color = UIColor.yellowColor()
  var size:BandSize
  var type = BandType.Leather
  required init(size _size: BandSize) {
    size = _size
  }
}
class Modern: IWatchBand {
  var color = UIColor.yellowColor()
  var size: BandSize
  var type = BandType.Modern
  required init(size _size: BandSize) {
    size = _size
  }
}

class LinkBracelet: IWatchBand {
  var color = UIColor.yellowColor()
  var size: BandSize
  var type = BandType.LinkBracelet
  required init(size _size: BandSize) {
    size = _size
  }
}
class SportBand: IWatchBand {
  var color = UIColor.yellowColor()
  var size: BandSize
  var type = BandType.SportBand
  required init(size _size: BandSize) {
    size = _size
  }
}

//Dials
class AluminiumDial: IWatchDial {
  var material: MaterialType = MaterialType.Aluminium
  var size: WatchSize
  required init(size _size:WatchSize){
    size = _size
  }
}

class StainlessSteelDial: IWatchDial {
  var material: MaterialType = MaterialType.StainlessSteel
  var size: WatchSize
  required init(size _size:WatchSize){
    size = _size
  }
}

class GoldDial: IWatchDial {
  var material: MaterialType = MaterialType.Gold
  var size: WatchSize
  required init(size _size:WatchSize){
    size = _size
  }
}

//Our AbstractFactory
class WatchFactory {

  func createBand(bandType: BandType) -> IWatchBand {
    fatalError("not implemented")
  }
  func createDial(materialtype: MaterialType) -> IWatchDial{
    fatalError("not implemented")
  }

  //our static method that return the appropriated factory.
  final class func getFactory(size: WatchSize) -> WatchFactory{
    var factory: WatchFactory?
    switch(size){
    case ._38mm:
      factory = Watch38mmFactory()
    case ._42mm:
      factory = Watch42mmFactory()
    }
    return factory!
  }

}

// Concrete Factory 1 for 42 mm
class Watch42mmFactory: WatchFactory {
  override func createBand(bandType: BandType) -> IWatchBand {
    switch bandType {
    case .Milanese:
      return MilaneseBand(size: .ML)
    case .Classic:
      return Classic(size: .ML)
    case .Leather:
      return Leather(size: .ML)
    case .LinkBracelet:
      return LinkBracelet(size: .ML)
    case .Modern:
      return Modern(size: .ML)
    case .SportBand:
      return SportBand(size: .ML)
    default:
      return SportBand(size: .ML)
    }
  }

  override func createDial(materialtype: MaterialType) -> IWatchDial {
    switch materialtype{
    case MaterialType.Gold:
      return GoldDial(size: ._42mm)
    case MaterialType.StainlessSteel:
      return StainlessSteelDial(size: ._42mm)
    case MaterialType.Aluminium:
      return AluminiumDial(size: ._42mm)
    }
  }
}

//Concrete Factory 2 for 38mm
class Watch38mmFactory: WatchFactory{
  override func createBand(bandType:BandType) -> IWatchBand {
    switch bandType {
    case .Milanese:
      return MilaneseBand(size: .SM)
    case .Classic:
      return Classic(size: .SM)
    case .Leather:
      return Leather(size: .SM)
    case .LinkBracelet:
      return LinkBracelet(size: .SM)
    case .Modern:
      return Modern(size: .SM)
    case .SportBand:
      return SportBand(size: .SM)
    default:
      return SportBand(size: .SM)
    }
  }

  override func createDial(materialtype: MaterialType) -> IWatchDial {
    switch materialtype{
    case MaterialType.Gold:
      return GoldDial(size: ._38mm)
    case MaterialType.Gold:
      return StainlessSteelDial(size: ._38mm)
    case MaterialType.Gold:
      return AluminiumDial(size: ._38mm)
    default:
      return AluminiumDial(size: ._38mm)

    }
  }
}

使用方法

为了模拟我们的客户端,我们将使用以下代码:

//Here we deliver products from the Manufacture 1 specialized in
//products for the 38 mm Watch
let manufacture1 = WatchFactory.getFactory(WatchSize._38mm)
let productA = manufacture1.createBand(BandType.Milanese)
productA.color
productA.size.rawValue
productA.type.rawValue

let productB = manufacture1.createDial(MaterialType.Gold)
productB.material.rawValue
productB.size.rawValue

//Here we delivers products from the Manufacture 2 specialized in
//products for the 42 mm Watch
let manufacture2 = WatchFactory.getFactory(WatchSize._42mm)
let productC = manufacture2.createBand(BandType.LinkBracelet)
productC.color
productC.size.rawValue
productC.type.rawValue

let productD = manufacture2.createDial(MaterialType.Gold)
productD.material.rawValue
productD.size.rawValue

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户下载所有已购买的 Packt 出版物的示例代码文件。如果您在其他地方购买了此书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

操场文件将显示我们的产品属性,具体取决于使用的工厂。以下截图显示了 manufacture1 对象的产品 A(表带)和产品 B(表盘)的详细信息:

Usage

以下截图显示了 manufacture2 对象的产品 C(表带)和产品 D(表盘)的详细信息:

Usage

表带和表盘的尺寸将适应提供产品的制造商。

注意

我们应该使用单例模式来确保我们只有一个抽象工厂的实例。这个实例可以在多个客户端之间共享。

构建模式

与会生产同一家族产品部分的抽象工厂模式不同,构建模式将帮助我们构建由多个部分组成的最终产品。

角色

构建模式的主要目的是将复杂对象的构建过程与其实际构建过程抽象化。具有相同的构建过程可以创建不同表示的产品。

此模式可用于以下情况:

  • 客户需要构建复杂对象,而不必了解其实现

  • 客户需要构建需要具有多个实现或表示的复杂对象

设计

以下图展示了构建模式的通用 UML 类图:

设计

参与者

此模式相当简单,因为它只有少数参与者:

  • Director:此类使用AbstractBuilder类的接口构建产品。

  • AbstractBuilder:此类定义了构建产品所有部分的签名方法,并包含一个返回构建完成后的产品的签名方法。

  • ConcreteBuilder:这是实现AbstractBuilder类方法的Concrete类。

  • Product:这是最终产品。产品包含手表的所有部分。

协作

客户创建ConcreteBuilderDirector类。然后,如果客户要求,Director类将通过调用构造函数构建一个对象,并将最终产品返回给客户。

示例

使用AbstractFactory方法,我们可以使用构建模式来构建手表。正如我们所见,手表有几个部分:表盘和表带。手表还可以有两种尺寸,正如我们之前所看到的,表盘或表带的表示也取决于手表的尺寸。

实现

如果我们想要构建一些由表盘和表带表示的表,我们将定义一个Director类,该类将定义我们手表所有部分的构建顺序,并将最终完成的手表返回给客户。

Director类将调用所有负责构建手表一个部分的构造函数。为了实现这一点,我们将重用抽象工厂模式的现有代码,并添加以下代码。

在 Xcode 中打开Builder.playground文件,查看文件底部的添加代码:

//Our builder1
class BuilderGoldMilanese38mmWatch: AbstractWatchBuilder {
  override func buildDial() {
    watch.band = MilaneseBand(size: BandSize.SM)
  }
  override func buildBand() {
    watch.dial = GoldDial(size: WatchSize._38mm)
  }
}

//Our builder2
class BuilderAluminiumSportand42mmWatch:AbstractWatchBuilder {
  override func buildDial() {
    watch.band = SportBand(size: BandSize.ML)
  }
  override func buildBand() {
    watch.dial = AluminiumDial(size: WatchSize._42mm)
  }
}

//our Director class
class Director {
  var builder: AbstractWatchBuilder?
  init(){

  }

  func buildWatch(builder: AbstractWatchBuilder){
    builder.buildBand()
    builder.buildDial()
  }
}

用法

为了模拟我们的客户,我们将告诉我们的导演创建两块手表:

  • 一款直径 42 毫米的铝合金表盘配运动表带

  • 一款直径 38 毫米的金色表盘配米兰尼斯表带

示例代码如下:

//We will build 2 Watches :
//First is the Aluminium Dial of 42mm with Sport Band
let director = Director()
var b1 = BuilderAluminiumSportand42mmWatch()
director.buildWatch(b1)

// our watch 1
var w1 = b1.getResult()
w1.band?.color
w1.band?.type.rawValue
w1.band?.size.rawValue
w1.dial?.size.rawValue
w1.dial?.material.rawValue

//Our 2nd watch is a Gold 38mm Dial with Milanese Band
var b2 = BuilderGoldMilanese38mmWatch ()
director.buildWatch(b2)

// Our watch 1
var w2 = b2.getResult()
w2.band?.color
w2.band?.type.rawValue
w2.band?.size.rawValue
w2.dial?.size.rawValue
w2.dial?.material.rawValue

结果在 Playground 中显示如下:

用法

注意

Swift 允许使用闭包来简化我们复杂对象的创建。关于我们之前提供的示例,我们可以编写以下代码来构建我们的两个手表。

使用闭包的实现

在这里,我们不需要使用DirectorConcreteBuilder类。相反,我们将告诉我们的Watch类,构建器将在闭包中。

在前面的示例中,移除DirectorAbstractBuilderConcreteBuilder类。

我们只需要编写如下的Watch类,如下代码所示(你可以在伴随本章的BuilderClosures.playground文件中找到以下代码):

//our Product Class : a Watch
//The builder will be in the closure
class Watch{
  var dial:IWatchDial?
  var band:IWatchBand?
  typealias buildWatchClosure = (Watch) -> Void

  init(build:buildWatchClosure){
    build(self)
  }
}

然后,为了模拟我们的客户端,我们可以编写以下代码,该代码将调用分配给Watch对象带或表盘属性的适当构造函数:

//Simulate our clients

let Gold42mmMilaneseWatch = Watch(build: {
  $0.band = MilaneseBand(size: BandSize.ML)
  $0.dial = GoldDial(size: WatchSize._42mm)
})

结果如下:

使用闭包的实现

概述

好吧,我希望这一章对使用 Swift 的模式进行了良好的介绍。我们学习了五种创建型模式:原型模式、工厂方法模式、单例模式、抽象工厂模式和建造者模式。我们还学习了何时使用它们以及如何实现它们。

在下一章中,我们将向您介绍三种旨在简化实体之间关系的结构模式。

第二章:结构型模式 - 装饰器、代理和桥接

在上一章回顾了五种创建型模式之后,我们现在将讨论另一类模式:结构型模式。有七个模式要讨论;这些模式通过识别实体之间关系实现的一种简单方式来简化设计。

我们将看到这些模式如何通过使用接口来封装对象的组合,从而方便地抽象你的系统,就像创建型模式封装对象的创建一样。结构型模式强调了接口的使用。

你将看到组合是如何设计的;我们不会干扰对象本身,而是会干扰那个将传递结构化的对象。这个第二个对象与第一个对象有很强的关联。确实,第一个对象向客户端展示接口并管理它与第二个对象的关系,而第二个对象管理组合,并且不与客户端有任何接口。

需要注意的一个重要事项是,这种结构化通过允许动态修改组合,为你的系统提供了极大的灵活性。确实,如果两个对象继承自相同的类并使用相同的接口,我们可以用另一个对象替换它。

静态和动态组合

我们可以有几种可能的实现。设计这种模式的一种经典方法是区分这些实现,在子类中。在这种情况下,我们将提供一个接口,我们的类将实现这个接口。

这种解决方案由静态组合组成。确实,一旦选择了对象的实现类,我们就不能再更改它。下面的图展示了通过继承实现对象的实现:

静态和动态组合

另一种方法是将在另一个对象中分离实现。实现部分由ConcreteImplementationA类的实例或ConcreteImplementationB类管理。这个引用通过implementation属性进行引用。这个实例可以在运行时轻松地被另一个实例所替代。这种组合是动态的。

下面的 UML 类图清楚地展示了如何使用动态组合来结构化你的对象。ConcreteImplementation类可以在运行时切换,而不需要修改Realization对象。

我们最终可以修改实现对象,而不需要修改原始对象,如下面的图所示:

静态和动态组合

在本章中,你将看到如何使用桥接模式来使用这个解决方案。

结构型模式的研究将跨越三章。在本章中,我们将介绍其中的三个:

  • 装饰器模式

  • 代理模式

  • 桥接模式

这三种模式提供了一种动态添加状态和行为的机制,控制对象的创建和访问,并保持规范和实现分离。

本章我们将看到的三个结构模式的目的是在以下表格中描述:

模式 目标
装饰者模式 这种模式允许您动态地向对象添加新的行为和功能。
代理模式 这种模式是另一个对象的替代品。它提供了一种可以适应优化或安全需求的行为。
桥接模式 这种模式解耦了抽象与其实现,使它们能够独立变化。

装饰者模式

我们将要讨论的第一个结构模式是装饰者模式。它通过添加新的功能或行为来介绍对象替代。

角色

这种模式的主要目标是动态地向对象添加新的功能。对象的接口将不会修改,因此从客户端的角度来看,这是完全透明的。

这种模式是向父类添加功能的子类添加的替代品。装饰者模式的一个关键实现点是装饰者既继承原始类,又包含其实例。

这种模式可以在以下情况下使用:

  • 一个系统可以动态地向对象添加新的功能,而不必修改其接口,这意味着不必修改该对象的客户端

  • 一个系统管理可以动态移除的行为

  • 由于已经有一个复杂的类层次结构,使用继承不是一个好的选择

设计

装饰者模式的通用 UML 类图相当简单:ConcreteComponentAbstractDecorator 类共享相同的接口,具有相同的方法名。我们的 AbstractDecorator 类定义了一个构造函数,其中我们传递我们的 Abstractcomponent 类作为参数。然后,在我们的 ConcreteDecorator 类中,我们将操作调用重定向到 additionalOperation 方法,以向原始组件添加新的功能或行为,如图所示:

设计

参与者

在前面的图中,这个模式中有四个参与者:

  • AbstractComponent:这是组件和装饰器的公共接口。

  • ConcreteComponent:这是我们想要添加行为和/或功能的主要对象。

  • AbstractDecorator:这个抽象类包含对组件的引用。

  • ConcreteDecoratorAConcreteDecoratorB:这些是 AbstractDecorator 的具体子类。这些类实现了添加到组件的功能。

协作

当装饰者收到必须到达组件的消息时,它会通过先前的或后续操作将该消息重定向。

样例

为了说明这个模式,我们将举一个简单的例子。假设你有一个绘图软件,它允许你在屏幕上绘制一些形状:一个矩形和一个正方形。

你已经知道如何绘制这些形状。现在,你需要添加一个新功能,该功能将为你的形状添加一个圆角。为此,你需要决定使用哪种装饰器模式,这将允许你不对现有类方法签名进行干扰。

实现

首先,我们将创建我们的界面,它定义了形状。我们将模拟一个 Draw() 操作。实际上,该方法将返回一个字符串,告诉我们绘制了什么:

protocol IShape {
  func draw() -> String
}

现在,我们将创建我们的两个具体类,它们实现了 IShape 接口。我们将有 SquareRectangle 类。它们都实现了 draw 函数。此函数返回当前绘制的形状:

class Square: IShape {
  func draw() -> String{
    return "drawing Shape: Square"
  }
}

class Rectangle: IShape {
  func draw() -> String {
    return "drawing Shape: Rectangle"
  }
}

我们已经准备好了类;现在,我们准备我们的抽象 ShapeDecorator 类,该类定义了我们未来具体装饰器的结构。这个类也实现了 IShape 接口,因此 Draw() 函数必须存在。然而,Swift 没有抽象类,所以我们实现了 draw 方法,但强制抛出异常来告诉我们必须实现此方法。ShapeDecorator 类本身不会被客户端使用。客户端将调用 ConcreteDecorator 对象来为其形状添加新功能:

class ShapeDecorator: IShape {
  private let decoratedShape: IShape

  required init(decoratedShape: IShape){
    self.decoratedShape = decoratedShape
  }

   func draw() -> String {
    fatalError("Not Implemented")
  }
}

现在,我们添加我们的具体装饰器类,该类继承自 ShapeDecorator 抽象类。我们将新的 setRoundedCornerShape 功能添加到这个类中,并重写 draw 函数以返回绘制的形状,但具有圆角:

class RoundedCornerShapeDecorator: ShapeDecorator{
  required init(decoratedShape: IShape) {
       super.init(decoratedShape: decoratedShape)
  }

  override func draw() ->String{
    //we concatenate our shape properties
     return  decoratedShape.draw() + "," + setRoundedCornerShape(decoratedShape)
  }

  func setRoundedCornerShape(decoratedShape: IShape) -> String{
    return "Corners are rounded"
  }
}

使用

现在,这是容易的部分,它展示了从客户端的角度如何使用我们已编写的所有代码。

我们首先创建我们的两个具体形状:

let rectangle = Rectangle()
let square = Square()

现在,我们想要一些具有圆角的形状。为此,我们只需调用我们感兴趣的 ConcreteDecorator 类,即 RoundedCornerShapeDecorator 类,并将新的形状(RectangleSquare)作为构造函数的参数传递:

let roundedRectangle = RoundedCornerShapeDecorator(decoratedShape: Rectangle())

let roundedSquare = RoundedCornerShapeDecorator(decoratedShape: Square())

现在,我们通过调用 draw 操作来模拟我们的形状屏幕上的 Draw() 方法:

print("rectangle with Normal Angles")
rectangle.draw()

print("square with Normal Angles")
square.draw()

//rounded corners shapes
roundedRectangle.draw()
roundedSquare.draw()

操场将返回以下结果:

使用

注意

Swift 允许你使用扩展的概念来实现装饰器模式。这允许你向具体类或结构添加额外的方法,而无需子类化或修改原始类。与子类不同,使用扩展可以添加新方法但不能添加新属性。

代理模式

本章我们将讨论的第二种模式是代理模式。它通常用于安全或优化目的。

角色

代理模式的目标是用另一个对象(主题)替换对象,该对象将控制其访问。替代主题的对象共享相同的接口,因此对消费者来说是透明的。代理通常是一个小的(公共)对象,代表一个更复杂的(私有)对象,一旦某些情况明确,就会激活。代理通过接受来自客户端对象的请求并将它们传递给真实主题作为必要的方式,增加了一层间接性。

代理模式用于面向对象编程。有几种类型的代理,如下所示:

  • 虚拟代理:这允许你在适当的时候创建一个“大”对象(用于创建过程缓慢时)

  • 远程代理:这允许你访问另一个环境(如多人游戏服务器)上可用的对象

  • 认证代理:这检查请求的访问权限是否正确

设计

以下类图相当简单;我们有一个定义我们的主题(代理和RealSubject)的接口。

客户端将调用代理,而不是直接调用RealSubject对象。代理包含对RealSubject对象的引用。当代理收到请求时,它可以分析它,如果认为请求是有效的,它可以将其重新路由到RealSubject.request()方法。代理可以决定何时创建或不需要创建RealSubject对象,以避免在内存中管理无用的过大对象。以下图表示代理模式的通用类图:

设计

参与者

这个模式中只有三个参与者:

  • ISubject:这是ProxyRealSubject对象的公共接口

  • RealSubject: 这是被代理控制并操作的对象。

  • Proxy:这是替代RealSubject的对象。它具有与RealSubject对象相同的接口(ISubject接口)。它创建、控制、增强和验证对RealSubject对象的访问。

协作

代理接收来自客户端的传入请求,而不是RealSubject。如果需要,消息随后被委托给RealSubject对象。在这种情况下,在委托之前,如果尚未完成,代理会创建RealSubject对象。

插图

我们正在开发一个新的软件;这个软件以列表的形式展示视频目录。对于列表中的每个视频,我们都有一个视频占位符和描述。视频的占位符首先显示视频的截图。如果我们点击这张图片,视频将被启动。

视频目录包含视频,因此如果将这些视频全部保存在内存中,将会很重,而且通过网络传输这些视频将花费很长时间。代理模式将帮助我们组织所有这些。我们将在需要时创建主题,一旦点击截图。

两个优点如下:

  • 列表加载迅速,主要是在从网络下载时

  • 只创建、加载和播放我们想要观看的视频

代表视频的Screenshot类被称为Video主题的代理。代理代替显示Video主题。Screenshot类实现了与Video主题相同的接口(即RealSubject对象)。

在我们的例子中,代理模式设计如下:

插图

当代理接收到display()消息时,如果该视频已存在,它将显示视频。如果它接收到click()消息,它将首先创建Video主题并加载视频。

实现

我们将首先定义我们的代理和真实主题将使用的接口。

当我们使用 Playground 模拟这些方法的真实行为时,这些方法返回一个字符串,告诉我们代码预期要做什么。我们可以通过检查 Playground 返回的消息来确保我们编写的代码是正确的。

接口将只有两种方法:click()display()

protocol IAnimation{
  func display() -> String
  func click() -> String
}

这里用视频类表示RealSubject对象。我们实现接口并根据动作显示消息:

class Video:IAnimation{
  func click() -> String{
    return ""
  }

  func display()->String{
    return "Display the video"
  }

  func load()->String{
    return "Loading the video"
  }

  func play()->String{
    return "Playing the video"
  }
}

代理现在实现了与RealSubject对象相同的接口:IAnimation接口,但具有在click方法中需要时创建RealSubject对象(这里指video对象)的智能:

class ScreenShot:IAnimation{
  var video:Video?

  func click() -> String {
    if let video = video {
      return video.play()
    } else {
      video = Video()
      return video!.load()
    }
  }

  func display() -> String {
    if let video = video {
      return video.display()
    } else {
      return "Display the screenshot of the video"
    }
  }
}

使用方法

最酷的部分是模拟客户端。

我们首先创建一个新的代理Screenshot,然后模拟操作。我们从代理调用display。由于视频尚未创建或加载,所以将显示截图。

然后,我们模拟一个点击。我们可以看到当我们调用click方法时,视频被加载。随着视频的创建和加载,我们调用display方法,它通知我们视频现在正在播放(而不是视频的截图):

var animation = ScreenShot()
animation.display()
animation.click()
animation.display()

在 Playground 中的结果是:

使用方法

注意

当你有以下对象时使用代理模式:

  • 创建成本高昂

  • 需要访问控制

  • 访问远程站点

  • 需要在访问时执行某些操作

此外,当你想要:

  • 只有在请求操作时才创建对象

  • 在访问对象时执行检查或维护工作

  • 拥有一个本地对象,该对象将引用远程对象

  • 在请求对象操作时对对象实施访问权限

桥接模式

记住,在章节开头,我们讨论了动态组合,它允许你在运行时更改对象的实现。桥接模式是另一种允许这种操作的构造型模式。

角色

桥接模式解耦了抽象与其实现。这意味着这个模式将对象的实现与其表示和接口分离。

因此,首先,实现可以完全封装,其次,实现和表示可以独立改变,而它们之间没有任何约束。

此模式可以用作:

  • 避免对象表示与其实现之间的强链接

  • 避免在对象及其客户端之间的交互受到对象实现修改的影响

  • 为了允许通过创建新的子类来保持对象及其实现的可扩展性

  • 为了避免获得非常复杂的类层次结构

设计

在桥接模式的类图中,抽象和实现之间的分离得到了很好的体现——注意以下图示的左侧和右侧:

设计

参与者

桥接模式使用最少的四个参与者:

  • AbstractClass 代表领域对象。这个类包含客户端使用的接口,并包含一个实现 Implementation 接口的对象的引用。

  • ConcreteClass 是实现 AbstractClass 中定义的方法的类。

  • ImplementationBase 类定义了具体实现类的函数签名。这里定义的方法与 Abstract 类的方法不同。这两组方法不同。通常,AbstractClass 的方法是高级方法,而 implementation 类的方法是低级方法。

  • ConcreteImplementationA (B …) 类是具体类,它们实现了在 ImplementationBase 接口中引入的方法。

小贴士

ImplementationBase 接口代表桥。

协作

AbstractClass 及其子类操作调用 ImplementationBase 接口中定义的方法,这些方法代表桥。

描述

我们应该能够使用同一个对象来打开灯或电视。一旦我的代码实现了打开灯或电视,如果 ConcreteImplementation 结构发生变化,我就不需要修改它。使用桥接模式,我将使用一个继承自 AbstractClass 的对象。这个对象包含一个客户端将使用的方法。这个方法不会打开电视,但它调用在 ImplementationBase 类中定义的方法;因此,根据我们的抽象对象使用的对象,它将运行在 ConcreteImplementation 类中定义的操作,这些操作由电视或灯表示。

实现

给定前面的问题,我们首先定义一个方法和一个包含我们想要操作的对象的属性。这个对象将实现ImplementationBase接口,它代表桥接。

客户端将要操作的对象将有一个turnOn()方法。这是客户端所知的唯一方法:

// IAbstractBridge
protocol IAbstractBridge {
  var concreteImpl: ImplementationBase {get set}
  func turnOn()
}

现在,我们将定义ImplementationBase接口。它包含每个ConcreteImplementation类将实现的run()方法:

//Bridge
protocol ImplementationBase {
  func run()
}

我们现在可以创建客户端将使用的RemoteControl类;根据concreteImpl属性中引用的对象,turnOn()方法将调用concreteImpl对象的 run 方法。为了获取concreteImpl对象的引用,我们将在RemoteControl类的构造函数(init)中添加一个参数:

/* Concrete Abstraction */
class RemoteControl: IAbstractBridge {
  var concreteImpl: ImplementationBase

  func turnOn() {
    self.concreteImpl.run()
  }

  init(impl: ImplementationBase) {
    self.concreteImpl = impl
  }
}

最后,我们为TVLight类实现我们的ImplementationBase类。每个类都需要一个run()方法。run()方法包含所有需要的逻辑,这将允许你打开灯或电视。在我们的例子中,我们只显示一个指示操作已完成的文本:

/* Implementation Classes 1 */
class TV: ImplementationBase {
  func run() {
    println("tv turned on");
  }
}

/* Implementation Classes 2 */
class Light: ImplementationBase {
  func run() {
    println("light turned on")
  }
}

使用

从客户端的角度来看,我们将使用我们的RemoteControl抽象类,在我们想要操作(LightTV类)时将最终对象传递给构造函数,并调用RemoteControl对象的turnOn()方法来执行操作:

let tvRemoteControl = RemoteControl(impl: TV())
tvRemoteControl.turnOn()

let lightRemoteControl = RemoteControl(impl: Light())
lightRemoteControl.turnOn()

多亏了 Playground,我们现在可以看到实时结果,如下所示:

使用

我们可以在 Playground 文件的右侧看到两条消息:电视已打开灯已打开,这意味着每个最终对象的run()方法已经正确执行。

摘要

本章我们讨论了三种结构模式:装饰者模式、代理模式和桥接模式。从高层次来看,它们都帮助你在不使用继承的情况下扩展类,而是使用其类层次结构的动态组合。

扩展我们的原始类对我们的原始对象有一些影响,除了代理模式外,它保持完全不变。需要设计的装饰者模式需要原始类已经开发,因为每个具体的装饰者都需要根据原始对象结构实现一个接口。桥接模式更紧密地耦合,有一种理解是原始对象必须包含对整个系统的大量引用。

我们还讨论了所有依赖于重路由操作的模式。我们了解到重路由总是从新代码回溯到原始代码。

重要的是要注意,在需要性能的实时应用中,重路由操作所需的时间开销可能不可接受。

在下一章中,我们将继续我们的结构模式探索之旅,探讨组合模式和享元模式,这些模式可以应用于拥有大量数据对象的系统。

第三章:结构模式 - 组合和享元

我们已经看到了三种结构模式:装饰者模式、代理模式和桥接模式,它们为我们提供了动态添加状态和行为、控制对象的创建和访问以及保持规范和实现分离的方法。现在,本章将重点关注组合模式和享元模式,这些模式旨在简化对一组对象或大量小对象的操作。组合模式经常被使用,我们还可以利用享元模式。

享元模式通过帮助您减少在许多值重复时的内存消耗或存储需求,有效地共享小对象中存在的公共信息。

在本章中,我们将讨论以下主题:

  • 组合模式

  • 享元模式

以下表格描述了这两种新结构模式的目标:

模式 目标
组合模式 此模式允许您将对象组合成树结构,并将对象组视为对象的一个实例。
享元模式 此模式允许您通过即时实例化来管理大量对象,从而有效地提高性能。

组合模式

此模式经常用于操作一组对象。Swift,像许多其他语言一样,已经在其内部结构中使用了组合模式。例如,在cocoa框架中可用的UIView类,它定义了应用布局的通用行为。然后,视图层次结构中的单个视图对象可以是叶节点(如标签)或具有其他视图集合的复合(如表视图控制器)。

角色

此模式允许您通过提供对象的分层结构来以相同的方式处理单个组件和一组组件。它允许您以树的形式构建对象结构,其中包含对象的组合和作为节点的单个对象。

使用此模式,我们可以创建复杂的树,并将它们作为一个整体或部分来处理。操作也可以应用于整体或部分。

我们通常在Composite类中找到addremovedisplayfindgroup操作。

当以下情况发生时,可以使用此模式:

  • 在系统中必须有组合层次结构

  • 如果客户端正在处理组合对象,则需要忽略它们。

设计

以下图表示了通用的 UML 类图:

设计

参与者

该模式的参与者如下:

  • Component:这是一个抽象类,它引入了对象的组合接口,实现了常用方法,并定义了管理组件添加或删除的方法签名。

  • Leaf:这是一个具体类,它定义了组成中元素的行为。它实现了 Composite 类支持的运算。Leaf 类没有自己的组件。

  • Composite:这是一个具体类,它定义了具有子组件并存储子组件的组件的行为。它实现了与 Leaf 类相关的操作。这个类聚合了 Component 类。

  • Client:这个类使用组件的接口来操作组成中的对象。

注意

Composite 包含组件。组件可以是 LeafComposite。这确实是递归的。一个复合体包含一组子组件;这些子组件可能是其他复合体或叶元素。

协作

客户端通过 Component 接口向叶组件发送请求。

当一个组件收到一个请求时,它会根据其类来反应。如果组件是叶组件,那么它将自行处理请求。

如果组件是复合体,它将首先处理自身,然后向每个子组件发送消息,这些子组件接着也会执行处理。然后,当每个子组件完成处理后,复合体将执行最后的处理。

插图

我们公司有一个在线的 视频点播VOD)目录。我们所有的电影都按类型分类。由于这是一个按次付费的系统,我们每个视频都将有一个价格、名称和简短描述。

现在,我们希望通过这个新模式轻松地操作我们完整目录的显示。以下图表示了目录的组织结构:

插图

实现

因此,现在是时候将组合模式的通用设计应用到我们的案例中。首先,我们将根据我们的场景重新设计我们的模式,以便了解我们需要做什么,如下面的图所示:

实现

VODManager 类将使用 VODComponent 接口来访问 VOD 类别和 VOD 项目。VODComponent 类是我们提供的抽象类,它将提供定义方法的默认实现。VODItem 类将只覆盖有意义的方 法。VODCategory 类也将覆盖有意义的方 法,包括添加新的 VODItemVODCategory 对象的方式。在重新组织我们的模式后,我们现在可以开始实现我们的解决方案。

VODComponent 的实现

首先,我们创建我们的抽象类,VODItemVODCategory都将从这个类继承。这个类将为叶子节点和组合节点提供接口。Swift 不支持抽象类;然而,没有什么应该阻止我们将默认行为应用到我们的方法上,比如使用assert来通知一个方法不被支持。assert 将只是通知我们,如果它在不适用的类中使用,那么该方法将不被支持。我们可以像这样编写我们的“假”抽象类:

// Abstract Class

class VODComponent {

  func add(vodComponent: VODComponent) {
    assert(false, "This method is not supported")
  }

  func remove(vodComponent: VODComponent) {
    assert(false, "This method is not supported")
  }

  func getName() -> String {
    assert(false, "This method is not supported")
  }

  func getDescription() -> String {
    assert(false, "This method is not supported")
  }

  func getPrice() -> Double {
    assert(false, "This method is not supported")
  }

  func getChild(i:Int) -> VODComponent {
    assert(false, "This method is not supported")
  }

  func display() {
    assert(false, "This method is not supported")
  }
}

VODItem 叶子节点的实现

我们组件类已经准备好了;每个方法都有一个默认行为。我们现在可以实现我们的VODItem类。它是在组合图中的一个叶子类,实现了组合中元素的行为:

class VODItem: VODComponent {
  private var name: String!
  Private var description: String!
  private var price: Double!

  init(name:String!, description:String!, price:Double!){
    self.name = name
    self.description = description
    self.price = price
  }

  override func getName() -> String {
    return name!
  }

  override func getDescription() -> String {
    return description!
  }

  override func getPrice() -> Double {
    return price!
  }

  override func display() {
    print(" \(name!), \(price!),  ----  \(description!)")
  }
}

看看我是如何使用!定义私有变量的。这意味着这些变量的值在初始化后不能为 nil。这是因为我们添加了一个构造函数(init方法),其中所有我们的参数都必须传递以初始化我们的私有字段。

然后,我们只重写这个类中我们感兴趣的方法。Add/RemoveGetChild方法在这里不需要重写;这将在组合类中完成。

为了确保每个名称、描述和价格都有一个值,我们添加了一个感叹号来解包它。

现在是时候实现我们的组合类别类了。我们将把这个组合类命名为:VODCategory。它将包含VODItemsVODCategory。你会看到,我们在这里只重写这个类中我们感兴趣的方法。getPrice()方法不会引起我们的兴趣,因为它没有意义。

VODCategory 组合的实现

VodCategory类的第一个版本可以写成如下:

class VODCategory: VODComponent{
  var vodComponents = [VODComponent]()
  private var name: String!
  private var description: String!

  init(name:String!, description:String!) {
    self.name = name
    self.description = description
  }

  override func add(vodComponent: VODComponent) {
    vodComponents.append(vodComponent)
  }

  override func remove(vodComponent: VODComponent) {
    vodComponents.remove(vodComponent)
  }

  override func getChild(i:Int) -> VODComponent {
    return vodComponents[i]
  }

  override func getName() -> String {
    return name!
  }

  override func getDescription() -> String {
    return description!
  }

  override func display() {
    print(" \(name!),  \(description!) \r\n ----------------")
  }
}

好吧,就像VODComponent一样,我们重写了我们感兴趣的方法。由于我们可以有任意数量的VODComponent,我们添加了一个类型为VODComponent的数组来保存它们。

我们添加了AddRemoveGetChild方法。Add方法将允许我们添加一个项目、类别或子类别。我们也可以删除它,并基于其索引返回一个VODComponent类。

再次,我们可以给我们的组合添加一个名称和描述,当调用display()方法时将显示出来。

让我们看看以下代码:

  override func remove(vodComponent: VODComponent) {
    vodComponents.remove(vodComponent)
  }

如果你像前面的代码那样编写,你会得到一个错误,因为 Swift 没有为Array类型提供remove方法。

是时候对我们的实现做一些修改,并介绍扩展的使用了。

我们的问题是,我们想要能够通过一个名为remove(或任何其他名称)的方法从我们的vodComponents数组中移除一个VODComponent类型的对象,其中我们传递一个表示我们想要从列表中移除的对象的实例。

如果我们在自动完成时检查可用的方法,我们看不到任何可以帮助我们实现这个目的的方法,如下面的截图所示:

VODCategory 组合的实现

在第二章结构型模式 – 装饰器、代理和桥接中,你可以使用装饰器模式将此类方法添加到Array类型;你也可以提议简单地添加一个方法到类中,该方法将通过逐个比较来测试数组中的所有元素,如果它们相同,则从列表中删除它们。

我们想要的是可以重用和通用的东西。为此,Swift 有extension。这允许你非常容易地向类添加行为。让我们通过向Array类型添加remove方法来实现这一点。

扩展不能添加到类中。实际上,扩展是全局的。如果你要将扩展添加到 OS X 或 iOS 项目中,你通常会将其添加到一个专门的 Swift 文件中。

这里是我们的扩展:

extension Array {
  mutating func remove <T: Equatable> (object: T) {
    for i in (self.count-1).stride(through: 0, by: -1) {
      if let element = self[i] as? T {
        if element == object {
          self.removeAtIndex(i)
        }
      }
    }
  }
}

由于这个函数修改了Array类型及其属性,我们将此函数标记为可变。然后,我们从列表的末尾开始,比较我们想要在列表当前元素中找到的元素:

      if let element = self[i] as? T {
        if element == object {
          self.removeAtIndex(i)
        }
      }

再次,这里有一些棘手的事情要做,以使这段代码没有任何错误。这个函数告诉我们,我们想要T类型的Array实现Equatable(用remove <T: Equatable>表示)以便能够进行比较:

        if element == object {

因此,我们需要修改我们的抽象类,说明我们的类实现了Equatable协议:

class VODComponent : Equatable {

  func add(vodComponent:VODComponent){
    assert(false, "This method is not supported")
  }

当然,添加这个协议会稍微修改我们的类图,但没关系。通过实现这个协议,我们是在说VODComponent类型的元素可以使用==!=进行比较。

如果这还没有完成,那么我们需要实现这个协议,所以最好的方法是在任何类之外创建一个全局函数:

// GLOBAL Func
func ==(left: VODComponent, right: VODComponent) -> Bool {
  return left === right
}

注意

===运算符告诉我们两个组件的实例是否相同。

因此,现在,这个类的所有代码都已经编写完成;remove方法可用且工作完美。

我们需要对我们的类进行一些改进,以完全完成组合的实现。

你看到我们如何在组合(VODCategory)中实现了display方法了吗?确实,组合的display()方法只显示它自己的信息,但它必须调用组合中每个元素的display()方法。为此,我们将简单地添加一小段代码,通过调用它们各自的display()方法来迭代组合中包含的所有元素。

让我们改变VODCategory类的display()方法,为所有我们的元素添加一个迭代:

//VODCategory
class VODCategory:VODComponent{

…
  override func display() {
    print(" \(name!),  \(description!) \r\n ----------------")
 for e in vodComponents{
 e.display()
 }
  }
}

因此,我们使用for … in遍历数组的每个元素,并调用每个元素的display()方法。

用法

我们的所有类现在都已准备就绪,现在是时候看看我们如何在客户端使用这个模式来测试所有这些了。

我们首先准备我们的VODManager类:

class VODManager{
  var catalog:VODComponent

  init(vod: VODComponent) {
      catalog = vod
  }

  func displayCatalog() {
      catalog.display()
  }
}

然后,我们编写我们的测试代码:

//USAGE
let horrorCategory = VODCategory(name: "Horror", description: "Horror movies category")
let tvSeriesCategory = VODCategory(name: "TV Series", description: "TV Series category")
let comedyCategory = VODCategory(name: "Comedy", description: "Comedy category")
let voSTTvSeries = VODCategory(name: "VOSTSeries", description: "VOST TV Series sub category")

let allVODComponents = VODCategory(name: "All VOD", description: "All vod components")
let vodManager = VODManager(vod: allVODComponents)

allVODComponents.add(horrorCategory)
allVODComponents.add(tvSeriesCategory)
allVODComponents.add(comedyCategory)

tvSeriesCategory.add(voSTTvSeries)

horrorCategory.add(VODItem(name: "Scream", description: "Scream movie", price: 9.99))
horrorCategory.add(VODItem(name: "Paranormal Activity", description: "Paranormal Activity movie", price: 9.99))
horrorCategory.add(VODItem(name: "Blair Witch Project", description: "Blair Witch movie", price: 9.99))

tvSeriesCategory.add(VODItem(name: "Game of thrones S1E1", description: "Game of thrones Saison 1 episode 1", price: 1.99))
tvSeriesCategory.add(VODItem(name: "Deadwood", description: "Deadwood Saison 1 episode 1", price: 1.99))
tvSeriesCategory.add(VODItem(name: "Breaking Bad", description: "Breaking Bad Saison 1 Episode 1 " , price: 1.99))

voSTTvSeries.add(VODItem(name: "Doc Martin", description: "Doc Martin French serie Saison 1 Episode 1", price: 1.99))
voSTTvSeries.add(VODItem(name: "Camping Paradis", description: "Camping Paradis French serie Saison 1 Episode 1", price: 1.99))

comedyCategory.add(VODItem(name: "Very Bad Trip", description: "Very Bad Trip Movie", price: 9.99))
comedyCategory.add(VODItem(name: "Hot Chick", description: "Hot Chick Movie", price: 9.99))
comedyCategory.add(VODItem(name: "Step Brothers", description: "Step Brothers Movie", price: 9.99))
comedyCategory.add(VODItem(name: "Bad teacher", description: "Bad Teacher Movie", price: 9.99))

vodManager.displayCatalog()

我们需要准备所有组件。所以,首先,我们准备我们的树形类别,然后向好的类别添加项目。

在脚本末尾,我们调用vodManager.displayCatalog()方法,这将调用所有组件的display方法。

那么,为什么我们在 Playground 中看不到有趣的东西呢?事实上,我们有一些线索告诉我们代码已经被正确执行。

在屏幕的右侧,我们可以看到方法被调用的次数,如下面的截图所示:

UsageUsage

然而,我们将对其进行一点修改,以使测试结果更准确。我们将通过添加字符串return类型并替换print语句为包含要返回的字符串的return语句来修改每个display()方法:

 override func display() -> String{
    return " \(name!), \(price!),  ----  \(description!)"
  }

你需要更改VODComponentVODItemVODCategory类的display()方法。

对于VODCategory,你需要像以下代码那样修改它,以便它易于阅读:

override func display() -> String{
    var text = " \(name!),  \(description!) \r\n ----------------"
    for e in vodComponents {
        text += "\r\n\(e.display()) \r\n"
    }
    return text
  }

对于VODManager,你只需要添加返回类型并将print替换为return

  func displayCatalog() -> String{
      return catalog.display()
  }

最后,将Usage部分(带有注释:// USAGE)中的tvSeriesCategory.add(voSTTvSeries)行(在vodManager.display()行之前)移动,这将使我们的结果更容易阅读。

现在,你将在vodManager.display()行后立即在右侧看到一些内容:

Usage

点击屏幕右侧的“眼睛”图标。你会看到我们的vodManager.display()调用的结果:

Usage

注意,组合或叶子的display()方法被递归调用。项目根据我们添加它们的类别进行组织。在前面的截图中,我们可以看到在调用Horror VODCategory之后,我们定义的所有恐怖电影(VODItem)都被显示出来,然后继续显示包含VOST TV Series子类别的电视剧,依此类推。

这标志着我们对组合模式的发现结束。

享元模式

当系统需要处理大量相似对象时,可以使用此模式。而不是逐个创建每个元素,此模式允许您重用具有相同数据的对象。

角色

享元模式通过减少创建的对象数量来减少包含数百甚至数千个相似对象的复杂模型的内存和资源使用。它试图重用相似现有的对象,或者在找不到匹配项时创建一个新的对象。

当以下情况发生时,可以使用此模式:

  • 我们需要操作很多小而相似的对象

  • 此操作的代价(内存/执行时间)很高

设计

下面的类图代表了该模式的通用结构:

Design

参与者

享元模式有三个参与者,如下所示:

  • Flyweight:这个接口声明了一个包含内在状态并实现方法的接口。这些方法可以接收并作用于享元的额外状态。

  • FlyweightFactory:这个工厂创建并管理享元对象。它通过返回享元的引用来确保享元的共享。

  • Client:这包含了对使用的享元对象的引用。它还包含这些享元的额外状态。

注意

外在状态:这是属于对象上下文的状态(外部)或对该实例唯一的状态。

内在状态:这是属于享元对象的状态,应该是永久或不可变的(内部)。

协作

客户端不会自己创建享元,而是使用 FlyweightFactory 方法,该方法保证了享元的共享。

当客户端调用享元的方法时,它需要发送其额外状态。

插图

假设我们想在 1024 x 768 的屏幕上显示 200000 个矩形。这些矩形是随机生成的;它们可以从 10 种不同颜色的列表中随机选择颜色。

我们需要减少执行函数所需的时间,并尽可能少地使用内存。

实现

在这个例子中,我们将使用带有 XCTest 框架和工具的 XCTest 项目来展示这个模式将如何帮助我们减少内存消耗。

首先,打开名为 Flyweight Pattern_Demo1 的项目,你可以在本章的源代码文件夹中找到它。

前往名为 FlyweightPattern_Demo1Tests 的 Xcode 项目,并点击下面的 FlyweightPattern_Demo1Tests.swift 文件,如图所示:

实现

在这个文件中,你会看到已经实现的不同测试方法。在我们开始实现享元模式之前,让我们看看我们目前有什么。

我们已经有一个名为 AbstractPerfTest 的抽象类,其中包含一些已经定义的属性、字段和方法:

class AbstractPerfTest {

  let colors:[SKColor] = [
    SKColor.yellowColor(),
    SKColor.blackColor(),
    SKColor.cyanColor(),
    SKColor.whiteColor(),
    SKColor.blueColor(),
    SKColor.brownColor(),
    SKColor.redColor(),
    SKColor.greenColor(),
    SKColor.grayColor(),
    SKColor.purpleColor()
  ]

  let sks = SKScene()
  let view = SKView(frame: NSRect(x: 0, y: 0, width: 1024, height: 768))

  let maxRectWidth = 100
  let maxRectHeight = 100

  //must be overriden
  func run(){
    preconditionFailure("Must be overriden")
  }

  // - MARK generate Rect Height and Width
  func generateRectWidth() -> Int{
    return Int(arc4random_uniform(UInt32(maxRectWidth)))
  }

  func generateRectHeight() -> Int{
    return Int(arc4random_uniform(UInt32(maxRectHeight)))
  }

  // - MARK generate Position X and Y
  func generateXPos() -> Int{
    return Int(arc4random_uniform(UInt32(view.bounds.size.width)))
  }

  func generateYPos() -> Int{
    return Int(arc4random_uniform(UInt32(view.bounds.size.height)))
  }
}

还有一个名为 NoPattern 的类,它继承自这个抽象类并重写了 run 方法:

import Foundation
// Inherits from our AbstractPerfTest class
// which contains default methods and init
class NoPattern:AbstractPerfTest {
  // Execute the test
  override func run(){
    var j:Int = 0
    for _ in 1...NUMBER_TO_GENERATE {
      let idx = Int(arc4random_uniform(UInt32(self.colors.count- 1)))

      let rect = SimpleRect(color: self.colors[idx])
      rect.display(generateXPos(), yPos: generateYPos(), width: generateRectWidth(), height: generateRectHeight())
      j++
    }
    print("\(j) rects generated")
  }
}

SimpleRect 类定义在 NoPattern 组文件夹的 SimpleRect.swift 文件中。它是一个由颜色、xy 位置、宽度和高度定义的对象。

我不会对 NoPattern 类进行过多的注释,但在这里我们看到的是,NoPattern 类的 run 方法生成了 NUMBER_TO_GENERATE(默认在 FlyweightPattern_Demo1Tests.swift 文件中设置为 100000)个随机颜色的矩形,这些颜色来自颜色数组列表(在抽象类中定义)。然后为这些矩形中的每一个生成一个位置和尺寸。

现在,让我们检查 run 方法的性能。

返回到 FlyweightPattern_Demo1Tests.swift 文件,检查名为 testSimpleScreenFilling_noFlyWeight() 的方法。在这里,该方法将执行 NoPattern 类中实现的代码,正如方法名所告诉我们的,它没有实现飞重量模式。这个方法的执行时间将被用作基线,以比较实现飞重量模式后的相同方法。

因此,让我们通过点击 func testSimpleScreenFilling_noFlyWeight() 函数左侧的小图标来执行测试,如下面的截图所示:

实现

我们需要确保在 Xcode 中可见控制台。在执行过程中,你会看到带有 200000 个矩形已生成 的控制台日志被重复了 10 次。这证明了我们的代码已经生成了 200,000 个矩形 10 次。默认情况下,self.measureBlock 闭包执行 10 次,并计算这 10 次执行的方差以获得平均执行时间:

实现

在我的 MacBook Pro 15 Retina Late 2013 上,平均时间是 0.804 秒:

实现

现在,最好的部分是将我们的代码重构以减少生成这些 200,000 个矩形所需的时间。正如你已经在模式的通用结构中看到的那样,我们需要几个类来管理我们的飞重量。

让我们从我们的 flyweightRect 类开始。注意,flyweightRectSimpleRect 类在 NoPattern 类中使用,以生成完全相同的矩形。

因此,在下面的代码中,你会找到我们的 FlyweightRect 类以及我们矩形的定义。因此,我们有一个颜色,xy 位置,矩形的高度和宽度。

注意,因为我真的很想看到性能的提升,我添加了两个字段:imagesprite。因为这些字段的值在类实例化时会有性能成本,所以我添加了它们,以便清楚地展示飞重量模式允许你在应用时减少计算成本(和内存使用)。

我们将向固有状态添加一个构造函数作为参数:这将是有色。我们将添加另一个 display() 方法,它将接收外延状态作为参数:

import SpriteKit
import Foundation

class FlyweightRect {

  var color: SKColor!
  var xPos: Int?
  var yPos: Int?
  var width: Int?
  var height: Int?
  var image: NSImage?
  var sprite: SKSpriteNode?

  //the constructor contains our intrinsic state
  init(color: SKColor) {
    self.color = color
    self.image = NSImage()
    self.sprite = SKSpriteNode()
  }

  func display(xPos: Int, yPos: Int, width: Int, height: Int){
    self.xPos = xPos
    self.yPos = yPos
    self.width = width
    self.height = height
  }

  func description() -> String  {
    return "rect position: \(self.xPos), \(self.yPos) : dimension: \(self.width), \(self.height)  : color: \(self.color)"
  }
}

一旦我们的飞重量被定义,我们现在可以准备我们的 FlyweightFactory 对象。记住,这个工厂将首先检查我们是否已经有了与我们要在屏幕上定位的新矩形相似的矩形;如果不是相似的,那么它将创建一个新的:

import SpriteKit
import Foundation

class FlyweightRectFactory{
    internal static var rectsMap = Dictionary<SKColor, FlyweightRect>()

  static func getFlyweightRect(color:SKColor) -> FlyweightRect{
    if let result = rectsMap[color]{
        return result
    } else { // if nil add it to our dictionnary
      let result = FlyweightRect(color: color)
      rectsMap[color] = result
      return result
    }
  }
}

我们声明一个静态的 rectsMap 变量,其类型为 Dictionary,它将包含我们的共享对象并管理它们的生存期。字典的 Key 将包含一个 Color 对象。

然后,我们定义一个名为 getFlyweightRect 的静态方法,它将返回一个 FlyweightRect 类。

由于 rectMaps[color] 返回 nil,我们使用 if let 语句解包可选值。如果它不是 nil,我们返回结果;否则,我们创建一个新的具有适当颜色的享元,将其添加到我们的字典中,并返回结果。

注意

FlyweightPattern_Demo1.swift 文件中,你可以找到几个测试方法,这些方法测试了根据管理我们的享元对象的对象类型,工厂的响应时间。在项目中,我使用了 Dictionary<SKColor, FlyweightRect>NSMutableDictionaryNSCache 类型来测试管理我们的享元对象的对象性能。

FlyweightRectFactory.swift 文件的完整代码如下:

import SpriteKit
import Foundation

class FlyweightRectFactory {

  internal static var rectsMap = Dictionary<SKColor, FlyweightRect>()
  internal static var rectsMapNS = NSMutableDictionary()
  internal static var rectsMapNSc = NSCache()

  static func getFlyweightRect(color:SKColor) -> FlyweightRect{
    if let result = rectsMap[color]{
        return result
    }else {       let result = FlyweightRect(color: color)
      rectsMap[color] = result
      return result
    }
  }

  static func getFlyweightRectWithNS(color: SKColor) -> FlyweightRect{

    let result = rectsMapNS[color.description]

    if result == nil {
      let flyweight= FlyweightRect(color: color)
      rectsMapNS.setObject(flyweight, forKey: color.description)
      return flyweightas FlyweightRect
    }else {
      return result as! FlyweightRect
    }

  }

  static func getFlyweightRectWithNSc(color: SKColor) -> FlyweightRect{

    let result = rectsMapNSc.objectForKey(color.description)

    if result == nil {
      let flyweight= FlyweightRect(color: color)
      rectsMapNSc.setObject(flyweight, forKey:color.description)
      return flyweight as FlyweightRect
    }else {
      return result as! FlyweightRect
    }
  }
}

使用方法

使用我们的模式非常简单。你需要检查 WithPattern.swift 文件中的 run() 方法:

class WithPattern:AbstractPerfTest{
  //Execute the test
  override func run(){
    var j:Int = 0
    for _ in 1...NUMBER_TO_GENERATE{
      let idx = Int(arc4random_uniform(UInt32(self.colors.count- 1)))
      let rect = FlyweightRectFactory.getFlyweightRect(self.colors[idx])
      rect.display(generateXPos(), yPos: generateYPos(), width: generateRectWidth(), height: generateRectHeight())
      j++
    }
    print("\(j) rects generated")
    //print("nb Map: \(FlyweightRectFactory.rectsMap.count)")
  }

我们将简单地循环创建 200000 个 FlyweightRect 对象(NUMBER_TO_GENERATE 是在 FlyweightPattern_Demo1Tests.swift 文件顶部定义的一个常量)。

在前面编写的 WithPattern 类执行以下操作:

  1. 我们首先生成一个随机数,该随机数返回一个值,将对应于颜色数组(在 AbstractPerfTest.swift 文件中定义)中可用的颜色的索引。

  2. 然后,我们告诉工厂返回具有适当颜色的享元。

  3. 然后,我们生成外部的状态(x 位置,y 位置,宽度,和高度)。

  4. 一旦循环完成,我们显示生成的矩形数量。

性能结果

要检查性能,项目中有一个 XCTest 类,它有一个 self.measureblock 闭包,允许我们测量我们代码块的性能。

要启动项目中所有可用的测试,请点击左侧的 显示测试导航器 按钮,如图所示:

性能结果

然后,点击高亮行右侧可见的播放按钮:

性能结果

几秒钟后,所有测试都会完成,你现在可以检查你的性能结果。

返回到 FlyweightPattern_Demo1Tests.swift 文件并检查每个 measureblock() 方法的末尾。这是使用字典的享元模式的性能结果。你可以看到生成 200000 个矩形平均花费了 0.247 秒,如图所示。你会看到一个带有 Time xxxx 文字的文本;这是执行此块的平均时间:

性能结果

与未使用模式时的平均 0.877 秒相比:

性能结果

在查看结果后,你会发现生成矩形时(性能最好的先列出来):

  • 使用 Dictionary 对象的享元模式用于管理我们的共享对象

  • 使用 NSDictionary 对象的享元模式用于管理我们的共享对象

  • 使用 NSCache 对象的享元模式用于管理我们的共享对象

  • 没有应用任何模式

在这个例子中,我们可以说,生成 200000 个 FlyweightRect 对象比不使用模式快 3,55 倍。

测试项目证明,Swift 比 Objective-C 和封装的 NSCache 更快。

在处理缓存时,NSDictionary 对象将拥有自己的逻辑。它可以在自己的隐藏代码结构内部创建更多对象,因此比 NSDictionary 对象慢。

摘要

在本章中,你学习了如何处理多个对象的结构。组合模式允许你以统一的方式访问和修改数据结构,而享元模式则是当存在多个相似对象时,更精确地节省内存空间或计算时间的做法。

享元模式与其他模式结合使用时,有助于将数据保持得尽可能小。组合模式与其他模式结合使用时,有助于管理数据结构。组合模式可以使用享元模式,但反之则不行。

在本章中,我尝试向你展示一些不同之处;使用 XCTest 框架来测试我们模式的性能。如果你想深入了解,你可以尝试使用 Xcode 提供的仪器工具查看内存分配的差异。

在下一章中,我们将继续通过学习适配器和外观模式来发现我们的结构模式。

第四章:结构模式 - 适配器和外观

在本章中,我们将讨论两种新的结构模式:适配器模式和外观模式。我们将重点关注适配器模式,它将那些未被设计为相互协作的类型联合起来。然后,我们将讨论外观模式,它简化了一组复杂系统的接口。

适配器模式

当处理框架、库和 API 时,这种模式经常被用来轻松地将旧的和现有的接口适应到程序的新需求。

角色

适配器模式将另一个现有类的接口转换为现有客户端期望的接口,以便它们可以一起工作。多亏了这个模式,您可以集成那些通常无法修改源代码的组件,以及那些经常与框架使用相关的事物。

注意,如果您已经可以访问组件的源代码,应避免使用此模式。

设计

该模式的通用类图如下:

设计

参与者

在前面的图中,您可以看到这个模式的四个参与者:

  • ITarget:通过这个接口引入了对象的函数签名

  • Client:客户端与实现ITarget接口的对象交互

  • Adapter:这个类实现了ITarget接口的方法,并调用适配对象的相应方法

  • Adaptee:这是我们需要适配其接口以使其可由客户端操作的对象

协作

客户调用methodA()适配器,该适配器本身调用adaptee对象的methodB()适配器。

下面的截图展示了我们项目的组织结构:

协作

示例

您公司的销售总监希望生产一款适用于手机的通用电池充电器。这款充电器可以输出高达 10 伏的电压。作为公司的 CIO,您的开发团队向您展示了这个充电器的第一个原型。

在本章中,我在first_prototype文件夹中创建了一个新的 OS X 命令行工具项目,您可以在其中找到它,并将其命名为ChargerPrototype

实现我们的第一个原型

我们的项目组织如下:

  • Interfaces文件夹包含了客户端将要调用的方法定义,用于给手机充电

  • PhonePrototype.swift文件是一个类,它定义了我们的测试手机并实现了IChargeable协议

结果如下:

实现我们的第一个原型

ChargeableProtocol接口是一个简单的协议,它定义了charge方法的签名:

import Foundation

protocol ChargeableProtocol {

  /// This function is called to charge a mobile phone
  ///
  /// Usage:
  ///
  ///    charge(5.5)
  ///
  /// - Parameter volts: voltage needed to charge the battery
  ///
  /// - returns: Void
  func charge(volts: Double)
}

提示

考虑到 Swift 协议与接口有相同的概念。

协议和 Java 接口之间有一些区别,如下所示:

  • Swift 协议也可以指定必须实现(例如,字段)的属性

  • Swift 协议需要通过使用mutating关键字来处理值/引用(因为协议可以被结构体和类实现)

你可以在任何位置使用protocol<>关键字组合协议,例如,声明一个必须遵循AB协议的函数参数,如下所示:

func foo ( var1 : protocol<A, B> ){}

接下来,我们定义一个PhonePrototype类,它可以使用ChargeableProtocol协议进行充电:

class PhonePrototype: ChargeableProtocol {
  /// This function is called to charge a mobile phone
  ///
  /// Usage:
  ///
  ///    charge(5.5)
  ///
  /// - Parameter volts: voltage needed to charge the battery
  ///
  /// - returns: Void
  func charge(volts: Double) {
    print("Charging our PhonePrototype")
    print("current voltage \(volts)")
  }
}

注意这里添加的代码注释。使用 Swift,你可以添加有组织的注释,这样你可以通过弹出窗口(通过按住Alt键并指向鼠标光标在 Swift 方法上)来查看信息。要检查注释的显示结果,请按照以下步骤操作:

  1. 打开Charger.swift文件。此文件代表我们的通用充电器。

  2. 按住并保持Alt键。

  3. 将鼠标定位在self.phone.charge(volts)语句中出现的charge上。一个问号?会出现。

  4. 点击充电。

然后,你会看到以下弹出窗口,如图所示:

实现我们的第一个原型

你应该考虑详细注释你的函数作为最佳实践。确保所有你的方法都得到了充分的注释。你可以检查弹出窗口是如何得到良好文档化的,如下所示:

  • 使用///开始注释你的代码

  • 使用– 参数 parametername: 参数描述来描述你的参数

  • 使用– 返回: 类型来描述返回的类型

注意

关于现有关键字以文档化你的代码的更多信息,你应该查看以下网站:

ericasadun.com/2015/06/14/swift-header-documentation-in-xcode-7/

你的工程师向你展示了充电器的原型,它看起来工作得很好。

在 Xcode 中加载ChargerPrototype.xcodeproject文件后,点击运行以启动代码。

在控制台上,你会看到以下结果:

实现我们的第一个原型

让我们看看Charger类是如何实现的:

import Foundation

class Charger {
  var phone: ChargeableProtocol!
  let volts = 10.0

  func plugMobilePhone(phone: ChargeableProtocol){
    print("A mobile is plugged")
    self.phone = phone
    self.phone.charge(volts)
  }
}

上述代码的实现相当简单。通过逐行阅读,我们可以推断出它是如何工作的:

  • 我们的充电器包含一个对手机的引用。该手机必须实现ChargeableProtocol。当然,我们的通用充电器只会与这个接口通信。

  • 然后,我们有一个可以插入手机的方法。

  • 我们将手机分配给我们的引用并调用引用手机的充电方法。

因此,你的手机充电器与所有实现ChargeableProtocol的手机充电器都工作得很好。

然而,有一个问题。你的充电器与所有实现ChargeableProtocol的手机都工作得很好,但与市场上可用的手机不兼容。确实,每个手机制造商都有自己的接口来为其产品充电。

使用我们现有的充电器给手机充电是不可能的!这是想要销售通用手机充电器的公司的屋顶。

你可以安排一个紧急会议,与你的团队一起找到解决方案,以避免你公司的破产。

小贴士

Mike,你们公司的新开发人员,提出了解决方案:“为了让所有手机都能使用我们的充电器,它们需要实现ChargeableProtocol,但我们不能修改手机,因为我们没有源代码。所以,我们可以告诉每个制造商在他们自己的 iPhone 上实现ChargeableProtocol。”

Kevin,IT 项目经理,回答说:“Mike,欢迎加入我们公司,但你的解决方案不是一个好的选择。制造商有自己的系统,他们不想改变它们。对于已经售出且没有实现ChargeableProtocol的手机,我们该怎么办?”

Kevin 继续说:“我们遇到的问题不是制造商的问题;他们不需要调整他们的代码来让我们的充电器与他们的产品一起工作,这是我们的公司需要调整的地方。是的,我们必须调整我们的代码。”

Mike 问道:“那么,你打算怎么进行?”

Kevin 回答说:“嗯,这个概念很简单。如果你计划去法国旅行,那里的电源插座规格和形状并不适合我们美国的。我在我家附近的电器店偶然发现了一个适配器。这个适配器接受我的美国电缆的形状和规格,以及可以插入法国电源插座的另一个适配器,因为插入的一侧具有与法国相同的形状和规格。所有美国和法国规格之间的转换都在适配器本身完成。”

“这里的问题也是一样的。我们必须根据制造商调整我们的充电器。所以,我们将为每款手机配备一个适配器,并继续使用一个独特的充电器。”

Julien,首席执行官,回答说:“太好了,Kevin!我们现在就去。我们必须在圣诞节前让我们的通用充电器可用!”

实现

我们需要做的第一件事是准备我们的适配器,使其与我们的充电器一起工作。充电器只与实现ChargeableProtocol的对象一起工作。我们将需要为我们将要创建的每个适配器实现ChargeableProtocol

一旦通过实现ChargeableProtocol完成充电器侧的“插入”适配器,我们将添加一个引用到适配器适配的手机。我们的通用充电器不会操作手机实例;这是适配器的角色来操作它。

打开ChargerWithAdapter.xcodeproj项目并检查我们代码的组织结构:

实现

我们已经将我们的演员分成了以下三个文件夹:

  • 适配器:这包含我们必须适配的组件。如果你没有源代码,可以考虑这个。记住,如果你拥有适配器文件夹的源代码,你不应该使用适配器模式。

  • Adapters:这包含了根据我们想要充电的手机类型而必须使用的适配器。

  • Interface:这包含了我们的适配器实现的接口和客户端操作的接口。

  • main.swift:这代表客户端:通用充电器。

  • Charger.swift:这是需要适配的对象。

适配器的实现

让我们研究两个需要适配的手机:即“Pear”手机和“SamSing”手机:请注意,在现实生活中,你将不会拥有adaptee对象的源代码,即需要适配的对象,你将只与它们的已知接口打交道。

让我们先分析一下 SamSing 手机:

import Foundation
class SamSingMobilePhone {

  enum VoltageError: ErrorType {
    case TooHigh
    case TooLow
  }

  ///Accept only 10 volts
  func chargeBattery(volts: Double) throws {
    if volts > 10 { throw VoltageError.TooHigh }
    if volts < 10 { throw VoltageError.TooLow }

    print("SamSing mobile phone is charging")
    print("Current voltage \(volts)")
  }
}

现在,让我们分析一下 Pear 手机:

import Foundation

class PearMobilePhone {

  enum PearVoltageError: ErrorType {
    case NoPower
    case TooLow
    case TooHigh
  }

  ///Accept only 5.5 volts
  func charge(volts: Double) throws {
    guard volts > 0 else { throw PearVoltageError.NoPower}
    if volts > 5.5 { throw PearVoltageError.TooHigh }
    if volts < 5.5 { throw PearVoltageError.TooLow }

    print("Pear mobile phone is charging")
    print ("Current voltage \(volts)")
  }
}

这两个类代表了我们需要适配的对象:第一个是SamSingMobilePhone类,它有一个名为chargeBattery的方法。这是原始充电器用来为 SamSing 手机充电的方法。

第二个是PearMobilePhone类,它允许使用原始充电器来充电电池,但这个类简单地被称作charge。请注意,这两种手机需要不同的电压来充电。

所以你在这里看到的是,我们的通用充电器在 Pear 手机插入时需要调用charge方法,而在 SamSing 手机插入时需要调用chargeBattery方法。

我们将不得不为每种我们想要能够充电的手机类型制作一个适配器。

如前所述,当你使用适配器模式时,你不应该有adaptee对象的源代码。这里提供的源代码只是为了演示目的。

你检查过我们两个adaptee的源代码了吗?我自愿引入了两个新的 Swift 关键字,我想向你介绍。

首先,这两个类都有一个可能的错误枚举,充电器可以处理。如果电压过高或过低,SamSing 将抛出一个错误;Pear 型号可以抛出相同的错误,如果手机插入充电器时完全没有电,它还可以抛出另一个错误。如文档中所述,在 Swift 中,错误由符合ErrorType协议的类型值表示。这就是为什么每个枚举都实现了ErrorType协议的原因:

  enum PearVoltageError: ErrorType {
    case NoPower
    case TooLow
    case TooHigh
  }

只有这个枚举是不够的;我们需要在调用charge方法时处理错误。为此,我们需要告诉方法抛出一个异常。

为了这个,我们只需在定义返回类型之前简单地添加throw关键字,如下所示:

func chargeBattery(volts:Double) throws {

记住,这个函数定义与以下定义完全相同:

func charge(volts:Double) throws -> Void {

不提供返回类型相当于声明返回类型是Void类型。

好吧,一旦我们的方法被告知它可以处理错误,我们仍然需要在必要时引发错误。我们在方法开始处进行一些条件检查,如果出现问题,就抛出异常。要引发异常,我们只需在想要引发的(实现 ErrorType 协议的)ErrorType对象之前使用throw关键字。(这里是一个VoltageError):

if volts > 10 { throw VoltageError.TooHigh }
if volts < 10 { throw VoltageError.TooLow }

如前所述,PearMobilePhone类会引发错误;这是当方法从充电器接收0电压时。在这种情况下,PearVoltageError.NoPowerPearVoltageError枚举类型值将被引发。

让我们调查PearMobilePhone类的charge方法:

 ///Accept only 5.5 volts
  func charge(volts: Double) throws -> Void {
    guard volts > 0 else { throw PearVoltageError.NoPower}
    if volts > 5.5 { throw PearVoltageError.TooHigh }
    if volts < 5.5 { throw PearVoltageError.TooLow }

    print("Pear mobile phone is charging")
    print("Current voltage \(volts)")
  }

我们可以看到,两个if语句检查电压是否在第一种情况下高于 5.5 伏,在第二种情况下低于 5.5 伏。每个if语句都可以抛出一个错误:TooHighTooLow

现在,让我们检查如何引发NoPower错误:

    guard volts > 0 else { throw PearVoltageError.NoPower}

这个语句引入了 Swift 2 的新关键字guard … else {

guard语句类似于if语句。它根据表达式的布尔值执行语句。在guard语句之后,程序必须继续执行的条件必须为真。guard语句始终有一个else子句。如果表达式不为真,则执行该子句中的语句。

guard语句允许你在检查条件时执行早期退出。

我真的很喜欢guard能够这样使用:

guard let unwrappedVar = myVar else {
  return
}
print("myVar : \(unwrappedVar)")

在 Swift 的早期版本中,你会写类似这样的事情:

If let unwrappedVar = myVar {
  Print("myVar : \(unwrappedVar"))
}else {
  return
}
//now if you call the print statement below this will not work
//because unwrappedVar is no longer available in this scope.
print("myVar : \(unwrappedVar)")

因此,回到我们的模式。我们已经看到我们的两个手机接口不同,我们现在需要为每个手机创建一个适配器,如下面的图所示:

我们适配器的实现

SamSingAdapter 类的实现

在前面的图中,我们现在可以编写我们的新适配器,使其能够与 SamSing 手机一起工作,如下所示:

import Foundation

class SamSingAdapter: ChargeableProtocol {

  var samSingPhone: SamSingMobilePhone!

  init(phone: SamSingMobilePhone){
    samSingPhone = phone
  }

  func charge(volts: Double) {
    do {
      print("Adapter started")
      _ = try samSingPhone.chargeBattery(volts)
      print("Adapter ended")
    }catch SamSingMobilePhone.VoltageError.TooHigh{
      print("Voltage is too high")
    }catch SamSingMobilePhone.VoltageError.TooLow{
      print("Voltage is too low")
    }catch{
      print("an error occured")
    }
  }
}

我们创建了一个新的SamSingAdapter类,该类实现了ChargeableProtocol。我们需要提供一个charge方法,该方法接受电压作为参数。

我们添加了一个名为samSingPhone的常量,它实例化了一个SamSingMobilePhone()对象,我们将使用它来调用其自己的chargeBattery方法。

我们将SamSingMobilePhone作为参数传递给适配器的构造函数,以获取我们想要充电的手机的引用。然后,我们实现charge方法的代码。(记住,这是客户端知道的唯一方法。)

再次,我想向你展示一些 Swift 2 带来的新特性。

Swift 2 引入了dotrycatchthrow机制。当我们发现我们的两个Adaptee类时,我们已经讨论了throw语句。

定义中包含throw关键字的方法如下:

func chargeBattery(volts:Double) throws {

它必须使用try语句调用,因为这个设计是为了让开发者更清晰:

    _ = try samSingPhone.chargeBattery(volts)

_符号是一个通配符,因为 SamSing 手机上的chargeBattery方法不返回任何值(实际上,它返回Void)。写这样的语句是没有用的:

    let myVar = try samSingPhone.chargeBattery(volts)

因为您想处理chargeBattery方法可能抛出的错误,所以这个语句必须在一个do { } catch块内:

    do {
      print("Adapter started")
      _ = try samSingPhone.chargeBattery(volts)
      print("Adapter ended")
    }catch SamSingMobilePhone.VoltageError.TooHigh{
      print("Voltage is too high")
}
//…..

因此,在do块中,您将添加可能抛出错误的调用方法,如果您想处理它,可以在catch块中捕获它:

catch SamSingMobilePhone.VoltageError.TooHigh{
      print("Voltage is too high")
    }catch SamSingMobilePhone.VoltageError.TooLow{
      print("Voltage is too low")
    }catch{
      print("an error occured")
    }

catch语句将静默所有错误,代码将继续运行。如果您想了解更多关于 Swift 2 中错误处理的信息,我建议您查看以下网站:

www.hackingwithswift.com/new-syntax-swift-2-error-handling-try-catch

我们的SamSingAdapter类现在已经准备好了。我们现在将对PearAdapter类做同样的事情。

PearAdapter 类的实现

我们将按照SamSingAdapter类的方式继续进行,但使用 Pear 手机:

import Foundation

class PearAdapter: ChargeableProtocol {

  var pearMobilePhone:PearMobilePhone!

  init(phone: PearMobilePhone){
    pearMobilePhone = phone
  }

  func charge(volts: Double) {
    do {
      print("Adapter started")
      _ = try pearMobilePhone.charge(5.5)
      print("Adapter ended")
    }catch PearMobilePhone.PearVoltageError.TooHigh{
      print("Voltage is too high")
    }catch PearMobilePhone.PearVoltageError.TooLow{
      print("Voltage is too low")
    }catch{
      print("an error occured")
    }
  }
}

在这里,主要的不同之处在于我们现在有一个PearMobilePhone对象的引用,并且我们的charge方法(实现了ChargeableProtocol)调用pearMobilePhone.charge方法。

我们还需要管理发送到手机的电压。适配器必须将任何值转换为适配对象接口的规范。如果我们发送过高的电压,我们的手机会烧毁,我们的客户将停止购买我们的产品。因此,在我们的适配器中,我们将发送到 Pear 手机的电压值设置为 5.5 伏。

我们还捕获了pearMobilePhone对象的charge方法可能抛出的所有错误。

注意

Swift 2 要求详尽的try/catch错误处理。最后的catch语句是我们的默认捕获所有错误的块。

由于我们已经将电压值设置为 5.5 伏,这是 Pear 手机唯一接受的电压,所以我们永远不会引发错误,因此拥有如此多的捕获块是难以阅读的。

好吧,苹果为您提供了一个替代方案。您可以这样编写我们的适配器:

  func charge(volts: Double) {
      print("Adapter started")
      _ = try! pearMobilePhone.charge(5.5)
      print("Adapter ended")
  }

try!方法允许您避免使用do/catch,因为您承诺调用永远不会失败。

我们已经有了现成的通用充电器,两个适配器和两部手机来测试我们的充电器。请记住,我们的充电器默认提供 10 伏。

让我们在main.swift文件中编写我们的简单测试程序:

import Foundation

print("*** start test program")
// Create our Charger
let charger = Charger()
print("*** charger ready test program")

//Test 1
//Charge a Pear Mobile Phone
print("Will charge a Pear Mobile Phone")
//1 mobile and adapter creation
let pearPhone = PearMobilePhone()
let pearAdapter = PearAdapter(phone: pearPhone)
//we plug the portable to our charger through the adapter
charger.plugMobilePhone(pearAdapter)

print("*** -")
//Test 2
//Charge a SamSing Mobile Phone
print("Will charge a SamSing Mobile Phone")
//1 mobile and adapter creation
let samSingPhone = SamSingMobilePhone()
let samSingAdapter = SamSingAdapter(phone: samSingPhone)
//we plug the portable to our charger through the adapter
charger.plugMobilePhone(samSingAdapter)

print("*** end test program")

我认为我不需要提供更多细节,因为完整的代码已经在这里提到了。我们准备我们的充电器,拿起第一部手机,使用适当的适配器,然后将适配器(它也连接到我们的手机)插入充电器。我们对第二部手机也做同样的事情。

让我们运行代码并检查我们的结果:

PearAdapter 类的实现

好吧,不管手机是什么型号;使用我们适当的适配器,默认情况下,充电器会将 10 伏电压发送到适配器,然后适配器将转换这种电压(或不会转换)并在手机本身上调用适当的充电方法。

这就完成了适配器模式的发现。

门面模式

门面模式是一种简单的模式,用于将一组对象的接口和易于客户端使用的统一接口组合在一起。

角色

门面模式允许你提供用户可能需要的操作。该模式封装了每个被视为低级接口的对象的接口。为了构建统一接口,我们可能需要实现一些方法,这些方法将组合低级接口:

  • 它必须用来提供一个复杂系统的简单接口。系统的架构可以由几个小型类组成,这些类允许极大的模块化,但客户端不需要这些属性。他们只需要满足他们需求的东西。

  • 它可以用来将系统划分为子系统。门面将成为子系统之间通信的接口。

  • 它还可以用来封装系统对外部消费者的实现。

设计

门面模式旨在隐藏系统的复杂性。它的接口可以是全新的。它必须不符合现有接口。我们可以根据我们拥有的最终用户和想要提供给门面的功能,为同一系统提供几个门面:

设计

参与者

此模式的参与者如下:

  • 门面及其接口是系统暴露给客户端的抽象部分。此类包含对系统内其他类和组件的引用,它们使用门面实现统一接口的方法。

  • 系统的类和组件实现系统的功能,并响应来自门面的请求。它们不需要门面就能工作。

协作

客户端通过门面与系统通信。然后,门面本身调用系统的类和组件。门面不仅向子系统中的类和组件发送请求。它还必须使用特定的代码将自己的接口适配到对象和组件接口,以允许对象通信。

以下序列图描述了这种情况:

协作

使用门面的客户端不应直接访问系统的对象。

说明

我们希望提供一个简单的接口,让我们的客户端能够轻松地找到输入地址附近的酒店,并符合某些标准(如星级数量。)

在我们公司,我们有一个子系统,它提供包含每个酒店位置和星级的酒店目录。

我们已经有一个可以搜索位置点(纬度和经度)附近的酒店的FindPoi Web 服务,它使用一些标准:搜索的最大距离和我们想要的星级数量。

如您所见,服务需要一个位置点,这意味着我们传递一个包含纬度和经度的对象。

由于我们建筑立面的消费者只会告诉我们其实际地址,我们需要能够将地址地理编码到具有纬度和经度的 GPS 点的服务。

序列图显示了我们将如何提供简化的接口:

插图

打开名为FacadePattern的 Xcode 项目以及main.swift文件。此文件代表一个将消费我们的立面的客户端。

我们的立面实际上是一个服务,允许客户端搜索输入位置附近的酒店,该输入有两个标准:distanceMax和酒店的星级数量。

这就是服务将被调用的方式:

var results = svcFacadeFindHotel.findHotel(myAdress, distanceMax: 2.0, stars: 4)

客户端的完整代码如下:

import Foundation

//I am a consumer of the service
// my addess is
// 1 infinite Loop
// Cupertino, CA 95014

let svcFacadeFindHotel = ServiceFindHotelNearBy()

let myAdress = " 1 Infinite Loop Cupertino, CA 95014 USA"
var results = svcFacadeFindHotel.findHotel(myAdress, distanceMax: 2.0, stars: 4)

print("*** RESULTS ")
print("Their is \(results?.count) results :")

if let results = results {
  for var h in results{
    print("Hotel latitude:\(h.location.latitude) longitude:\(h.location.longitude), stars: \(h.stars)")
  }
}

我们为ServiceFindHotelNearBy服务打开一个连接。我们告诉我们的当前地址,然后显示结果(如果有)。在这里,我们有 125 个结果(来自HotelCatalog对象中生成的 1,000 个结果):

插图

面向对象实现

根据序列图和客户端使用的服务方法,我们样本项目中的ServiceFindHotelNearBy立面将具有以下代码:

import Foundation

class ServiceFindHotelNearBy: ServiceFindHotelNearByProtocol {

  //return a list of hotel that corresponds to our criteria
  func findHotel(from: String, distanceMax: Double, stars: Int) -> [Hotel]? {
      let svcGeocoding = Geocoding()
      let svcFindPoi = FindPoi()
      let systemhotelCatalog = HotelCatalog()

      //Geocode our adress to GPS Points
      let fromLocation = svcGeocoding.getGeocoordinates(from)

      //retrieve all hotels in the catalog
      let allHotels = systemhotelCatalog.getCatalog()

      //find POI that corresponds to our criteria
      let results = svcFindPoi.findPoiNearBy(fromLocation, distanceMax: distanceMax, stars: stars, catalog: allHotels)
    return results
  }
}

立面可以这样描述:

  • 首先,我们连接到所有需要的系统:GeocodingFindPoiSystemHotelCatalog Web 服务(我们的子系统)。

  • 然后,我们根据序列图编排我们的调用。

  • 我们首先将地址地理编码到一个 GPS 点。

  • 然后,我们从systemHotelCatalog对象(代表一个子系统)中获取allHotels,因为我们需要将其作为参数传递给FindPoi服务。

  • 这是我们下一个语句中需要做的事情。我们需要将distanceMax值、星级数量值和刚刚地理编码的 GPS 点作为FindPoi服务的findPoiNearBy方法的参数传递。

  • 然后,我们将结果返回给客户端。

如您所见,立面封装了所有必要的子系统调用,隐藏了获取符合客户愿望的酒店的复杂性。这完成了对外观模式的描述。

摘要

本章完成了对七个结构模式的发现。适配器模式与桥接模式有很多共同之处。主要区别在于模式的目的。

桥接将接口与其实现分离,而适配器则改变现有对象的接口。

装饰者在不改变对象接口的情况下添加功能,并且应该对应用程序是透明的。对于适配器来说,情况并非如此,它从客户端的角度来看并不透明:适配器是客户端看到的接口的命名实现,因此适配器不会被客户端隐藏。

代理模式不会改变任何接口。它为其他对象定义了替代对象。

门面模式通过与其他子系统通信,将高级请求转换为低级请求。它通过提供一个简单的客户端可以看到的接口来隐藏这些子系统的复杂性。

在下一章中,我们将从我们的前三个行为模式开始:策略状态模板方法。

第五章. 行为模式 – 策略、状态和模板方法

希望您还在这里;现在,我们将向您介绍设计模式的第三和最后一个类别,它被归类为四人帮GoF)设计模式:行为模式。行为模式致力于算法及其之间的通信。

由于算法由多个操作组成,这些操作被分为不同的类,因此行为模式可以处理这些类的组织和它们之间通信的方式。

行为类别包含 11 个模式,我们将通过四章来讨论。在本章中,我们将讨论以下三个模式:

  • 策略模式

  • 状态模式

  • 模板方法模式

策略模式

当您需要在运行时更改对象的部分算法,而不修改客户端时,策略模式是合适的模式。

它将算法从其宿主类中移除并移动到单独的类。可以更改的算法部分是策略。每个策略都使用相同的接口。使用策略模式的类将算法的处理委托给策略。

角色

策略模式用于创建一个可互换的算法系列,在运行时从其中选择所需的过程。

算法变更不会影响客户端部分。此模式可以在以下情况下使用:

  • 一个类的行为可以通过不同的算法实现,其中一些在执行时间或内存消耗方面表现更好

  • 使用 if 条件指令选择合适的算法会使代码复杂化

  • 一个系统具有类似类,其中只有行为发生变化;在这种情况下,策略模式允许您将这些类组合到一个类中,这大大简化了客户端的接口

设计

策略平台的通用结构如下:

设计

参与者

策略模式中的参与者如下:

  • IStrategy:此类定义了所有算法实现的通用接口。这是 ClassUsingStrategy 类用来调用正确算法的接口。

  • ConcreteStrategyAConcreteStrategyB:这些是根据 IStrategy 接口实现不同算法的具体类。

  • ClassUsingStrategyAClassUsingStrategyB:这些是使用实现 IStrategy 接口的类的算法的类。这些类有一个对具体策略类实例的引用。这些类可以向实现类公开一些内部数据。

协作

ClassUsingStrategyConcreteStrategy 类交互以实现算法。

在大多数情况下,算法所需的数据作为参数发送给构造函数,也可以通过设置属性发送。如果需要,ClassUsingStrategy类可以提供一些方法,让您访问其内部数据。

Client实例将使用Strategy对象初始化ClassUsingStrategy,并调用使用策略模式的ClassUsingStrategy方法。

然后,这个类将把从客户端接收到的请求发送到策略属性引用的实例。

插图

我们将通过一个简单的例子来了解如何实现策略模式。

一些物体可以移动,但它们移动的方式并不相同。每个物体都有其特定的移动方式:有的可以行走,有的可以奔跑和飞翔。移动行为是我们的策略,我们将把performMove()动作封装在具体策略类中,该类在具体类中使用策略。

实现

打开策略模式 Xcode 项目,查看我们代码的组织结构。该模式的参与者被分为四个文件夹,如下面的截图所示:

实现

我们首先将定义移动策略的接口,然后描述AbstractClassUsingMoveStrategy

移动策略的接口非常简单;我们只需告诉实现策略的类我们正在等待一个performMove方法。

MoveStrategyProtocol.swift文件如下:

//Common Interface used by algorithms
protocol MoveStrategyProtocol {
  func performMove()
}

我们的抽象类需要有一个角色来保持对将要应用策略的引用,通过封装对当前策略的performMove方法的调用到我们自己的方法中,我们将调用该方法为move()。策略对象的实例将在实现AbstractObjectThatMove类的类的构造函数中接收。

对于我们的演示,我们将添加一个内部计算属性WhoAmI,我们将使用策略模式设置或获取具体类的名称。

我们的AbstractObjectThatMove.swift文件如下:

class AbstractObjectThatMove {
  private var strategy: MoveStrategyProtocol!
  private var whoAmI:String = "Unknown Object"

  required init(strategy: MoveStrategyProtocol) {
    self.strategy = strategy
  }

  func move(){
    strategy.performMove()
  }

  internal var WhoAmi: String {
    get {
      return whoAmI
    }
    set {
      whoAmI = newValue
    }
  }
}

注意

Swift 2.0 目前还没有支持抽象类。在这里,我们命名为AbstractObjectThatMove类,即使它不是一个“真正的”抽象类,也是为了尽可能接近模式的一般概念。尽管如此,可能有一种方法可以得到类似抽象类的东西,但它与通过初始化器传递策略的一般模式概念不同。Swift 2.0 有协议扩展,它提供了向协议添加部分实现方法的机会,并在“某种方式”上实现了一个抽象类:

protocol AbstractObjectThatMove {
   var WhoAmi: String { get set}
}
extension AbstractObjectThatMove {
    func move(strategy: IMoveStrategy) {
       strategy.performMove()
    }
}
class Human : AbstractObjectThatMove {
    var WhoAmi: String = "i'm a human"
}

然后,我们可以处理以下代码:

print("- *** working with Human")
let strategyForHuman = WalkMoveStrategy()
let human = Human()

// Tell who am I
print(human.WhoAmi)

//perform human move:
human.move(strategyForHuman)

因此,我们回到模式的实现。在我们的当前示例中,我们有三个具体类实现了抽象类:RabbitBirdHuman。由于这三个对象的实现相同,我将只展示具体Human类的实现:

class Human: AbstractObjectThatMove {
  required init(strategy: MoveStrategyProtocol){
    super.init(strategy: strategy)
    self.WhoAmi = "i'm a human"
  }
}

我们想在初始化 WhoAmI 属性时添加一个值,因此我们通知 init 方法这个方法是需要调用的。然后,我们使用简单的 self.WhoAmI = "I'm a human" 语句设置属性的值。

备注

我们也可以通过在 AbstractClass 中定义的构造函数中添加一个额外的参数来实现这一点,该参数接受 WhoAmI 的值并将其分配给内部的 whoAmI 变量。

现在,我们将实现 WalkMoveStrategy,如 MoveStrategyProtocol 协议中定义的那样:

class WalkMoveStrategy: MoveStrategyProtocol {
  func performMove() {
    print("I am walking")
  }
}

这里没有复杂的东西;我们实现了 performMove 方法并打印移动动作的消息。RunMoveStrategyFlyMoveStrategy 方法以相同的方式实现;只是 print 语句有所不同:

Last, to complete the example, we will make our human, bird, and rabbit perform a move according to the strategy they apply:print("- *** working with Human")
let strategyForHuman = WalkMoveStrategy()
let human = Human(strategy: strategyForHuman)

// Tell who am I
print(human.WhoAmi)

//perform human move:
human.move()

print("- *** working with Bird")
let strategyForBid = FlyMoveStrategy()
let bird = Bird(strategy: strategyForBid)

// Tell who am I
print(bird.WhoAmi)

//perform human move:
bird.move()

print("- *** working with Rabbit")
let strategyForRabbit = RunMoveStrategy()
let rabbit = Rabbit(strategy: strategyForRabbit)

// Tell who am I
print(rabbit.WhoAmi)

//perform human move:
rabbit.move()

在前面的高亮代码中,您可以看到我们如何通过以下步骤将策略应用于 Human 类:

  1. 首先,我们实例化一个策略。

  2. 接下来,我们使用策略模式实例化一个具体类,其中首先声明的策略作为参数传递。

  3. 然后,我们在具体类中执行一个移动动作。移动动作会调用具体类所引用的策略的 performMove 方法。

  4. 点击运行按钮查看结果。

您应该在 Xcode 的控制台中看到以下结果:

实现

状态模式

在状态模式中,一个类的行为根据其状态而改变。这种设计模式属于行为模式。

在状态模式中,我们创建代表各种状态的对象以及一个上下文对象,其行为随着状态对象的变化而变化。

角色

这个模式的角色是根据对象的内部状态调整其行为。当条件语句变得复杂时,可以用来实现状态对象的依赖。

设计

状态模式的通用类图结构如下:

设计

参与者

状态模式中的参与者如下:

  • StateMachine:这是一个具体类,它描述了状态机对象的属性,这意味着它们有一组可以在状态转换图中描述的状态。这个类有一个指向实现状态抽象类的子类实例的引用,并定义了当前状态。

  • IState:这是一个抽象类,它向您介绍了状态行为的方法签名。

  • ConcreteStateAConcreteStateB:这些是具体子类,根据状态实现行为方法。

协作

StateMachine 对象根据当前状态将方法调用委派给一个 ConcreteState 对象。

如果需要,StateMachine 对象可以发送对其自身和对 ConcreteState 对象的引用。然后,这个引用可以通过 concreteState 对象的初始化或每次调用委派来发送。

说明

您的公司需要销售一款只有两个按钮的新设备,这两个按钮可以播放收音机或音乐。您需要根据设备的当前状态实现以下功能:

状态 动作按钮 源按钮
收音机 这将切换到下一个电台并播放 这将切换到音乐播放模式
播放音乐 这将暂停音乐 这将切换到待机模式
暂停音乐 这将播放音乐 这将切换到待机模式
待机 这将切换到收音机模式 这将不执行任何操作

前面的表格显示我们需要实现四个状态。根据状态的不同,按钮的行为将不同。

实现方式

首先,打开 StatePattern.xcodeproj 文件以查看项目的结构。

我们的音乐播放器设备由 Player.swift 类在 ConcreteClassWithState 文件夹中表示。

我们定义的公共接口,它定义了状态行为的方法签名,在 IPlayerState.swift 文件中。实现 IPlayerState 接口的每个状态都分组在 ConcreteState 文件夹中。

main.swift 文件包含我们的演示客户端:

实现

和往常一样,我们首先定义我们的接口。每个状态将为音频播放器上可见的两个按钮中的每一个实现一个行为,并将设备对象作为参数传递。这将允许当前状态对象操作音频播放器对象的当前状态:

protocol IAudioPlayerState{
  func buttonAction(player:AudioPlayer)
  func buttonSource(player:AudioPlayer)
}

然后,我们可以实现我们的音频播放器。init 方法正在等待接收一个 concreteState 实例,我们将记住它在状态变量中。

我们定义了两个按钮。每个按钮将通过调用适当的按钮将请求委派给状态对象。

然后我们添加了一个名为 CurrentState 的计算属性,允许我们返回音频播放器的当前状态或通过状态对象更改它。

AudioPlayer 类的最终代码如下:

import Foundation

class AudioPlayer {
  private var state:IAudioPlayerState!

  required init(state:IAudioPlayerState){
    self.state = state
  }

  //Press the Action Button
  func ActionButton(){
    state.buttonAction(self)
  }

  //Press the Source Button
  func SourceButton(){
    state.buttonSource(self)
  }

  var CurrentState:IAudioPlayerState{
    get{
      return state
    }
    set{
      state = newValue
    }
  }
}

我们的音乐播放器现在已准备就绪,状态对象实现的接口已定义。我们现在可以开始编写我们的第一个状态:RadioState 类。

此类表示音频播放器播放收音机的状态:

import Foundation

class RadioState: IAudioPlayerState {

  init(){
    print("RADIO MODE")
  }

  func buttonSource(player: AudioPlayer) {
    print("Changing to MUSIC Mode")
    player.CurrentState = MusicPlayingState()
  }

  func buttonAction(player: AudioPlayer) {
    print("Choosing next Station & playing it")
  }
}

实现相当简单;我们通知 init() 方法我们处于收音机模式。我们实现了 IAudioPlayerState 协议以及 buttonSourcebuttonAction 方法。

由于我们处于收音机模式,按下 buttonAction 将将其切换到下一个电台,点击源按钮将将其移动到 MusicPlaying 状态。

要更改音频播放器的状态,我们只需要调用播放器对象的 CurrentState 属性:

    player.CurrentState = MusicPlayingState()

使用相同的逻辑实现,并基于前一个示例中的表格,我们可以完成我们的代码。以下代码是 MusicPlayingState 类的实现:

class MusicPlayingState: IAudioPlayerState {

  init(){
    print("MUSIC PLAY MODE")
  }

  func buttonSource(player: AudioPlayer) {
    print("Changing source to Standby Mode")
    player.CurrentState = StandByState()
  }

  func buttonAction(player: AudioPlayer) {
    print("Changing to Pausing Mode")
    player.CurrentState = MusicPausedState()
  }
}

以下代码是 MusicPausedState 类的实现:

class MusicPausedState: IAudioPlayerState {

  init(){
    print("MUSIC PAUSED MODE")
  }

  func buttonSource(player: AudioPlayer) {
    print("Changing source to Standby Mode")
    player.CurrentState = StandByState()
  }

  func buttonAction(player: AudioPlayer) {
    print("Changing to playing Mode")
    player.CurrentState = MusicPlayingState()
  }
}

以下代码是StandBySTate类的实现:

class StandByState: IAudioPlayerState {

  init(){
    print("STANDBY MODE")
  }

  func buttonSource(player: AudioPlayer) {
    print("Changing to Radio Mode")
    player.CurrentState = RadioState()
  }

  func buttonAction(player: AudioPlayer) {
    print("cannot launch an action in standby mode")
  }
}

我们的游戏玩家现在已准备好工作。我们将编写我们的演示案例代码来测试实现的功能是否与样本介绍中给出的表格中描述的一致。

打开main.swift文件并编写以下代码:

let standbyMode = StandByState()
let player = AudioPlayer(state: standbyMode)

player.ActionButton()
player.SourceButton()

player.ActionButton()
player.SourceButton()

player.ActionButton()
player.ActionButton()
player.SourceButton()

首先,我们实例化第一个状态,我们的音频播放器将处于其中。我们决定将其置于StandBy模式。

然后,我们实例化我们的音频播放器,并将standbymode状态作为参数传递。最后,我们将通过点击动作或源按钮来模拟一个动作。让我们运行代码,你将看到以下示例中的结果:

Implementation

我们从待机模式开始。动作按钮告诉我们,在待机模式下不能使用它。因此,我们点击源按钮进入radioMode。我们再次按下动作按钮;这会切换到下一个电台并播放。

我们再次按下源按钮,通过播放音乐切换到音乐模式。我们按下动作按钮,音乐暂停。然后我们再次按下动作按钮,音乐再次播放。

最后,我们按下源按钮,音频播放器回到音频模式。

模板方法

模板方法模式是一个简单的模式,当需要通用行为但算法的细节必须针对子类具体化时使用。

角色

模板方法模式将算法的各个部分隔离开来。算法骨架在抽象类中定义,其中一些算法步骤被委派给其子类,而另一些则固定在抽象类本身中,不能在子类中重写。

设计

以下图表描述了模板方法的通用结构:

Design

参与者

本模式的参与者如下:

  • 定义模板方法并调用算法子部分签名的AbstractClass,由模板方法调用。

  • ConcreteClass实现了由AbstractClass的模板方法使用的抽象方法。可以有多个具体类。

协作

模板方法中定义的算法在通用 UML 类图中称为TemplateMethod(),并调用子类中算法的部分。

说明

你正在开发一个包含多个人物类型的新模拟游戏。每个角色都有几个属性,如金钱、幸福、疲劳、饥饿和知识。

这些人物中的每一个都可以“玩”一天。一天被分解为几个部分:

  • GetUp

  • EatBreakFast

  • DoWashingUp

  • GoToWork

  • Work

  • GoHome

  • DoPersonalActivites

  • EatDinner

  • Sleep

我们有三种人物类型:StudentSearcherFireMan;他们每个人都可以“玩”一天,但根据一天的不同阶段,他们的反应方式并不相同。

因此,我们将通过定义人物类型来在具体类中覆盖算法的部分。算法中唯一固定的部分是DoWashingUp函数。这部分不会也不会在子类中被覆盖。

实现

使用 Xcode 打开TemplateMethod项目。该项目相当简单。我们将在AbstractPersonage.Swift类中的TemplateMethod文件夹以及所有具体子类中找到实现算法三部分的具体类:SearcherStudentFireMan

实现

为了实现前面的示例,我们首先准备我们的抽象类,该类定义了一个人物。请记住,将此类视为抽象类。你不得直接在代码中实例化它,但必须只实例化AbstractPersonage的子类:

class AbstractPersonage {
  private final var fatigue = 100
  private final var money = 0
  private final var happiness = 100
  private final var hungry = 100
  private final var knowledge = 100
  private final var name:String!

  final var canBePaid: Bool = true

  required init(name: String) {
   self.name = name
  }

  func toString() {
    print("("Name: \(name) / fatigue : \(Fatigue) / happiness \(Happiness) / Hungry \(Hungry) / knowledge \(Fatigue) / money: \(Money) / ")
  }

  //Play a day for the Personage
  func playDay() {
    print("PLAYING DAY")
    print("Get Up!")
    getUp()
    print("Eat Breakfast")
    eatBreakfast()
    doWashingUp()
    print("Go to work")
    goToWork()
    print("Work")
    work()

    if canBePaid {
      print("Receive Pay")
      getPaid()
    }
    print("BackHome")
    backToHome()

    print("Do personal activities")
    doPersonalActivities()

    print("Eat dinner")
    eatDinner()

    print("Sleep")
    sleep()
  }

  func getUp() {
    Fatigue = 0
    Happiness = 25
    Hungry = -25
    Knowledge = 0
  }

  func eatBreakfast() {
    Fatigue = -5
    Happiness = 25
    Hungry = 60
    Knowledge = 0
  }
  final func doWashingUp() {
    print("do washing up")
  }

  func goToWork() {
    Fatigue = -15
    Happiness = -15
    Hungry = -10
    Knowledge = 0
  }
  func work(){
    Fatigue = -40
    Happiness = -25
    Hungry = -40
    Knowledge = 25
  }

  func getPaid() {
    Money = 1000
  }

  func backHome() {
    Fatigue = -15
    Happiness = 10
    Hungry = -10
    Knowledge = 0
  }

  func doPersonalActivities() {
    Fatigue = -15
    Happiness = 15
    Hungry = -10
    Knowledge = 0
  }

  func eatDinner() {
    Fatigue = -10
    Happiness = 5
    Hungry = 40
    Knowledge = 0
  }
  func sleep() {
    Fatigue = 90
    Happiness = 0
    Hungry = -5
    Knowledge = 2
  }

  var Fatigue: Int {
    get{
      return fatigue
    }
    set{
    fatigue += newValue
    }
  }

  var Hungry: Int {
    get{
      return hungry
    }
    set{
      hungry += newValue
    }
  }

  var Happiness: Int {
    get{
      return happiness
    }
    set{
      happiness += newValue
    }
  }

  var Money: Int {
    get{
      return money
    }
    set{
      money += newValue
    }
  }

  var Knowledge: Int {
    get{
      return knowledge
    }
    set{
      knowledge += newValue
    }
  }
}

在前面的代码中,我们可以区分三个部分。第一部分是私有变量声明。我们标记访问修饰符以避免在子类中进行修改:

  private final var fatigue = 100
  private final var money = 0
  …

我们的playDay模板方法调用算法的所有部分:

//Play a day for the Personage
  func playDay() {
    print("PLAYING DAY")
    print("Get Up!")
 getUp()
    print("Eat Breakfast")
 eatBreakfast()
 doWashingUp()
    print("Go to work")
 goToWork()
    print("Work")
 work()

    if canBePaid {
      print("Receive Pay")
 getPaid()
    }
    print("BackHome")
 backHome()

    print("Do personal activities")
 doPersonalActivities()

    print("Eat dinner")
 eatDinner()

 doWashingUp()

    print("Sleep")
 sleep()
  }

… 

然后,我们定义了算法的一部分的方法签名,我们最终将实现它们。在这里,我们为每个方法定义了一个默认实现:

func eatBreakfast() {
    Fatigue = -5
    Happiness = 25
    Hungry = 60
    Knowledge = 0
  }

  func goToWork() {
    Fatigue = -15
    Happiness = -15
    Hungry = -10
    Knowledge = 0
  }

//others methods 

最后,我们定义了计算属性,通过在属性分配新值时对其自身进行添加来修改 setter 行为:

  var Fatigue: Int {
    get{
      return fatigue
    }
    set{
    fatigue += newValue
    }
  }

  var Hungry: Int {
    get{
      return hungry
    }
    set{
      hungry += newValue
    }
  }

以下两个步骤使我们的示例变得更好:

  • 我们添加了一个必需的构造函数,其中注入一个名称到我们将要实例化的人物:

      required init(name: String) {
      self.name = name
      }
    
  • 我们定义了一个toString()方法,它将打印出人物的属性和值:

      func toString() {
        print("Name: \(name) / fatigue : \(Fatigue) / happiness \(Happiness) / Hungry \(Hungry) / knowledge \(Fatigue) / money: \(Money) / ")
      }
    

好吧,我们实现模板方法的抽象类已经完成。现在,我们有一个骨架来创建一个新的具体人物,例如,一个学生。

学生没有工作,所以不会获得报酬。学生在个人活动中读书。

因此,我们将创建一个新的Student类,该类实现了包含模板方法的抽象类,并且我们只覆盖了父类中改变的部分的算法:

class Student: AbstractPersonage {

  required init(name: String) {
    super.init(name: name)
    //student cannot be paid
    canBePaid = false
  }

  override func doPersonalActivities() {
    //student Read Books during its personal activities
    //so life indicators must be updated
    Fatigue = -5
    Happiness = 15
    Hungry = -5
    Knowledge = 15
  }
}

同样地,我们定义了SearcherFireMan类,它们实现了我们的抽象类,并且两者都可以获得报酬,但金额并不相同。此外,每个类都必须覆盖算法的一些部分,以便更精确地反映该类所代表的实体的特定性:

对于Searcher类,我们将按照以下方式实现AbstractPersonage协议:

class Searcher: AbstractPersonage {

  override func getPaid() {
    Money = 3000/30
  }

  override func sleep() {
    //Searcher sleep very well
    Fatigue = 90
    Happiness = 0
    Hungry = -5
    Knowledge = 10
  }

  override func doPersonalActivitie() {
    //Searcher Read ScientificBooks during its personal activities
    //so life indicators must be updated
    Fatigue = -5
    Happiness = 10
    Hungry = -5
    Knowledge = 25
  }
}

对于FireMan类,我们将按照以下方式实现它:

import Foundation

class FireMan: AbstractPersonage {

  override func getPaid() {
    Money = 2500/30
  }

  override func sleep() {
    //FireMan doesn't sleep a lot
    Fatigue = 80
    Happiness = 5
    Hungry = -5
    Knowledge = 0
  }

  override func doPersonalActivities() {
    //FireMan makes lot of sports during personal activities
    //so life indicators must be updated
    Fatigue = -10
    Happiness = 5
    Hungry = -5
    Knowledge = 15
  }

  override func work() {
    Fatigue = -25
    Happiness = -55
    Hungry = -45
    Knowledge = 10
  }

}

我们的模式方法和具体类现在已准备就绪。我们现在可以在main.swift文件中编写代码。我们的简单客户端将实例化一个名为Simon的学生,一个名为Natasha的搜索者,以及一个名为Edward的消防员。

我们将在模拟他们 30 天生活之前显示他们的属性。然后,我们将使用以下代码告诉这三个人物生活 30 天:

student.toString()
searcher.toString()
fireMan.toString()

然后,我们将使用for循环模拟 30 天的生活:

for i in 1...30{
  student.playDay()
  searcher.playDay()
  fireMan.playDay()
}

在这 30 天生命周期的最后,我们将检查每个对象的属性:

print("- **** 30 days later:")
student.toString()
searcher.toString()
fireMan.toString()

最终的代码如下:

  import Foundation

let student = Student(name: "Simon")
let searcher = Searcher(name: "Natasha")
let fireMan = FireMan(name:"Edward")

print("- **** Starting with:")
student.toString()
searcher.toString()
fireMan.toString()

//Play a month
for i in 1...30{
  print("**************")
  print("Play Day \(i) ")
  print("**************")
  student.playDay()
  searcher.playDay()
  fireMan.playDay()
}
print("- **** 30 days later:")
student.toString()
searcher.toString()
fireMan.toString()

点击运行按钮。在控制台,你将在 30 天生命周期的结束后看到结果:

实现

摘要

策略模式和状态模式之间存在相似之处,但主要区别在于意图:

  • 策略对象封装了一个算法

  • 状态对象封装了一个依赖于对象内部状态的行为

在这两种模式中,我们都使用多态。因此,对于这两种模式,我们定义一个父接口或抽象类,然后在具体的子类中实现父接口或抽象类中定义的方法。该模式维护上下文,并根据它决定使用适当的对象。这两种模式之间最大的区别在于,在策略模式中,我们将算法封装到策略类中,但在状态模式中,我们将状态封装到状态类中。

模板方法模式更像是策略模式;它基于算法的正确应用。在这个模式中,所有步骤都在模板方法中指定,一些子部分被延迟到子类中。

在下一章中,我们将学习如何使用两种其他的行为模式:责任链和命令。

两者都用于将动作请求传递给适当的对象。

第六章。行为模式 – 责任链和命令模式

在本章中,我们将继续探讨行为模式——责任链命令模式。这两个模式都涉及将请求传递给将执行操作的适当对象。

这两种模式之间的主要区别在于请求在对象之间传递的方式。

在本章中,我们将讨论以下主题:

  • 责任链模式

  • 命令模式

责任链模式

当你编写应用程序时,可能一个对象生成的事件需要由另一个对象处理。你可能还希望处理对另一个对象不可访问。

角色

在本节中,你会注意到责任链模式以这种方式创建对象链,如果链中的对象无法处理请求,它将请求发送到下一个对象,即后续对象,直到其中一个可以处理请求。

此模式允许一个对象发送请求,而无需知道哪个对象将接收并处理它。请求从一个对象发送到另一个对象,使它们成为链的一部分。链中的每个对象都可以处理请求,将其传递给其后续对象,或者两者都做。

当你需要使用此模式时:

  • 你希望解耦请求的发送者与接收者,允许其他对象也处理请求

  • 能够处理请求的对象是工作链的一部分,请求从一个对象传递到另一个对象,直到至少有一个这些对象可以处理它。

  • 你希望允许能够处理请求的对象按照优先级顺序排列,并且可以重新排序,而不影响调用组件

设计

以下图展示了责任链模式的通用表示:

设计

参与者

此模式有三个参与者,如下所示:

  • AbstractHandler:这定义了请求的接口并实现了责任链模式的关联。

  • ConcreteHandlers:这些对象可以处理它们负责的请求。如果它们无法处理请求,则将请求传递给其后续对象或停止链。

  • Client:客户端将请求发送到可能处理请求的链中的第一个对象。

协作

以下序列图展示了对象之间的协作:

协作

客户端将请求发送到链中的第一个对象。然后,此请求在整个链中传播,直到至少有一个链中的对象可以处理它。

描述

假设你正在监督一个移动应用程序的开发,并且你想要根据记录器的优先级以不同的方式处理一些日志消息。

你定义了三种类型的优先级,这意味着三种日志级别:DEBUGINFOERROR

根据日志消息的级别,你可以如下处理:

  • 如果级别(或优先级)是 DEBUG,那么这将由标准输出记录器处理

  • 如果级别是 INFO,那么我们将使用标准输出记录器和电子邮件记录器,它们将发送包含消息的电子邮件

  • 如果级别是 ERROR,那么三个日志记录器都将处理消息:标准输出记录器、电子邮件记录器和错误记录器

如我们所见,我们需要以下顺序定义对象链:StdOutLoggerEmailLoggerErrorLogger

客户端将只调用第一个具体处理程序,即可能处理请求的类:StdOutLogger

实现

要实现我们的模式,我们首先需要准备我们的抽象类。记住,在 Swift 中,抽象类实际上并不存在。我们将以类的方式编写我们的抽象类,但需要重写的方法将具有以下声明:

preconditionFailure("Must be overridden")

在这种情况下,如果调用抽象类的代码而不是派生类中可用的代码,将引发类似于以下异常:

实现

现在我们开始实施。

首先,打开你可以在 第六章 文件夹中找到的 ChainOfResponsibilityPattern 项目。

项目按照以下结构组织:

实现

这里没有什么是复杂的;我们将定义我们的链并调用 main.swift 文件。抽象类在 Logger.swift 文件中定义,我们的三个 concreteHandlers 类有自己的 Swift 文件。

我们将如下定义抽象类:

class Logger {
  static var ERROR = 1
  static var INFO = 2
  static var DEBUG = 3

  var mask:Int?
  var next:Logger?

  func nextHandler(nextLogger:Logger) -> Logger? {
    next = nextLogger
    return next
  }

  func message(message: String, priority: Int){
    if priority <= mask {
      writeMessage(message)
      if let next = next {
        next.message(message, priority: priority)
      }
    }
  }

  func writeMessage(message: String) {
    preconditionFailure("Must be overridden")
  }

  static func prepareDefaultChain() -> Logger? {
    var l: Logger?
    var l1: Logger?

    l = StdOutLogger(mask: Logger.DEBUG)
    l1 = l!.nextHandler(EmailLogger(mask: Logger.INFO))
    l1 = l1!.nextHandler(ErrLogger(mask: Logger.ERROR))
    return l
  }}

我们定义了三个静态变量,将代表我们不同的日志级别:ERRORINFODEBUG

然后,我们还有两个其他变量被声明,具体如下:

  • 掩码:这个变量是对象固有的,将在处理程序的初始化期间设置。这个变量将用于将其值与接收到的请求级别进行比较,这意味着如果掩码小于或等于级别,对象将能够处理请求。

  • 下一个:这个变量也是对象固有的,这允许链式调用。这个变量包含将请求传递的下一个 ConcreteHandler

我们有以下三个函数:

  • func nextHandler(…): 这是一个允许你将下一个具体处理程序分配给 next 变量的函数。请注意,这个函数返回一个记录器。这被称为 nextLogger

    因此,如果我们编写以下语句:

    l = StdOutLogger(mask: Logger.DEBUG)
    l1 = l!.nextHandler(EmailLogger(mask: Logger.INFO))
    

    然后,l1 是一个 EmailLogger 实例,而不是 StdOutLogger

  • func message(…): 这是主要函数,它负责(或不)处理请求和/或将其传递给链中的下一个对象。

  • writeMessage(…):此函数由message(…)函数调用,以模拟对请求应用的工作。在这里,我们只会显示与当前具体处理器对象相关联的消息。由于我们处于抽象类中,我们添加了一个preconditionfailure(…)语句,它将通知我们this函数必须在派生类中重写。如果代码执行且派生类没有重写此方法,将引发一个致命错误,这在模式的实现部分中有所解释。

  • prepareDefaultChain(…):这是一个类函数,封装了我们默认链的创建。

我们的抽象类现在已经准备好了;我们只需要编写我们的派生类。记住,writeMessage(…)函数必须被重写,并且我们需要初始化我们具体处理器的掩码。

首先,让我们看看具体的StdOutLogger处理器,如下所示:

class StdOutLogger: Logger {
  init(mask: Int) {
    super.init()
    self.mask = mask
  }

  override func writeMessage(message: String) {
    print("Sending to StdOutLogger: \(message)")
  }
}

接下来,我们有EmailLogger类:

class EmailLogger: Logger {
  init(mask: Int) {
    super.init()
    self.mask = mask
  }

  override func writeMessage(message: String) {
    print("Sending by Email: \(message)")
  }
}

此外,我们还有ErrLogger类:

class ErrLogger: Logger {
  init(mask: Int) {
    super.init()
    self.mask = mask
  }

  override func writeMessage(message: String) {
    print("Sending to ErrorLogger: \(message)")
  }
}

我们的所有具体处理器现在都准备好了。现在是时候在main.swift文件中编写我们的测试了。

我们首先使用Logger类的函数prepareDefaultChain准备我们的链:

print("Building the Chain")
var l: Logger?

l = Logger.prepareDefaultChain()

然后,我们向链的第一个对象(lStdOutLogger)发送一个请求(一个包含日志类型的字符串消息):

print("- *** stdOutLogger:")
// Handled by StdOutLogger
l?.message("Entering the func Y()", priority: Logger.DEBUG)

print("- StdOutLogger && EmailLogger:")
// Handled by StdOutLogger && EmailLogger
l?.message("Step 1 Completed", priority: Logger.INFO)

print("- all three loggers:")
// Handled by all Logger
l?.message("An error occurred", priority: Logger.ERR)

现在,我们将构建并运行项目。你将在控制台上看到以下结果:

实现

控制台输出非常清晰。第一个处理器只处理了第一个请求,第二个请求由StdOutLogger类和EmailLogger类处理。第三个请求由所有三个处理器处理。

命令模式

此模式背后的概念是将请求转换成一个对象,以便简化某些操作,例如撤销/重做、将请求插入队列或跟踪请求。

角色

命令模式在请求操作的客户端和可以执行它的对象之间创建距离。请求被封装成一个对象。此对象包含对将实际执行操作的接收者的引用。

实际操作由接收者管理,命令就像一个指令;它只包含对调用者的引用,即执行动作的对象,以及一个执行函数将调用工作者的实际操作。

此模式允许以下功能:

  • 向不同的接收者发送请求

  • 排队、记录和拒绝请求

  • 可撤销的操作(execute方法可以记住状态,并允许你回到那个状态)

  • 将请求封装在对象中

  • 允许客户端使用不同的请求进行参数化

设计

通用图类表示如下:

设计

参与者

参与此模式的类如下:

  • Command:这声明了执行操作的接口。

  • ConcreteCommand:通过在 Receiver 上调用相应的操作来实现 Command 接口,并使用 execute 方法。它定义了 Receiver 类和动作之间的链接。

  • Client:创建一个 ConcreteCommand 对象并设置其接收者。

  • Invoker:请求命令执行请求。

  • Receiver:知道如何执行操作。

协作

以下序列图定义了所有参与命令模式的对象的协作:

协作

让我们详细讨论前面的图示:

  • 客户端请求执行命令并指定其接收者

  • 客户端随后将命令发送给执行者,执行者将其存储(或将其放入队列系统,如果某些操作需要在执行命令之前执行)以便稍后执行

  • 然后,调用执行者来启动命令,通过在适当的命令对象上调用执行函数

  • 具体命令请求接收者执行适当的操作

插图

想象一下,您的公司正在开发一个新的通用控制器,它可以管理多达四个命令。这个控制器有四个插槽,我们可以为每个插槽添加两个命令。在每个插槽附近,我们有两个按钮:“开”和“关”按钮。

您的团队已经有两个对象及其规格,允许遥控器操作它们:

  • 我们将与之交互的对象是一个灯具和一个音频播放器

  • 灯具只能打开和关闭

  • 音频播放器可以打开或关闭,我们可以播放或停止音乐

您的工作是构思将被存储在通用遥控器中的命令。

当我们按下插槽的开关按钮时,应向适当的设备(音频播放器或灯具)发送适当的命令。

注意

在这个例子中,我们不会实现撤销/重做操作。我们将在下一章展示另一个针对这种情况的专用模式。

实现

打开名为 CommandPattern.xcodeproj 的 Xcode 项目。以下是我们的项目组织结构:

实现

我们项目的结构反映了我们在命令模式的 设计 部分看到的类图:

  • 我们有 Invoker 文件夹,其中包含我们的 UniversalRemoteController 对象

  • Receiver 文件夹包含两个设备,它们将能够接收命令以执行适当的操作

  • Interface 文件夹包含了命令的定义

  • ConcreteCommand 文件夹包含了我们想要与我们的通用遥控器一起使用的所有命令

  • 最后,main.swift 文件包含了允许我们查看演示的代码

为了实现这个例子,让我们从定义我们的 Command 接口开始。

我们只需要一个 execute() 方法来执行命令:

protocol ICommand {
  func execute()
}

在我们编写具体的命令对象之前,让我们看看我们的 LightAudioPlayer 对象是如何实现的:

class Light {

  func on() {
    print("Light is On")
  }

  func off() {
    print("Light is Off")
  }
}

这相当简单;on() 函数会打开灯光,而 off() 函数会关闭它。

现在,让我们定义 AudioPlayer 类:

class AudioPlayer {

  enum AudioPlayerState {
    case On
    case Off
    case Playing
  }

  private var state = AudioPlayerState.Off

  func on() {
    state = AudioPlayerState.On
    print("Audio Player is On")
  }

  func off() {
    state = AudioPlayerState.Off
    print("Audio Player is Off")
  }

  func playCD(){
    if state == AudioPlayerState.Off {
      print("doesn't work : the audio player is currently off")
    } else {
      state = AudioPlayerState.Playing
      print("AudioPlayer is playing")
    }
  }

  func stopCD(){
    if state == AudioPlayerState.Off {
      print("doesn't work : the audio player is currently off")
    }
    if state == AudioPlayerState.On {
      print("doesn't work : the audio player currently doesn't play music")
    } else {
      state = AudioPlayerState.On
      print("AudioPlayer has stopped to play music")
    }
  }
}

这个对象更复杂。我们拥有相同的 on()off() 方法,但我们还有 playCD()StopCD() 方法。

我们可以看到这个对象有一个内部状态。状态根据调用的函数和状态也会用来控制请求的函数是否可行。

现在我们已经拥有了所有必要的信息,我们可以开始编写我们的命令。

让我们从灯光开始。我们希望能够使用我们的通用遥控器根据插槽附近的按钮来打开或关闭灯光。

因此,我们首先编写我们的 LightOnCommand 具体命令对象:

class LightOnCommand: ICommand {

  var light:Light

  init(light: Light) {
    self.light = light
  }

  func execute() {
    self.light.on()
  }
}

在这里,我们创建了一个名为 LightOnCommand 的对象,该对象实现了 ICommand 接口。

命令需要知道接收器对象是什么,因此我们在初始化对象时传递一个参数给它:

 init(light: Light) {
    self.light = light
  }

然后,execute 方法封装了对 Light 对象 on() 函数的调用,以有效地处理命令。

就这样;你的 LightOnCommand 对象现在准备好了。

我们对 LightOffCommand 类也做了同样的处理,并在适当的地方进行修改,以便使用 Light 对象的 off() 函数而不是 on()

class LightOffCommand: ICommand {

  var light:Light

  init(light: Light) {
    self.light = light
  }

  func execute() {
    self.light.off()
  }
}

我们控制灯光的命令都已经准备好了。现在让我们看看我们将为音频播放器做些什么。我们希望能够打开或关闭音频播放器,播放或停止音乐。这些命令与我们之前对灯光所做的是相似的。

AudioPlayerOnCommand 类编写如下:

class AudioPlayerOnCommand: ICommand {
  var audioPlayer:AudioPlayer

  init(audioPlayer:AudioPlayer) {
    self.audioPlayer = audioPlayer
  }

  func execute() {
    audioPlayer.on()
  }
} 

AudioPlayerOffCommand 类编写如下:

class AudioPlayerOffCommand: ICommand {
  var audioPlayer:AudioPlayer

  init(audioPlayer:AudioPlayer) {
    self.audioPlayer = audioPlayer
  }

  func execute() {
    audioPlayer.off()
  }
}

AudioPlayerPlayCdCommand 类编写如下:

class AudioPlayerPlayCDCommand: ICommand {
  var audioPlayer:AudioPlayer

  init(audioPlayer:AudioPlayer) {
    self.audioPlayer = audioPlayer
  }

  func execute() {
    audioPlayer.playCD()
  }
}

AudioPlayerStopCDCommand 类编写如下:

class AudioPlayerStopCDCommand: ICommand {
  var audioPlayer:AudioPlayer

  init(audioPlayer:AudioPlayer) {
    self.audioPlayer = audioPlayer
  }

  func execute() {
    audioPlayer.stopCD()
  }
}

到目前为止,所有需要的命令都已经编写完成。

我们希望使用只有四个插槽的遥控器,如下面的图所示。通过遥控器,我们希望能够操作两个灯光:一个在卧室,一个在大厅,以及一个音频播放器来播放和停止音乐:

实现

为什么不创建一个命令,该命令可以在同一个命令对象中打开音频播放器和播放音乐呢?确实,使用我们的遥控器只执行开或关命令是毫无用处的。我们想要的只是播放或停止音乐。

想象一下,你只想通过按一个按钮就能打开音频播放器和播放光盘;同样地,你只想通过按一个按钮就能停止光盘播放器和关闭音频播放器。

为了实现这一点,我们只需要在命令的 execute 函数中封装适当的音频播放器对象的函数。当我们的遥控器调用 execute 方法时,我们首先调用 audioPlayer 类的 on 函数,然后调用 playCD() 函数:

class AudioPlayerSetOnAndPlayCommand: ICommand {
  var audioPlayer:AudioPlayer

  init(audioPlayer:AudioPlayer) {
    self.audioPlayer = audioPlayer
  }

  func execute() {
    audioPlayer.on()
    audioPlayer.playCD()
  }

}

同样,我们继续我们的 StopMusicAndSetOff 命令:

class AudioPlayerStopMusicAndSetOff: ICommand {
  var audioPlayer:AudioPlayer

  init(audioPlayer:AudioPlayer) {
    self.audioPlayer = audioPlayer
  }

  func execute() {
    audioPlayer.stopCD()
    audioPlayer.off()
  }

}

我们设备已准备好接受命令,命令对象也已就绪。在我们开始编写演示代码之前,让我们看看遥控器是如何工作的:

class UniversalRemoteControl {
  var onCommands = [ICommand]()
  var offCommands = [ICommand]()

  init() {
    for _ in 1...4 {
      onCommands.append(NoCommand())
      offCommands.append(NoCommand())
    }
  }

  func addCommandToSlot(slot:Int, onCommand:ICommand, offCommand:ICommand) {
    onCommands[slot] = onCommand
    offCommands[slot] = offCommand
  }

  func buttonOnIsPushedOnSlot(slot:Int) {
    onCommands[slot].execute()
  }

  func buttonOffIsPushedOnSlot(slot:Int) {
    offCommands[slot].execute()
  }
}

当遥控器初始化时,四个槽位分配了一个NoCommand对象。该对象如下:

class NoCommand: ICommand {

  func execute() {
    print("No command associated to this")
  }
}

因此,如果我们不使用addCommandToSlot(…)函数,每个按钮将调用NoCommand对象的execute函数,这意味着没有事情要做。

遥控器有两个按钮靠近每个槽。根据按钮和槽,将调用buttonOnIsPushedOnSlot(…)buttonOffIsPushedOnSlot

由于命令存储在onCommandsoffCommands数组中,当调用addCommandToSlot时,我们将调用相应对象的execute命令。要执行槽的开启命令,我们将运行以下代码:

    onCommands[slot].execute()

要执行相同槽位的关闭命令,我们也将运行以下代码:

    offCommands[slot].execute()

在这里,slot是按钮槽的索引。现在,是时候实现我们的演示代码了。

首先,我们初始化我们的遥控器,创建我们的audioPlayer,并创建我们的两个灯:卧室灯和走廊灯:

let uRemoteControl = UniversalRemoteControl()

let audioPlayerLivingRoom = AudioPlayer()
let lightBedroom = Light()
let lightHall = Light()

然后,我们创建所有命令对象:

// MARK: Definition of our commands
let bedroomLightOnCommand = LightOnCommand(light: lightBedroom)
let bedroomLightOffCommand = LightOffCommand(light: lightBedroom)

let hallLightOnCommand = LightOnCommand(light: lightHall)
let hallLightOffCommand = LightOffCommand(light: lightHall)

let audioPlayerLivingRoomOnCommand = AudioPlayerOnCommand(audioPlayer: audioPlayerLivingRoom)
let audioPlayerLivingRoomOffCommand = AudioPlayerOffCommand(audioPlayer: audioPlayerLivingRoom)

let audioPlayerOnAndPlayLivingRoom = AudioPlayerSetOnAndPlayCommand(audioPlayer: audioPlayerLivingRoom)
let audioPlayerStopAndOffLivingRoom = AudioPlayerStopMusicAndSetOff(audioPlayer: audioPlayerLivingRoom)

当我们的命令准备就绪后,我们可以使用addCommandToSlot函数将它们分配给遥控器:

// Mark: Assign commands to the remote controller
uRemoteControl.addCommandToSlot(0, onCommand: bedroomLightOnCommand, offCommand: bedroomLightOffCommand)
uRemoteControl.addCommandToSlot(1, onCommand: hallLightOnCommand, offCommand: hallLightOffCommand)

uRemoteControl.addCommandToSlot(2, onCommand: audioPlayerLivingRoomOnCommand, offCommand: audioPlayerLivingRoomOffCommand)
uRemoteControl.addCommandToSlot(3, onCommand: audioPlayerOnAndPlayLivingRoom, offCommand: audioPlayerStopAndOffLivingRoom)

演示所需的最后一件事是模拟每个按钮的按下:

// Mark: Usage of the remote controller
uRemoteControl.buttonOnIsPushedOnSlot(0)
uRemoteControl.buttonOffIsPushedOnSlot(0)

uRemoteControl.buttonOnIsPushedOnSlot(1)
uRemoteControl.buttonOffIsPushedOnSlot(1)

uRemoteControl.buttonOnIsPushedOnSlot(2)
uRemoteControl.buttonOffIsPushedOnSlot(2)

uRemoteControl.buttonOnIsPushedOnSlot(3)
uRemoteControl.buttonOffIsPushedOnSlot(3)

注意

注意,我们还没有添加并发保护。如果命令被多个组件使用,我们应该确保添加并发保护。

为了实现这一点,我们需要创建一个队列,该队列将接收所有命令,以同步方式执行它们,并且第一个进入队列的命令将是第一个执行的命令(先进先出)。要了解如何实现并发保护,您可以查看第七章中中介者模式的实现,行为型模式 – 迭代器、中介者和观察者以及同一章节中关于并发保护的注释。

点击构建并运行演示。

您现在将在控制台看到以下结果,对应于通用遥控器上按下的每个按钮:

实现

责任链模式和命令模式之间的比较

这两种模式之间的区别在于请求解耦的方式。

在责任链模式中,请求被传递给潜在的接收者,而命令模式使用一个封装请求的命令对象。

以下表格描述了责任链模式和命令模式之间的区别:

责任链 命令
客户端创建 处理器对象 命令对象
不同类型的 不同级别的处理器类 命令类和接收者类
客户端可以与 多个处理器 不同的接收者
客户端调用 处理器对象 接收器对象
在处理器中完成工作 处理器中的HandleRequest 接收器中的ActionToPerform
基于决策 处理器中的掩码 命令中的路由

摘要

我希望这一章对您来说很有趣。在这一章中,我们学习了如何在责任链模式和命令模式中解耦发送者和接收者,从而提高系统的分层和可复用性。

在下一章中,我们将探讨三种新的模式:迭代器模式、中介者模式和观察者模式,这些模式专注于在保持对象独立性的同时进行对象间的通信。

第七章。行为模式 – 迭代器、中介者和观察者

本章向您介绍了三种其他的行为模式,这些模式支持对象之间的通信。对象保持其独立性,有时甚至保持匿名性。迭代器模式通常与数组、集合和字典对象一起使用。中介者允许两个对象之间进行通信,而无需知道彼此的身份,观察者模式则反映了在分布式系统中广为人知的发布/订阅方法。

本章分为三个部分:

  • 迭代器模式

  • 中介者模式

  • 观察者模式

迭代器模式

这个模式在许多使用数组或对象集合的语言中很常见。它允许遍历集合中包含的对象列表。

角色

迭代器模式允许你按顺序遍历对象的聚合对象,而无需知道集合的结构。

设计

这里,你可以找到该模式的通用 UML 类图,但请注意,我们不会使用这种方式来实现它。

事实上,Swift 提供了一些类型,可以简化迭代器模式的实现,无需手动生成所有必需的要求。

为什么要重新发明轮子?以下图表示了通用的 UML 类图:

设计

参与者

就像到目前为止描述的每一个模式一样,即使在我们实现该模式时不会看到所有参与者,我仍会告诉你这个模式中的参与者:

  • Collection: 这是一个抽象类,它实现了集合与项之间的关联以及 CreateIterator() 方法

  • ConcreteCollection: 这是一个具体的集合子类,它将 CurrentItem 对象链接到 ConcreteItem 对象,并将 Iterator 接口链接到 ConcreteIterator 对象

  • Iterator: 这是一个抽象类,它实现了迭代器和集合项之间的关联以及方法

  • ConcreteIterator: 这是一个具体子类,它将我们的 currentItem 链接到 ConcreteItem 对象

  • Item: 这是集合项的抽象类

  • ConcreteItem: 这是一个由 ConcreteIteratorConcreteCollection 使用的具体 Item 子类

协作

迭代器会记住集合中的当前项。它还可以计算并预测迭代中的下一个对象。

插图

你正在开发一个游戏,你可以有最多四个玩家。你想要能够遍历所有四个玩家来进行某些操作。在我们的示例中,我们将显示每个玩家的名字。

实现

这里提供的方法来自在 lillylabs.no/2014/09/30/make-iterable-swift-collection-type-sequencetype/ 网站上发现的思路。

好吧,根据我们的说明,我们想要能够使用 for…in 循环结构来迭代。

全靠 Swift,我们有一些元素将简化此模式的实现。

事实上,Swift 提出了一个 SequenceType 协议和一个 AnyGeneratorType<T> 类,该类实现了 GeneratorType 协议。

SequenceType 协议定义了一个协议,允许我们使用 for…in 循环结构迭代集合中的元素。它要求类实现 generate() 方法,该方法返回一个符合 GeneratorType 协议的对象。

<T> 表示任何类型的 Item

说了这么多,我们如何使用所有前面的函数轻松地迭代任何类型的集合?比如说,我们有一个以下类:

class Player {
  var name: String!

  init(name: String) {
   self.name = name
  }
}

因此,我们定义了一个简单的 Player 类,我们在构造函数中传递一个字符串,它对应于玩家的名字。

我们假设我们的游戏中我们有四个玩家,并希望能够迭代每个玩家以显示他们的名字。

因此,最终的测试代码将类似于以下内容:

for player in players {
  print("analysing \(player.name)")
}

现在如何用奖金来完善我们的代码,一些可以与任何我们想要迭代的类一起工作的东西?嗯,我们将使用 Swift 提供的另一个概念:扩展

首先要做的事情是创建一个对象或结构体,它将允许我们迭代任何类类型:

struct OurCollection<T> {
  let items: [T]
} 

因此,我们定义了一个我们称之为 OurCollection 的结构体,其中项目类型为 T

现在,我们将能够写出以下内容:

let player1 = Player(name: "Helmi")
let player2 = Player(name: "Raphael")
let player3 = Player(name: "Adrien")
let player4 = Player(name: "Alain")

let players = OurCollection(items:[player1,player2, player3, player4])

然而,for…in 循环仍然不会工作,如下面的截图所示:

实现

即使玩家没有实现 SequenceType 协议。这里就是魔法:

extension OurCollection: SequenceType {
  typealias Generator = AnyGenerator<T>

  func generate() -> Generator {
    var i = 0
    return anyGenerator {
      return i >= self.items.count ? nil : self.items[i++]
    }
  }
}

哇!这里有很多新东西:

首先,我们通过告诉我们要实现 SequenceType 协议来创建 OurCollection 结构体的一个扩展。

因此,我们实现了 generate() 方法,该方法将返回迭代中的下一个类型 T 对象。注意以下行:

 func generate() -> Generator {

生成器是 AnyGenerator<T> 的别名:

  typealias Generator = AnyGenerator<T>

我们使用这个来简化编写。我们可以移除类型别名声明并写:

func generate() -> AnyGenerator<T> {

另一个函数是这里看到的 anyGenerator 函数:

 return anyGenerator {
 return i >= self.items.count ? nil : self.items[i++]
 }

Swift 2.0 文档说明,anyGenerator 函数具有以下签名:

func anyGenerator<Element>(body: () -> Element?) -> AnyGenerator<Element>

这个函数的目的是返回一个 GeneratorType 实例,其 next 方法调用 body 并返回结果。

因此,我们从索引 0 开始到 self.items.count 值的索引,并在新的 GeneratorType 实例中添加 sel.items[i++]。当 i 大于项目数组中的元素数量时,返回新的 GeneratorType 实例。

我们也可以写出如下函数:

  func generate() -> Generator {
    var i = 0
   let seq = anyGenerator {i < self.items.count ? self.items[i++] : nil}
    return seq
 }

同样,还有这样的:

  func generate() -> Generator {
    var i = 0
   return anyGenerator {i < self.items.count ? self.items[i++] : nil}
 }

这里,我们使用一个带有 anyGenerator 函数的闭包来返回一个我们可以迭代的新的元素序列。我们的最终代码如下:

import Foundation

struct OurCollection<T> {
  let items: [T]
}

class Player {
  var name: String!

  init(name: String) {
   self.name = name
  }
}

extension OurCollection: SequenceType {
  typealias Generator = AnyGenerator<T>

  func generate() -> Generator{
    var i = 0
    // Construct a AnyGenerator<T> instance, passing a closure
    // that returns the next type T object in the iteration
    return anyGenerator {
      return i >= self.items.count ? nil : self.items[i++]
    }
  }
}

let player1 = Player(name: "Helmi")
let player2 = Player(name: "Raphael")
let player3 = Player(name: "Adrien")
let player4 = Player(name: "Alain")

let players = OurCollection(items:[player1,player2, player3, player4])

for player in players {
  print("Name: \(player.name)")
}

打开 iteratorPattern 项目,构建并运行它。你现在将看到以下结果:

实现

中介者模式

中介者模式用于减少相互通信的类之间的耦合。

角色

此模式构建了一个对象,该对象管理两个或更多类之间的通信。

这些类不知道彼此的实现。消息是从类发送到中介器对象的。

中介者模式定义了一个对象,该对象封装了一组对象如何相互通信的方式。中介者通过保持对象不显式地相互引用来促进松散耦合,它还允许你独立地改变它们的交互。

中介器是一个用于解耦许多对等的中间人。当我们想要设计可重用组件,但潜在可重用部分的依赖关系表现出“意大利面代码”现象时,可以使用此模式。

设计

以下类图展示了中介者模式的通用结构:

设计

参与者

在此模式中,我们发现以下参与者:

  • Mediator:这定义了与元素通信的中介器接口

  • ConcreteMediator:这实现了元素之间的协调并管理与元素的联系

  • Elements:这是一个元素抽象类,它引入了常见的属性、属性和方法

  • ConcreteElement1ConcreteElement2:这些是混凝土元素类,它们与中介器通信而不是与其他元素通信

协作

元素向中介器发送消息并从中接收消息。中介器实现了元素之间的协作和协调。

插图

你正在编写一个允许用户相互通信的系统。通信不是直接从对等方发送到另一个对等方。我们将使用一些中介器来管理所有用户及其之间的通信。

对于此,每个由中介器管理的用户都将被注册(添加)到中介器中。然后,当用户发送消息时,我们将中介器对象作为参数传递,通知系统这是将要广播给由中介器管理的所有其他用户的消息,调用 receiveMessage 函数。

实现

打开 MediatorPattern Xcode 项目。这是一个命令行项目,其组织结构如下截图所示:

实现

我们项目的组织结构不超过我们在类图中描述的参与者。我们在 Mediator 文件夹中检索 MediatorProtocolConcreteMediator 对象,我们的 Elements 和具体元素 UserProtocolUserElements 文件夹中,最后,main.swift 文件包含我们的客户端代码以模拟项目。

首先,我们在 UserProtocol.swift 文件中定义 userProtocol

protocol UserProtocol {
  func sendMessage(mediator:MediatorProtocol, message:AnyObject)
  func receiveMessage(message:AnyObject)
}

sendMessage 方法将被用来告诉传入的中介参数当前 concreteUser 的消息内容。当中介向所有用户广播消息时,将触发 receiveMessage 方法。

然后,在 User.swift 文件中,我们实现我们的协议,如下所示:

class User: UserProtocol {
  var name: String

  init(name: String){
   self.name = name
  }

  func sendMessage(mediator:MediatorProtocol, message:AnyObject){
    mediator.broadcastMessage(self, message: message as AnyObject)
  }

  func receiveMessage(message:AnyObject){
    print("\(self.name) received \(String(message))")
  }
}

这里,我们在构造函数中添加一个参数来传递用户的名称。

sendMessage 方法中,我们看到我们正在调用传入参数的中介的 broadcastMessage 方法。

receiveMessage 方法被触发时,它将显示当前用户的名称以及接收到的消息。

接下来,让我们看看 MediatorProtocol 是如何定义的:

protocol MediatorProtocol {
  var users:[UserProtocol]? { get }

  func broadcastMessage(sender:UserProtocol, message:AnyObject)
  func register(users: UserProtocol)
}

MediatorProtocol 管理元素集合;这里,它是 Users。它还可以向特定用户广播特定消息。

为了将用户添加到中介管理的用户集合中,我们添加了一个 Register 方法。

让我们看看我们是如何在 Mediator.swift 文件中实现所有这些的:

class Mediator: MediatorProtocol {
  private let queue = dispatch_queue_create("MediatorPattern", DISPATCH_QUEUE_CONCURRENT)
  var users:[UserProtocol]? = [User]()

  func broadcastMessage(sender:UserProtocol, message:AnyObject){
    dispatch_barrier_sync(self.queue, { () in

      guard let users = self.users else {
        return
      }

      for u in users{
        if u as! User !== sender as! User {
          u.receiveMessage(message)
        }
      }

    })
  }
  func register(user: UserProtocol){
    dispatch_barrier_sync(self.queue, { () in
      users?.append(user)
    })
  }
}

首先,我们初始化一个 User 数组,它准备好管理用户集合。

register 方法中,我们接收一个用户参数,该参数被添加到中介管理的集合中。

然后,broadcastMessage 方法:

    guard let users = users else {
      return
    }

我们需要确保 user 数组没有 nil 值,如果是这种情况,我们就不做任何事情,通过留下方法调用 return 关键字。

然后,我们遍历集合中的所有用户,如果迭代中的当前用户与发送消息的用户(sender)不同,则我们调用当前用户的 receiveMessage 方法,以及要传输的消息。

注意

并发保护

你可能已经看到了以下这一行:

  private let queue = dispatch_queue_create("MediatorPattern", DISPATCH_QUEUE_CONCURRENT)

当多个用户同时尝试访问同一个用户时,我们需要在这个模式中创建并发保护,因此我们使用了苹果开发的一项技术:Grand Central Dispatch,它允许程序中可以并行运行的任务被排队执行,并且根据处理资源的可用性,将它们调度到任何可用的处理器核心上。

通过这一行,我们使用 dispatch_queue_create 初始化 queue 为一个并发队列。第一个参数简单地描述了我们的队列是什么(在调试代码时可能很有帮助),第二个参数指定我们希望我们的队列是并发的。

接下来,我们想要保护我们的代码的访问,这些代码是读取和写入数组的。为此,GCD 提供了一个创建读写锁的优雅解决方案,使用 dispatch barrier。因此,我们使用 dispatch_barrier_sync 来传递我们的队列和队列要执行的语句。由于我们编写的代码是一个屏障闭包,它将永远不会与队列中的任何其他闭包同时运行。有关 Grand Central Dispatch 的更多信息:www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1

我们的所有参与者现在都已就绪。我们现在将在main.swift文件中尝试所有这些。我们创建四个用户,每个用户都有一个名字:

var user1 = User(name: "Julien")
var user2 = User(name: "Helmi")
var user3 = User(name: "Adrien")
var user4 = User(name: "Raphael")

然后,我们实例化第一个中介者并将刚刚创建的前三个用户添加到由mediator1管理的用户集合中:

var mediator1 = Mediator()
mediator1.register(user1)
mediator1.register(user2)
mediator1.register(user3)

现在,我们想要测试user1是否可以向user2user3发送消息。我们只需要调用user1sendMessage方法,传递mediator1和要发送的消息:

user1.sendMessage(mediator1, message: "message1 from \(user1.name)")

因此,在这种情况下,只有user2(Helmi)和user3(Adrien)应该从user1(Julien)那里接收消息。

我们想要尝试使用另一个中介者,但只有两个用户:user2user4

var mediator2 = Mediator()
mediator2.register(user2)
mediator2.register(user4)

user2.sendMessage(mediator2, message: "message 2 from \(user2.name)")

在这里,只有user4(Raphael)应该从user2(Helmi)那里接收消息。

构建并运行项目。你现在应该能够在控制台对话框中看到以下结果:

实现

观察者模式

观察者模式是另一种常用于网络系统的行为模式,其中主题(服务器)将通知一些客户端。iOS 通过NSNotificationCenter对象大量使用此模式。

角色

观察者模式在主题和观察者之间创建依赖关系,以便在主题被修改时通知观察者以更新它们的状态。

这种组合意味着观察者不需要询问主题的当前状态。他们只需要注册到其通知。

此模式可以在以下情况下使用:

  • 对象内部的状态修改需要动态更新其他对象

  • 一个对象想要阻止其他对象,而不需要知道它们的类型(不需要与它们高度耦合)

  • 我们不希望将两个对象合并成一个

设计

以下图表示观察者模式的 UML 类图:

设计

参与者

此模式由以下参与者组成:

  • Subject:这定义了添加、删除和通知观察者所需的方法。

  • ConcreteSubject:这实现了Subject方法。当其状态被修改时,它发送一个通知。

  • Observer:这是一个具有update()方法的通用接口,当观察者需要被通知主题的修改时,主题将调用此方法。

  • ConcreteObserver1ConcreteObserver2:这实现了update()方法。

协作

ConcreteSubject类的内部状态被修改时,它会通知观察者。当一个具体的观察者接收到这个通知时,它会相应地更新。为了完成更新,它可以调用一些主题方法,这些方法可以访问其状态。

插图

你正在开发一个新网站,你希望允许互联网用户通过聊天系统相互通信。你的第一项工作将是提供一个房间,这是所有互联网用户的入口点。每次有新用户加入房间时,每个用户都会收到通知。

观察者模式完全适用于实现代码,从而解决了这个问题。

实现

打开ObserverPattern Xcode 项目,查看我们代码的当前结构:

实现

我们将检索Subject文件夹和Observers文件夹,在那里我们将找到我们模式的参与者。Helpers文件夹包含一个类,我们将在稍后发送消息时使用它。

Extension文件夹包含一个数组扩展,这是为了使我们能够从主题管理的用户集合中删除特定的对象。

最后,我们找到用于模拟交互的main.swift文件。

因此,让我们从在UserProtocol.Swift文件中定义我们的观察者开始我们的代码:

protocol UserProtocol {
  func update(object:AnyObject)
}

我们简单地定义一个带有对象参数的更新方法。UserProtocol的实现将如下所示:

class User: UserProtocol{
  let name: String!

  init(name: String) {
    self.name = name
  }

  func update(object:AnyObject) {
    let info = object as! Info
    print("\(self.name) notified that \(info.message) have status \(info.status) on \(info.date.description)")
  }
}

我们在User对象的构造函数中传递一个名称。

然后,在update方法中,我们准备一个将在控制台上显示的消息。我们将我们的AnyObject类型对象向下转换为Info对象;这个对象是一个辅助对象。你将在Helper文件夹中的Info.swift文件中找到它的代码:

class Info {
  var date = NSDate()
  var message:String!
  var status:InfoStatus!

  init(msg: String, status:InfoStatus) {
    self.message = msg
    self.status = status
  }
}

Info对象包含三个值:日期、消息和状态。

日期是当前日期,在初始化Info对象时定义。消息是在初始化 info 对象时接收到的字符串,状态是在初始化对象时通过参数传递的枚举,它可以有以下值:

enum InfoStatus {
  case Join
  case Leave
}

现在,我们只需要定义我们的主题协议,并在我们的具体主题中实现它。主题代表需要被观察的对象。

我们的主题定义可以在Subject文件夹中的RoomProtocol.swift文件中找到:

protocol RoomProtocol {
  func addObserver(user: User)
  func removeObserver(user: User)
  func notifyObserver(object: AnyObject)
}

这三个方法是观察者模式中任何主题所必需的最小方法。

addObserver函数允许你在主题管理的观察者集合中注册一个观察者。

removeObserver方法用于从主题管理的集合中删除观察者。

最后,notifyObserver方法用于通知所有我们的观察者。

实现将在Room.swift文件中找到,如下所示:

class Room: RoomProtocol {

  private var users = [User]()

  func addObserver(user: User) {
    users.append(user)
    let info = Info(msg: "\(user.name)", status: .Join)
    notifyObserver(info)
  }

  func removeObserver(user: User) {
    users.removeObject(user)
    let info = Info(msg: "\(user.name)", status: .Leave)
    notifyObserver(info)
  }

  func notifyObserver(object: AnyObject){
    for u in users {
      u.update(object)
    }
  }
}

在这里,你检索三个方法,在前两个方法中,你看到对notifyObserver方法的调用。

每次调用addObserverremoveObserver方法时,所有用户都会收到通知,因为当新用户加入房间时,会调用addObserver方法,我们在Info消息中显示Join状态。按照同样的原则,当调用removeObserver方法时,我们显示Leave状态。

notifyObserver方法接收一个类型为AnyObject的参数,该参数将通过Room方法管理的集合中每个用户对象的update方法进行传播。

现在是我们编写演示代码的时候了,打开main.swift文件,并编写代码。

首先,我们初始化我们的Room方法和四个互联网用户:

let room = Room()
let user1 = User(name:"Julien")
let user2 = User(name:"Alain")
let user3 = User(name:"Helmi")
let user4 = User(name:"Raphael")

然后,我们将每个用户注册到房间中:

room.addObserver(user1)
room.addObserver(user2)
room.addObserver(user3)
room.addObserver(user4)

注意

每次调用addObserver方法时,所有当前注册的用户都会收到通知,表明当前注册的用户已加入。

因此,当调用room.addObserver(user1)时,只有user1会被通知,但当注册user2时,user1user2都会被通知,依此类推。

现在,我们按顺序移除user2user3user1

room.removeObserver(user2)
room.removeObserver(user3)
room.removeObserver(user1)

为了完成我们的示例,我们再次注册user2

room.addObserver(user2)

现在,让我们构建并运行项目。你将得到以下结果:

实现

这里,我们看到了所有的通知。第一行对应于第一次调用的addObserver。接下来的两行对应于第二次调用的addObserver,依此类推。

摘要

中介模式与观察者模式之间的比较显示了它们的一些相似之处和不同之处。两种模式都促进了对象之间的通信,并且都解耦了发送者和接收者之间的链接。主要区别在于,在中介模式中,存在参与者概念,并且它们通过中介作为中心枢纽相互通信,而在观察者模式中,发送者和接收者之间有一个清晰的区分,接收者只是监听发送者的变化。

中介模式中的通信更容易理解。元素向中介发送消息,并将信息的进一步传输处理在当前组中的某个地方。

在观察者模式中,观察者等待从多个主题接收信息而被调用。在中介模式中,耦合比在观察者模式中更紧密。

这就结束了本章的内容。在下一章,我们将讨论最后三个行为模式:访问者模式、解释器模式和备忘录模式。

第八章。行为模式 – 访问者、解释器和备忘录

在本章中,我们将完成对 23 个设计模式的探索之旅。现在,让我们看看行为模式类别的最后三个设计模式。它们如下:

  • 访问者模式

  • 解释器模式

  • 备忘录模式

访问者模式

在本节中,我们将讨论访问者模式,该模式允许我们分离数据和它们相关的处理。

角色

访问者模式允许我们将必须在对象上执行的操作外部化和集中化;这些对象之间不能有任何链接。

这些操作将在外部类中实现,而不是在对象的类中实现。

因此,这允许我们在外部类中添加任何操作,甚至是一个实现IVisitor的具体访问者。

当需要使用此模式时:

  • 我们需要在不需要增加这些类负担的情况下向一组类添加功能。

  • 一组类具有固定的结构,我们需要在不修改它们接口的情况下向它们添加一些功能。

当需要在没有共享公共基类或符合公共协议的对象集合上执行操作时,必须应用和使用访问者模式。

设计

以下图表展示了对象和处理是如何分离的。处理在ConcreteVisitor类中实现。对象在ConcreteElement类中实现,如下所示:

设计

参与者

以下列出的是访问者模式参与者:

  • Visitor:这个接口引入了在一系列类中实现功能的方法签名。每个类都有一个方法,该方法接收这个类的实例作为参数。

  • ConcreteVisitors:这实现了与类对应的功能性方法。这些功能分布在不同的元素中。

  • Element:这是具体元素类的抽象类。它引入了accept(visitor)方法。

  • ConcreteElements:这实现了accept()方法,该方法包括通过对应于类的的方法调用访问者。

协作

使用访问者的客户端需要在其选择的类中创建访问者实例,然后将它作为参数传递给一组元素的accept方法。

元素随后调用与其类对应的访问者方法。将对其自身的引用发送回访问者,允许它访问其内部结构。

插图

我们是一家汽车销售商,拥有 DS、雷诺和雪铁龙三个品牌,每个品牌都有一个价格。

我们希望能够在不修改我们的汽车具体类的情况下修改价格。为此,我们将引入我们的访问者模式。

实现

对于最后一章,我们将使用 Playground。现在,打开VisitorPattern.playground文件,让我们看看它是如何工作的。

在这里,我们将使用一种称为双重分派的技术,这将允许我们根据对象的类型执行适当的操作。这项技术还可以帮助我们避免进行一些类型转换以执行适当的操作。(有关此技术的更多信息,请参阅以下网址:[en.wikipedia.org/wiki/Double_dispatch 如果您需要更多关于此技术的信息](https://en.wikipedia.org/wiki/Double_dispatch 如果您需要更多关于此技术的信息))

首先,我们定义我们的访问者协议。访问者有三个具有ConcreteElement作为参数的visit方法,以接受每种汽车类型,如下所示:

 protocol CarVisitor {
  func visit(car: DSCar)
  func visit(car: RenaultCar)
  func visit(car: CitroenCar)
}

然后,我们定义我们的Car协议。一辆车可以接受一个具体的CarVisitor对象:

protocol Car {
  func accept(visitor: CarVisitor)
}

我们可以轻松实现我们的三个具体车型。每个车型都有一个默认价格,还有一个接受具有具体Visitor对象的accept方法:

class DSCar: Car {
  var price = 29000.0
  func accept(visitor: CarVisitor) { visitor.visit(self) }
}
class RenaultCar: Car {
  var price = 17000.0
  func accept(visitor: CarVisitor) { visitor.visit(self) }
}
class CitroenCar: Car {
  var price = 19000.0
  func accept(visitor: CarVisitor) { visitor.visit(self) }
}

注意

Car协议定义并由类实现的accept方法是双重分派技术的关键。通过将self作为参数传递给visitor.visit方法,其中visitor是我们对CarVisitor的具体实现,Swift 将选择具有最具体类型的visit方法版本。

最后,我们必须实现我们的具体访问者,我们的访问者负责修改Element类的价格。Element类的修改取决于传递给参数的对象类型。

DS 汽车的价格将增加 20%,雷诺和雪铁龙汽车的价格将增加 10%:

class PriceVisitor: CarVisitor {
  var price = 0.0
  func visit(car: DSCar)  { price = car.price * 0.8  }
  func visit(car: RenaultCar) { price = car.price * 0.9 }
  func visit(car: CitroenCar)  { price = car.price * 0.9 }
}

客户端将通过以下代码进行模拟。我们首先实例化我们的三个汽车对象并将它们添加到Car数组中。然后,我们将定义一个新的变量price,它是一个包含我们三个新价格的数组。

为了做到这一点,我们将使用map函数,它是数组类型的一个扩展。它允许我们对数组的每个元素执行处理。在这里,我们可以(对于每个元素)实例化一个PriceVisitor对象,并将其传递给当前car对象的accept方法。

然后,我们返回新的visitor.price,这是当前汽车对象的新价格。

正如我在此模式的角色部分所说,访问者模式用于当数组管理一个不共享公共基类或符合公共协议的异构对象集合时。通过应用此模式,所有三个Cars类都可以共享并符合相同的协议,使我们能够管理以下数组:

let cars: [Car] = [DSCar(), RenaultCar(), CitroenCar()]

然后,我们可以通过应用适当的访问者计算来计算新的价格:

let prices = cars.map { (car: Car) -> Double in
  let visitor = PriceVisitor()
  car.accept(visitor)
  return visitor.price
}

要显示结果,请检查以下截图的右侧部分。232001530017100是我们汽车的新价格:

实现

解释器模式

解释器模式实际上并没有被使用,但它确实非常有用。通常,这种模式是用形式语法来描述的,但可以应用此模式的领域可以扩展。

您可以参考以下内容获取更多信息:罗马数字解读(www.oodesign.com/interpreter-pattern.html)和en.wikipedia.org/wiki/Roman_numerals

角色

解释器模式定义了一种语言语法的对象表示,以便通过解释来评估用此语言编写的某些表达式。

此模式可以用来解释一些以分层树表示的表达式。它可能适用于以下情况:

  • 表达式语法很简单

  • 评估不需要很快

可以使用此模式的一些示例包括:

  • 在规则引擎中

  • 要向组合模式添加一些功能

设计

此模式的实现体现在将组合模式应用于表示语法(参考第三章中的组合模式结构型模式 – 组合和享元)。区别在于解释器模式定义了行为,而组合模式只定义了结构。

该模式的 UML 类图如下:

设计

参与者

此模式的参与者如下:

  • AbstractExpression: 这定义了抽象语法树中所有节点共有的interpret()方法。

  • TerminalExpression: 这实现了与语法终止符号相关的解释方法。语法的每个终止符号都需要一个具体的类。

  • NonTerminalExpression: 这实现了解释方法,也可以包含其他AbstractExpression实例。

  • Context: 这包含对解释器全局的信息。例如,变量的实际值。

  • Client: 这将构建一个由NonTerminalExpressionTerminalExpression实例组成的抽象语法树。这是我们演示该模式的使用。

协作

客户端构建抽象语法树,初始化解释器的上下文,然后调用解释方法。

TerminalExpressionNonTerminalExpression节点上的解释方法使用上下文来存储和访问解释器的状态。

插图

我们想要创建一个罗马数字转换器;您知道那些将 XIV 解读为十进制中的 14 的转换器。我们示例的主要目的是编写一个罗马数字,我们的转换器将告诉我们它的十进制值。

实现

打开InterpreterPattern.xcodeproj文件,看看我们是如何实现该模式的。

注意

对于此模式,所有代码都已添加到main.swift类中,以便您可以轻松将其导出到 Playground 中,如果您想看到代码的实时执行。

在开始理解代码之前,请查看以下链接了解罗马数字是如何工作以及它们是如何书写的。

你可以在4thgradecrocs.weebly.com/roman-numerals.html看到一些关于罗马数字的规则,以及罗马数字表,同时也可以了解罗马数字系统是如何工作的,详情请见en.wikipedia.org/wiki/Roman_numerals

对于模式,我们将使用一个字符串扩展,它允许我们通过传递要忽略的字符数来轻松地执行子字符串操作:

extension String {
  func subStringFrom(pos: Int) -> String {
    var substr = ""
    let start = self.startIndex.advancedBy(pos)
    let end = self.endIndex
    let range = start..<end
    substr = self[range]
    return substr
  }
}

要解释的表达式是一个字符串,它被放入上下文中:

class Context {
  var input: String!
  var output: Int = 0

  init(input: String){
    self.input = input
  }
}

这个类将帮助我们应用模式时进行工作;它包括剩余未解析的罗马数字字符串以及已解析的数字的结果。

根据解释的类型(千位、百位、十位和个位),上下文被传递给四个子解释器之一。在这个例子中,只使用了TerminalExpressions

接下来,我们将定义我们的AbstractExpression类。这个类必须实现interpret()方法并定义将在子类中重写的方法:

class Expression {
  func interpret(context: Context) {
    if context.input.characters.count == 0 {
      return
    }

    if context.input.hasPrefix(nine()){
      context.output = context.output + (9 * multiplier())
      context.input = context.input.subStringFrom(2)
    } else  if context.input.hasPrefix(four()){
      context.output = context.output + (4 * multiplier())
      context.input = context.input.subStringFrom(2)
    } else  if context.input.hasPrefix(five()){
      context.output = context.output + (5 * multiplier())
      context.input = context.input.subStringFrom(1)
    }

    while context.input.hasPrefix(one()) {
      context.output = context.output + (1 * multiplier())
      context.input = context.input.subStringFrom(1)
    }
  }

  func one() -> String {
    fatalError("this method must be implemented in a subclass")
  }

  func four() -> String {
      fatalError("this method must be implemented in a subclass")
  }

  func five() -> String {
      fatalError("this method must be implemented in a subclass")
  }
  func nine() -> String {
      fatalError("this method must be implemented in a subclass")
  }
  func multiplier() -> Int {
      fatalError("this method must be implemented in a subclass")
  }
}

Expression类由接收上下文的interpret方法组成。根据当前对象,它使用特定的千位、百位、十位、个位和特定的乘数值。

Expression类的one()four()five()nine()multiplier()方法是抽象的。它们将在我们的具体TerminalExpressions类中实现。

我们现在可以实现我们的四个TerminalExpression类。每个类都重写了one()four()five()nine()multiplier()方法。这些方法将根据我们是否处于千位、百位、十位或个位表达式来解释。实际上,这些类用于定义每个特定的表达式。通常,这些类实现interpret方法,但在这里它已经在基表达式类中定义,每个TerminalExpression类通过实现抽象方法:one()four()five()nine()multiplier()来定义其行为。这是一个模板方法(请参阅第五章中的模板方法部分 Chapter 5,行为模式 – 策略、状态和模板方法):

class ThousandExpression: Expression {
  override func one() -> String {
    return "M"
  }
  override func four() -> String {
    return " "
  }
  override func five() -> String {
    return " "
  }
  override func nine() -> String {
    return " "
  }
  override func multiplier() -> Int {
    return 1000
  }
}

class HundredExpression: Expression {
  override func one() -> String {
    return "C"
  }
  override func four() -> String {
    return "CD"
  }
  override func five() -> String {
    return "D"
  }
  override func nine() -> String {
    return "CM"
  }
  override func multiplier() -> Int {
    return 100
  }
}

class TenExpression: Expression {
  override func one() -> String {
    return "X"
  }
  override func four() -> String {
    return "XL"
  }
  override func five() -> String {
    return "L"
  }
  override func nine() -> String {
    return "XC"
  }
  override func multiplier() -> Int {
    return 10
  }
}

class OneExpression: Expression {
  override func one() -> String {
    return "I"
  }
  override func four() -> String {
    return "IV"
  }
  override func five() -> String {
    return "V"
  }
  override func nine() -> String {
    return "IX"
  }
  override func multiplier() -> Int {
    return 1
  }
}

模式现在已经编写好了;我们只需要测试它。在编写测试代码之前,我们将创建一个辅助的RomanToDecimalConverter()类,它有一个calculate()方法,该方法返回转换的结果:

class RomanToDecimalConverter {
  var tree = [ThousandExpression(), HundredExpression(), TenExpression(),OneExpression()]

  func calculate(romanString: String) -> Int {
    let context = Context(input: romanString)
    for t in tree {
      t.interpret(context)
    }
    return context.output
  }
}

这个类负责构建表示我们特定值的语法树;罗马数字,在由语法定义的语言中。

注意,我们只能以特定的顺序将我们的终端表达式添加到数组中:从千位到个位表达式,因为我们将从左到右解析罗马数字字符串。

在构建语法树之后,我们调用interpret方法。一旦执行了模式的全部表达式,我们返回context.output值,这对应于十进制结果。

在编写测试代码之前,我们将向RomanToDecimalConverter类添加一个新方法。这将验证我们试图转换的罗马数字是否正确:如果不正确,将显示一条消息,告知我们的数字不是罗马数字。以下代码中添加的代码被突出显示:

enum FormatError: ErrorType {
 case RomanNumberFormatError
}

//Helper
class RomanToDecimalConverter {
 static let pattern = "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"
 let validation = NSPredicate(format: "SELF MATCHES %@", pattern)

  var tree = [ThousandExpression(), HundredExpression(), TenExpression(),OneExpression()]

 func calculate(romanString: String) throws -> Int {
 guard validate(romanString) else {
 throw FormatError.RomanNumberFormatError
 }

    let context = Context(input: romanString)
    for t in tree {
      t.interpret(context)
    }
    return context.output
  }

 func validate(romanString: String) -> Bool {
 return validation.evaluateWithObject(romanString)
 }
}

我们首先定义一个新的enum函数,我们称之为FormatError,其类型为ErrorType。我们将使用它来抛出异常,当罗马数字的格式不正确时。

然后,在RomanToDecimalConverter类中有一个静态字符串pattern,其中包含我们的正则表达式,还有一个实例常量validation,它将通过NSPredicate对象处理验证过程。该对象的目的在于定义约束搜索所需的逻辑条件,无论是用于检索还是内存过滤。

calculate方法中,我们有一个guard语句,它执行validate函数,如果validate函数返回false(表示数字不正确),则抛出FormatError.RomanNumberFormatError异常;否则,我们将继续操作并使用模式计算十进制值。注意方法返回类型之前添加的throws关键字。这意味着此方法可以抛出错误。

validate()方法通过传递罗马数字作为参数调用NSPredicate实例的evaluateWithObject方法。如果格式正确,该方法返回true,否则返回false

注意

代码中使用的正则表达式来自以下 URL,并且在该网站上解释得非常详细:stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression

关于NSPredicate的更多信息,您可以参考此网站:realm.io/news/nspredicate-cheatsheet/

现在,我们可以像以下这样编写测试代码:

let romanNumberToTest = ["XIV", "MCCMXXVIII","MCMXXVIII"]
var converter = RomanToDecimalConverter()
for roman in romanNumberToTest {
  var decimal = try? converter.calculate(roman)
  guard (decimal != nil) else {
    print("\(roman) is not a correct roman number")
    continue
  }
  print(decimal!)
}

我们初始化一个包含一些罗马数字的数组,然后初始化我们的RomanToDecimalConverter转换器对象,并遍历所有元素进行计算。

我们正在使用以下声明:

var decimal = try? converter.calculate(roman)

如果抛出异常,十进制将返回nil。因此,接下来的三行代码帮助我们打印出适当的消息,当罗马数字格式不正确时。

在构建和运行项目后,使用数组的三个值,您将获得以下结果:

实现

程序给出的先前结果告诉我们,MCCMXXVIII有一个错误的罗马数字格式,而其他两个数字都很好地转换成了第一个罗马数字的 14 十进制值和第二个罗马数字的 1928。

你可以尝试使用本节开头提供的罗马数字表运行代码。

你可以看到这个模式真的很有趣。你已经看到组合模式和模板方法模式也被用来构建和实现我们的解释器模式,使用我们的用例。

备忘录模式

备忘录模式将是我们将要一起发现的最后一个模式。你会发现这个模式与工作非常有趣,并且有很多用途。

角色

备忘录模式的作用是捕获对象的内部状态并将其外部保存,以便以后可以恢复,而不会破坏该对象的封装。

设计

通用 UML 类图定义为以下:

设计

参与者

以下备忘录模式的参与者:

  • Memento:这是保存原始对象内部状态(或该状态的一部分)的对象的类,例如,保存状态的实现可以独立于对象本身。备忘录有两个接口:

    • 一个完整的Originator对象接口,允许访问需要保存或恢复的所有内容

    • 一个窄接口的看护者,可以保持和传递备忘录引用,但不再有更多

  • Originator:这是创建备忘录以保存其内部状态并可以从备忘录中恢复的对象类。

  • caretaker:这负责管理备忘录列表,不提供对原始对象内部状态的访问。这也是客户端代码需要访问的类。

  • CreateMemento:这个方法用于保存原始对象的状态。它通过将状态变量保存到Memento对象中并返回它来创建一个Memento对象。这用于记录Originator的状态。

  • SetMemento:这个类存储了Originator对象的历史信息。信息存储在其状态变量中。

协作

Caretaker实例通过调用createMemento方法请求originator对象的备忘录并将其保存。在取消操作并返回备忘录中保存的状态时,你可以使用setMemento方法将其发送给originator对象。

插图

你开发了一个平台游戏,每次你的英雄通过一个关卡时,都会存储一个检查点。这允许玩家从他已经通过的任何关卡重新加载。

保存检查点时需要存储的数据是关卡编号、当前武器和得分数量。

实现

为了简化我们的代码,我们将使用一个 OS X 命令行项目,其中我们将在main.swift文件中编写所有代码。

在 Xcode 中打开MementoPattern.xcodeproj文件,让我们看看我们是如何组织我们的代码的。

首先,我们定义我们的GameState结构:需要保存的数据是levelweaponpoints

struct GameState {
  var level: Int
  var weapon: String
  var points: Int
}

然后我们定义我们的Originator,带有createMementosetMemento方法:

protocol Originator {
  func createMemento() -> GameMemento  func setMemento(memento: GameMemento)
}

然后我们实现我们的GameState Memento:它包含CheckPoint恢复状态所需的所有信息。

首先,我们定义三个变量,这将帮助我们存储状态:

  • entries是一个GameState列表。

  • nextId包含entries数组的下一个索引

  • totalPoints是一个变量,它包含每个条目的总分数。

我们有一个构造函数,我们可以传递一个CheckPoint对象作为参数。检查点的状态被分配给备忘录内部变量。

apply()方法接收一个检查点对象作为参数。然后,我们将当前备忘录的值分配给检查点的属性,以便恢复检查点状态:

struct GameMemento {
  private let entries: [Int: GameState]
  private let nextId: Int
  private let totalPoints: Int

  init(checkPoint: CheckPoint){
    self.entries = checkPoint.entries
    self.nextId = checkPoint.nextId
    self.totalPoints = checkPoint.totalPoints
  }

  func apply(checkPoint: CheckPoint) {
    print("Restoring a game state to a checkpoint...")
    checkPoint.nextId = nextId
    checkPoint.totalPoints = totalPoints
    checkPoint.entries = entries
  }
}

现在我们可以保存和恢复一个备忘录。接下来要做的事情是创建我们的发起者:CheckPoint对象。在玩游戏的过程中,一个条目将被添加到检查点条目列表中。

就像在Memento对象中一样,我们将定义三个变量。不同之处在于我们在这个对象中初始化它们:

  • Entries:这是一个将包含所有条目的数组。

  • totalPoints:这是一个初始化为0的整数,它将包含每个级别的总分数。

  • nextId:这是一个从0开始的整数。它包含entries数组中的下一个索引值。

我们有四个方法:

  • addGameStateEntry:此方法用于向我们的条目列表中添加一个新的条目。这将在客户端每次完成游戏的一个级别时被调用。

  • createMemento:此方法创建并返回一个备忘录对象。检查点本身作为方法的参数传递。

  • setMemento:此方法允许我们恢复一个Memento对象。

  • printCheckPoint:此方法用于轻松查看CheckPoint对象的当前状态。

CheckPoint类有以下实现:

class CheckPoint: Originator {
  private var entries: [Int: GameState] = [:]
  private var totalPoints: Int  = 0
  private var nextId: Int = 0

  func addGameStateEntry(level: Int, weapon: String, points: Int) {
    let entry = GameState(level: level, weapon: weapon, points: points)
    entries[nextId++] = entry
    totalPoints += points
  }

  func createMemento() -> GameMemento {
    return GameMemento(checkPoint: self)
  }

  func setMemento(memento: GameMemento) {
       memento.apply(self)
  }

  func printCheckPoint() {
    print("Printing checkPoint....")
    entries.sort {$0.0 < $1.0 }
.map {
        print("Level: \($0.1.level)   Weapon: \($0.1.weapon)   Points: \($0.1.points) ")
    }
    print("Total Points: \(totalPoints)\n")
  }
}

你可能已经看到了以下语句:

    entries.sort {$0.0 < $1.0 }
.map {
        print("Level: \($0.1.level)   Weapon: \($0.1.weapon)   Points: \($0.1.points) ")
    }

这就是我们如何轻松按索引值排序我们的数组。然后我们可以使用map函数,只是为了能够在数组的每个条目上执行打印语句。

你可以在以下 URL 中找到更多关于闭包的信息:developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html

我们的模式现在已经就绪,让我们看看如何使用它:

let checkPoint = CheckPoint()
checkPoint.addGameStateEntry(0, weapon: "gun", points: 1200)
checkPoint.addGameStateEntry(1, weapon: "tommy gun", points: 2250)
checkPoint.printCheckPoint()

let memento = checkPoint.createMemento()
checkPoint.addGameStateEntry(2, weapon: "bazooka", points: 2400)
checkPoint.addGameStateEntry(4, weapon: "knife", points: 3000)
checkPoint.printCheckPoint()

checkPoint.setMemento(memento)
checkPoint.printCheckPoint()

初始化检查点对象后,我们在检查点中添加两个条目。

在调用checkpoint对象的printCheckPoint方法后:我们得到以下结果:

Printing checkPoint....
Level: 0   Weapon: gun   Points: 1200 
Level: 1   Weapon: tommy gun   Points: 2250 
Total Points: 3450

printCheckPoint 方法遍历检查点对象的所有条目并显示每个条目的状态和总分。

在这个时候,我们还没有创建Memento对象,因此无法恢复到之前的状态。

要创建一个备忘录,我们只需调用Originator对象(在我们的例子中是checkPoint对象)的createMemento方法:

let memento = checkPoint.createMemento()

此方法返回一个备忘录对象,我们将其分配给我们的备忘录常量,以便在需要时恢复。

我们继续游戏并成功通过两个其他关卡,然后再次打印当前的检查点状态:

checkPoint.addGameStateEntry(2, weapon: "bazooka", points: 2400)
checkPoint.addGameStateEntry(4, weapon: "knife", points: 3000)
checkPoint.printCheckPoint()

结果如下:

Printing checkPoint....
Level: 0   Weapon: gun   Points: 1200 
Level: 1   Weapon: tommy gun   Points: 2250 
Level: 2   Weapon: bazooka   Points: 2400 
Level: 4   Weapon: knife   Points: 3000 
Total Points: 8850

CheckPoint现在在其数组中包含四个条目。

我们希望将检查点状态恢复到最后一次保存的状态。为了继续,我们只需调用检查点对象的setMemento()方法,并使用我们之前创建的备忘录对象,然后调用printCheckPoint方法以显示结果,如下所示:

checkPoint.setMemento(memento)
checkPoint.printCheckPoint()

printCheckPoint 调用的结果是以下内容:

Restoring a game state to a checkpoint...
Printing checkPoint....
Level: 0   Weapon: gun   Points: 1200 
Level: 1   Weapon: tommy gun   Points: 2250 
Total Points: 3450

我们可以看到检查点对象已经恢复到其之前的状态。

通过这个简单的例子,你现在应该有一个很好的模式来使用和轻松管理对象的取消和恢复。

备忘录模式是一种在科学计算中被广泛使用的模式,用于保存长时间运行的计算状态。正如这里所看到的,它可以用来保存数小时或数天的游戏状态。在图形工具包中,它可以用来在对象移动时保存显示状态。

注意

在以下情况下使用此模式:

  • 对象的状态必须保存以便稍后恢复

  • 直接暴露状态是不理想的

比较三种模式

虽然它们看起来不同,但我们有几种方法可以比较这三个模式:

  • 可重用性:所有三种模式都旨在减轻开发者重复实现通用代码的负担。实际上,一旦实现了访问者,它们可以与不同类型的对象一起重用,而无需任何更改。为了促进重用,你必须限制访问者需要了解的状态数量。解释器模式旨在解析一个输入,其中结构是数据驱动的。《Memento》模式的Caretaker和备忘录类与数据无关,只有Originator应该改变。

  • 与结构一起工作:这些模式旨在与结构一起使用。访问者和解释器模式要求开发者编写遍历代码,而备忘录模式将整个结构移动,依赖于序列化来进行遍历。

  • 作为参数的对象:在备忘录和访问者模式中,对象作为其结构的一部分被传递,而在解释器模式中,它依赖于一个上下文,基本上是正在被解释以输出到输出的输入的演变状态。

摘要

本章总结了由四人帮描述的八个行为模式,并且本章也标志着本书的结束。

我希望这本书能够非常易于跟随,而不会在提供的示例中增加复杂性。本书旨在成为任何想要使用 Swift 实现设计模式的人的参考。

有些我们没有涉及到的点,比如多并发访问、使用闭包的 Swift 2 特殊编码等等。我认为这些点在教材中并不常见。本书的主要目的是在需要时能够轻松找到合适的模式,并且有一个易于理解的示例来跟随。

好的,你现在已经拥有了所有必要的知识来以可扩展的方式组织你的代码。这将帮助你正确地组织代码,提高代码的性能,并降低维护成本。

我真的非常感谢你一直跟随这本书。现在,将所学知识付诸实践就取决于你了。

posted @ 2025-10-24 10:06  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报