第9章:建造者模式

构建器模式允许你通过逐步提供输入来创建复杂的对象,而不是通过初始化器预先要求所有输入。这种模式涉及三种主要类型。

  1. 指导者接受输入并与构建者协调。这通常是一个视图控制器或一个被视图控制器使用的辅助类。

  2. 产品是要创建的复杂对象。它可以是一个结构体,也可以是一个类,取决于所需的引用语义。它通常是一个模型,但也可以是任何类型,取决于你的用例。

  3. 构建器接受一步步的输入并处理产品的创建。

这通常是一个类,所以它可以通过引用来重复使用。

你什么时候应该使用它?

当你想用一系列的步骤来创建一个复杂的对象时,请使用构建器模式。

当一个产品需要多个输入时,这种模式的效果特别好。构建器抽象出这些输入是如何被用来创建产品的,并且它以主管想提供的任何顺序接受这些输入。

例如,你可以用这种模式来实现一个 "汉堡包构建器"。产品可以是一个汉堡包模型,它的输入包括肉的选择、配料和酱汁。主管可以是一个雇员对象,它知道如何制作汉堡包,也可以是一个视图控制器,接受用户的输入。

因此,"汉堡包制作者 "可以接受任何顺序的肉类选择、配料和酱汁,并根据要求创建一个汉堡包。

游乐场实例

打开Starter目录下的FundamentalDesignPattern.xcworkspace,或者从上一章中你自己的playground工作区继续,然后打开Overview页面。

你会看到Builder被列在创意模式下。这是因为这个模式是关于创建复杂产品的。点击Builder链接,打开该页面。

你将实现上面的 "汉堡包生成器 "的例子。你首先需要定义产品。在代码示例后直接输入以下内容。

依次看每个评论部分。

  1. 你首先定义了汉堡包,它有肉、酱和配料的属性。

一旦汉堡包被制作出来,你就不能改变它的组成部分,你通过let属性对其进行编码。你还使汉堡包符合CustomStringConvertible,这样你就可以在以后打印它。

  1. 你把Meat声明为一个枚举。每个汉堡包必须有一个肉类选择:对不起,不允许有牛肉-鸡肉-豆腐的汉堡。你还指定了一种外来的肉,小猫。谁不喜欢 "小猫 "汉堡呢?

  2. 您将酱汁作为一个选项集。这将允许你把多种酱料组合在一起。我个人最喜欢的是番茄酱-蛋黄酱-秘制酱。

  3. 同样,你也要把配料作为一个选项集。要做一个好的汉堡,你需要的不仅仅是泡菜。

接下来,添加以下代码来定义构建器。

这里有几个重要的微妙之处。

  1. 你为肉、酱汁和配料声明了属性,这与汉堡包的输入完全一致。与汉堡包不同的是,你用var声明这些属性,以便能够改变它们。你还为每个属性指定了private(set),以确保只有HamburgerBuilder可以直接设置它们。

  2. 由于你用private(set)声明了每个属性,你需要提供公共方法来改变它们。你通过addSauces(:)、removeSauces(:)、addToppings(:)、removeToppings(:)和setMeat(_:)来实现。

  3. 最后,你定义了build()来从选择中创建汉堡包。

private(set)强制消费者使用公共setter方法。这允许构建器在设置属性之前进行验证。

例如,在设置一个肉之前,你要确保它是可用的。

在其他属性之后添加以下属性。

private var soldOutMeats: [Meat] = [.kitten]

如果一种肉被卖光了,每当调用setMeat(_:)时你就会抛出一个错误。你需要为此声明一个自定义的错误类型。在HamburgerBuilder的开头大括号后添加以下代码。

public enum Error: Swift.Error { 
  case soldOut 
}

最后,将setMeat(_:)替换为以下内容。

如果你现在试图为这块肉设置小猫,你会收到一个错误,说它已经卖完了。毕竟,它真的很受欢迎!

接下来,你需要声明主管。在Playground的末尾添加以下内容:

员工知道如何创建两个汉堡:createCombo1 和 createKittenSpecial。最好保持简单,对吗?你终于准备好看到这个代码了!在Playground的末尾添加以下内容:

在这里,你创建一个名为 burgerFlipper 的 Employee 实例并请求创建 combo1。你应该看到这个打印到控制台:

Nom nom beef burger

接下来,在Playground的末尾添加以下内容:

在这里,你要求一个小猫专用的汉堡包。由于小猫已经卖完了,你会看到这个打印在控制台。

Sorry, no kitten burgers here... :[

噢,伙计,你得去别的地方满足你对小猫汉堡的渴望了!

你应该注意什么?

构建器模式最适用于创建复杂的产品,这些产品需要使用一系列的步骤进行多个输入。如果你的产品没有多个输入,或者不能一步一步地创建,那么构建器模式可能会带来更多的麻烦,而不值得。

相反,考虑提供方便的初始化器来创建产品。

教程项目

你将继续上一章的RabbleWabble应用程序。特别是,你将添加使用构建器模式创建一个新的QuestionGroup的功能。

如果你跳过了前一章,或者你想重新开始,请打开Finder并导航到你下载本章资源的地方。然后,在Xcode中打开starter/RabbleWabble/RabbleWabble.xcodeproj。然后你应该跳到实现构建器模式,因为启动器项目中已经有你需要的所有文件。

如果你选择继续构建上一章的项目,你将需要添加一些文件。这些文件的内容对于理解构建器模式并不重要。相反,它们提供了一个简单的起点,所以你不需要做繁琐的视图设置。

打开Finder,导航到你为本章下载项目的地方。除了Starter和Final目录,你会看到一个Resources目录,其中包含Controller和View的子目录。

将Finder窗口置于Xcode上方,像这样将Controllers\CreateQuestionGroupViewController.swift拖入应用程序的Controllers组。

当提示时,如果需要的话,勾选复制项目的选项,然后按完成添加文件。

同样地,将resources/Views中的所有文件拖放到应用程序的视图中。然后,右键点击 "视图",选择 "按名称排序"。之后,你的文件层次结构应该是这样的。

CreateQuestionGroupViewController提供了创建一个新的QuestionGroup的能力。然而,目前在应用程序中不可能做到这一点。

为了解决这个问题,请打开Main.storyboard,并平移到选择问题组的场景。然后,按下对象库按钮,选择 "显示对象库 "选项卡,在搜索栏中输入bar按钮。然后,拖放一个新的酒吧按钮作为 "选择问题组 "场景的右侧酒吧按钮。

选择新添加的酒吧按钮项目,进入属性检查器,将系统项目设置为添加。

接下来,按下对象库按钮,在搜索栏中输入storyboard,并在问题视图控制器场景上方拖放一个新的storyboard参考。

选择这个故事板引用,进入属性检查器,将故事板设置为NewQuestionGroup。

最后,从 "+"条按钮控制拖动到NewQuestionGroup故事板参考。在出现的新窗口中,选择Present Modally。这就为NewQuestionGroup故事板的初始视图控制器创建了一条通道。

打开NewQuestionGroup.storyboard,你会看到它的初始视图控制器被设置为UINavigationController,它的根视图控制器是CreateQuestionGroupViewController。

构建并运行,然后按 "+"键,就可以看到它的运行情况了!

然而,如果你按下 "取消 "键,什么也没有发生!这是怎么回事?这到底是怎么回事?CreateQuestionGroupViewController在取消按钮被按下时调用一个委托方法。但是,你还没有把这个委托方法连接起来。

为了解决这个问题,打开SelectQuestionGroupViewController.swift,在文件末尾添加以下扩展。

这使得SelectQuestionGroupViewController符合CreateQuestionGroupViewControllerDelegate。

这个协议需要两个方法:createQuestionGroupViewControllerDidCancel(:)在取消按钮被按下时被调用,createQuestionGroupViewController(:created:)在一个新的QuestionGroup被创建时被调用。

要处理取消,你只需取消视图控制器。为了处理创建,你将新的QuestionGroup追加到questionGroupCaretaker.questionGroups中,要求它保存(),解散视图控制器并刷新表视图。

当触发到CreateQuestionGroupViewController的segue时,你还需要实际设置委托属性。将prepare(for segue:sender:)替换为以下内容。

下面是这个的作用。

  1. 还有一个可能的转场,它显示了QuestionViewController。以前,这是这个方法内唯一的代码。你检查是否是这样,如果是,就正确设置QuestionViewController的属性。

  2. 然后,你要检查这个segue是否过渡到UINavigationController中的CreateQuestionGroupViewController。如果是,你就在新的CreateQuestionGroupViewController实例上设置委托。

  3. 如果两个if语句都不匹配,你就直接忽略这个转场。

建立并运行,点选+,然后点选取消。视图控制器现在将被正确地驳回。

但是,如果你按下 "保存 "键,什么也不会发生!这是因为你还没有添加任何东西。这是因为你还没有添加代码来实际创建一个提问组。你需要使用构建器模式来做到这一点。

实现构建器模式

CreateQuestionGroupViewController是本章中新增的一个文件。它使用一个

表视图来接受创建问题组的输入。它显示CreateQuestionGroupTitleCell和CreateQuestionCell来收集用户的输入。

因此,CreateQuestionGroupViewController是导演,而

QuestionGroup是产品。你的工作是首先创建一个构建器,然后修改CreateQuestionGroupViewController以使用它。

首先,右击黄色的RabbleWabble组,选择新建组。输入Builders作为它的名字,并把它移到AppDelegate组的下面。这让其他开发者清楚地看到你在使用构建器模式。

右键点击你新添加的Builders组,选择新文件。然后选择iOS ▸ Swift文件,点击下一步。然后输入QuestionGroupBuilder.swift作为文件名,按Create键添加新文件。

QuestionGroupBuilder将负责创建新的QuestionGroups。然而,QuestionGroup也包含复杂的子对象,即Question.Builder。

你可以用什么来创建这些复杂的子对象?当然是另一个构建器! 你将首先创建这个构建器。将QuestionGroupBuilder.swift的内容替换为以下内容。

QuestionBuilder拥有创建一个问题所需的所有输入的属性:答案、提示和提示。最初,每个属性都被设置为一个空字符串。每当你调用build()时,它就会验证答案和提示是否已经被设置。如果没有设置,它会抛出一个自定义错误;提示在应用程序中是可选的,所以如果是空的也没关系。否则,它将返回一个新的问题。

现在你可以创建QuestionGroupBuilder,它将在内部使用QuestionBuilder。在QuestionBuilder之前添加以下代码。

以下是发生的情况。

  1. 你首先声明与所需输入匹配的属性,以创建一个问题组。你创建一个QuestionBuilder数组,它将构建单个问题对象。你最初创建一个QuestionBuilder,这样就有了一个开始。毕竟,一个问题组必须至少有一个问题。

  2. 顾名思义,你将使用addNewQuestion()来创建并将一个新的QuestionBuilder附加到问题上。同样,removeQuestion(at:)将从问题中按索引删除一个QuestionBuilder。

  3. 每当你调用build()时,QuestionBuilder会验证标题是否已被设置,并且问题中至少有一个QuestionBuilder。如果没有,它会抛出一个错误。如果这两个条件都通过了,它就试图通过对每个QuestionBuilder调用build()来创建问题。这也可能失败,并导致一个无效的问题所抛出的错误。如果一切顺利,它会返回一个新的QuestionGroup。

现在你已经准备好使用QuestionBuilder了。打开CreateQuestionGroupViewController.swift,你会看到有几个//TODO注释。每一条都需要你使用QuestionBuilder来完成。

首先,在delegate后面直接添加这个属性。

public let questionGroupBuilder = QuestionGroupBuilder()

因为你把QuestionGroupBuilder的所有属性都设置为默认值,所以你不需要传递任何东西来创建一个QuestionGroupBuilder。很好,很容易!

接下来,用下面的语句替换

tableView(_:numberOfRowsInSection:)中的返回语句改为这样。

return questionGroupBuilder.questions.count + 2

CreateQuestionGroupViewController显示三种类型的表视图单元格:一种用于显示QuestionGroup的标题,一种用于显示每个QuestionBuilder,一种用于添加

额外的QuestionBuilder对象。因此,这导致了questionGroupBuilder.questions.count + 2的单元格总数。

在tableView(_:cellForRowAt:)中,替换这一行。

} else if row == 1 {

用这个代替。

} else if row >= 1 && 
          row <= questionGroupBuilder.questions.count {

之前的代码假设只有一个QuestionBuilder单元。在这里,你要更新一下,以考虑到可能有几个。

将titleCell(from:for:)中的//TODO: 替换为以下内容。

cell.titleTextField.text = questionGroupBuilder.title

在这里,你只需显示来自questionGroupBuilder.title的文本。

接下来,在questionCell(from:for:)后面添加以下内容。

这是一个辅助方法,用于为给定的索引路径获取QuestionBuilder。此后你会多次需要这个方法,所以只在一个地方定义这个方法是很有好处的。

用下面的内容代替//TODO:在questionCell(from:for:)中的内容。

这样就可以在给定的索引路径上使用QuestionBuilder的值来配置给定的CreateQuestionCell。

替换 // TODO: - 添加UITableViewDelegate方法,内容如下。

每当表视图单元格被点击,tableView(_:didSelectRowAt:)就会检查indexPath是否与isLastIndexPath匹配。如果符合,那么用户就点击了表视图底部的 "添加 "单元。在这种情况下,你请求questionGroupBuilder.addNewQuestion()并插入一个新的单元格来显示新的QuestionBuilder。

建立并运行。然后点 "+"导航到CreateQuestionGroupViewController来试试这些变化。

你现在可以在表格视图中添加额外的QuestionBuilder实例和单元格。棒极了!

如果你为几个问题输入了文本,并在此后创建了许多新的单元格,你会发现在滚动表格视图后,你的文本就不见了。这是因为你没有真正地将输入到单元格中的文本持久化到每个QuestionBuilder上。

幸运的是,CreateQuestionGroupViewController已经符合CreateQuestionCellDelegate的要求,每当CreateQuestionCell改变时,它就会被调用。

答案、提示和提示文本发生变化时,CreateQuestionCell就会调用它。所以你只需要完成这些方法就可以了

在createQuestionCell(_:promptTextDidChange:)后面紧接着添加以下内容。

你将使用这个助手来确定给定单元格的QuestionBuilder,你通过找到单元格的索引路径,然后使用你之前为questionBuilder(for indexPath:)编写的助手方法来完成。

将createQuestionCell(_:answerTextDidChange:)中的//TODO: 替换为以下内容。

questionBuilder(for: cell).answer = text

这就为给定的单元格设置了QuestionBuilder上的答案。

同样地,替换掉//TODO:中的

createQuestionCell(_:hintTextDidChange:),改为这样。

questionBuilder(for: cell).hint = text

然后,替换掉//TODO:中的

createQuestionCell(_:promptTextDidChange:),用这个。

questionBuilder(for: cell).prompt = text

这些设置了给定单元格的QuestionBuilder上的提示和提示。

当你在做这个的时候,你还需要完成

createQuestionGroupTitleCell(_:titleTextDidChange:)来保持问题组的标题。把里面的//TODO替换成以下内容。

questionGroupBuilder.title = text

建立并运行,导航到CreateQuestionGroupViewController,再次输入几个文本的问题并尝试滚动。这一次,一切都像预期的那样工作

然而,"保存 "按钮仍然没有任何作用。是时候让你解决这个问题了。将savePressed(_:)改为以下内容。

你试图通过调用questionGroupBuilder.build()来创建一个新的提问组。如果这成功了,你就通知委托人。

如果它抛出一个错误,你就提醒用户输入所有需要的字段。建立并运行,导航到CreateQuestionGroupViewController,输入一个标题,并创建几个问题。

点击 "保存",你就会看到你全新的问题组被添加到 "选择问题组 "列表中。

#关键点

你在本章中学习了构建器模式。下面是它的关键点。

它涉及三个对象:主管、产品和构建器。

今后的发展方向是什么?

自从你创建了RabbleWabble之后,它确实有了长足的进步,但是你仍然有很多功能可以添加。

这些都可以使用你在 "基本设计模式 "这一节中学到的现有模式。你可以随心所欲地继续构建Rabble Wabble。

如果你已经完成了整个第一节的学习,那么恭喜你。你已经学会了许多最常用的iOS设计模式!

但你的设计模式之旅并没有到此为止。继续学习下一节,了解中级设计模式,包括MVVM、Adapter、Factory等。

关键点

你在本章中学习了构建器模式。下面是它的关键点。

它涉及三个对象:主管、产品和构建器。

今后的发展方向是什么?

自从你创建了RabbleWabble之后,它确实有了长足的进步,但是你仍然有很多功能可以添加。

这些功能都可以使用你在 "基本设计模式 "部分学到的现有模式。你可以随心所欲地继续构建Rabble Wabble。

如果你已经完成了整个第一节的学习,那么恭喜你。你已经学会了许多最常用的iOS设计模式!

但你的设计模式之旅并没有到此为止。继续学习下一节,了解中级设计模式,包括MVVM、Adapter、Factory等。


上一章 目录 下一章