组合模式是一种结构模式,它将一组对象组合成一个树状结构,这样它们就可以像一个对象一样被操作。它使用三种类型。
组件协议确保树状结构中的所有构造都可以被同样的方式处理。
叶子是树上的一个组件,它没有子元素。
复合体是一个容器,可以容纳叶子对象和复合体。
复合体和叶子节点都源自于组件协议。你甚至可以在一个复合对象中持有几个不同的叶子类。
例如,一个数组就是一个复合体。组件是数组本身。复合体是Array用来容纳叶子对象的一个私有容器。每个叶子都是一个具体的类型,如Int、String或任何你添加到数组中的类型。
如果你的应用程序的类层次结构形成了一个分支模式,试图为分支和节点创建两种类型的类会使这些类很难沟通。
你可以用组合模式来解决这个问题,通过使分支和节点符合一个协议,对它们进行相同的处理。这为你的模型增加了一层抽象,最终降低了模型的复杂性。
打开Starter目录下的AdvancedDesignPatterns.xcworkspace,然后打开Composite页面。
在这个playground实例中,你将制作一个以树状模式存储不同元素的应用程序。
一个文件层次结构是组合模式的一个日常例子。想一想文件和文件夹。所有的.mp3和.jpeg文件,以及文件夹,都有很多共同的功能。"打开"、"移动到垃圾箱"、"获取信息"、"重命名 "等。你可以移动和存储不同的文件组,即使它们不都是同一类型,因为它们都符合一个组件协议。
要在playground上制作自己的文件层次,请在代码示例后添加以下内容。
import Foundation
protocol File {
var name: String { get set }
func open()
}
你刚刚创建了一个组件协议,所有的叶子对象和合成物都将符合这个协议。接下来,你要添加几个叶子对象。在playground的末端添加以下内容。
final class eBook: File {
var name: String
var author: String
init(name: String, author: String) {
self.name = name
self.author = author
}
func open() {
print("Opening \(name) by \(author) in iBooks...\n")
}
}
final class Music: File {
var name: String
var artist: String
init(name: String, artist: String) {
self.name = name
self.artist = artist
}
func open() {
print("Playing \(name) by \(artist) in iTunes...\n")
}
}
你已经添加了两个符合组件协议的叶子对象。它们都有一个名字属性和一个打开函数,但每个open()都根据对象的类别而不同。
接下来,将以下代码添加到playground的末尾。
final class Folder: File {
var name: String
lazy var files: [File] = []
init(name: String) {
self.name = name
}
func addFile(file: File) {
self.files.append(file)
}
func open() {
print("Displaying the following files in \(name)...")
for file in files {
print(file.name)
}
print("\n")
}
}
你的文件夹对象是一个复合体,它有一个数组,可以容纳任何符合文件协议的对象。这意味着,一个文件夹不仅可以容纳音乐和电子书对象,还可以容纳其他文件夹对象。
请自由发挥,创建对象并将它们放在playground的文件夹中。这里有一个例子,展示了一些叶子对象和合成物。
let psychoKiller = Music(name: "Psycho Killer",
artist: "The Talking Heads")
let rebelRebel = Music(name: "Rebel Rebel",
artist: "David Bowie")
let blisterInTheSun = Music(name: "Blister in the Sun",
artist: "Violent Femmes")
let justKids = eBook(name: "Just Kids",
author: "Patti Smith")
let documents = Folder(name: "Documents")
let musicFolder = Folder(name: "Great 70s Music")
documents.addFile(file: musicFolder)
documents.addFile(file: justKids)
musicFolder.addFile(file: psychoKiller)
musicFolder.addFile(file: rebelRebel)
blisterInTheSun.open()
justKids.open()
documents.open()
musicFolder.open()
你能够统一对待所有这些对象,并对它们调用相同的函数。但是,引用上面提到的Talking Heads的歌来说 "Qu'est-ce que c'est? (这是什么意思?)"
当你能够以同样的方式对待不同的对象时,使用组合模式就变得有意义了,重用对象和编写单元测试就变得不那么复杂了。
想象一下,在不使用组件协议的情况下,试图为你的文件创建一个容器。存储不同类型的对象会很快变得复杂。
在使用组合模式之前,请确保你的应用程序有一个分支结构。如果你看到你的对象有很多几乎相同的代码,让它们符合一个协议是个好主意,但不是所有涉及协议的情况都需要复合对象。
在本节中,你将为一个名为 "打败你的ToDo列表 "的应用程序添加功能。
在Projects ▸ Starter目录下,在Xcode中打开DefeatYourToDoList\DefeatYourToDoList.xcodeproj。这个应用程序允许用户将项目添加到待办事项列表中。
当用户勾选项目时,屏幕上方的战士会向地牢尽头的宝藏靠近。当用户完成了100%的任务时,战士就会到达终点。
在这个项目中,你要添加一个功能,用户可以创建一个任务,其中包含较小的任务,就像一个检查清单。
首先,打开Models.swift,在下面添加导入Foundation。
protocol ToDo {
var name: String { get set }
var isComplete: Bool { get set }
var subtasks: [ToDo] { get set }
}
final class ToDoItemWithCheckList: ToDo {
var name: String
var isComplete: Bool
var subtasks: [ToDo]
init(name: String, subtasks: [ToDo]) {
self.name = name isComplete = false
self.subtasks = subtasks
}
}
在这里,你已经添加了一个组件协议,叫做ToDo,你所有的待办事项对象都应该符合这个协议。你还添加了一个名为ToDoItemWithCheckList的复合对象,它在一个名为subtasks的数组中存储你的检查列表项目。
现在,为了真正使用组合模式,你需要使你的默认待办事项符合组件协议。还是在Models.swift中,用以下代码替换ToDoItem。
final class ToDoItem: ToDo {
var name: String
var isComplete: Bool
var subtasks: [ToDo]
init(name: String) {
self.name = name
isComplete = false
subtasks = []
}
}
你会注意到,为了让你的默认ToDoItem符合ToDo协议,你必须给它一个子任务属性。虽然将子任务初始化为一个空数组可能看起来是不必要的附加复杂性,但在接下来的步骤中你会看到,让两个类都包括所有可能的属性,可以使你的视图控制器中的集合视图更容易重复使用自定义ToDoCell。
接下来,打开ViewController.swift。你想从顶部开始重构,在IBOutlet连接下面。每个任务都存储在一个叫做toDos的数组中,当完成后,它们会被添加到 completedToDos中。有两个数组,以便你知道任务完成的百分比,这将使战士沿着路径移动。
首先,你希望两个数组都能接受符合组件协议的项目,而不是简单的ToDoItem。把这两个属性替换成以下内容。
var toDos: [ToDo] = []
var completedToDos: [ToDo] = []
你应该在collectionView(:didSelectItemAt:)中得到一个编译器错误。为了消除这个错误,在collectionView(:didSelectItemAt:)里面,替换掉。
let currentToDo = toDos[indexPath.row] 。
用下面的方法。
var currentToDo = toDos[indexPath.row]。
你必须这么做,因为Swift无法判断ToDo这个协议是一个结构还是一个类。
如果它是一个结构体,那么currentToDo就必须被声明为var,以便能够对其进行变异。当然,你知道它实际上一直是一个类。
接下来,打开ToDoCell.swift并替换。
var subtasks: [ToDoItem] = []
用下面的内容替换。
var subtasks: [ToDo] = []
类似于你在ViewController.swift中的做法,你需要滚动到collectionView(_:didSelectItemAt:)并替换。
let currentToDo = subtasks[indexPath.row]
用下面的方法。
var currentToDo = subtasks[indexPath.row]
接下来,打开ViewController.swift。现在,是时候让你的集合视图单元格同时显示ToDoItem和ToDoItemWithCheckList。
首先,在UICollectionViewDataSource扩展中导航到collectionView(_:cellForItemAt:)。
在返回单元格上方添加以下内容。
if currentToDo is ToDoItemWithCheckList {
cell.subtasks = currentToDo.subtasks
}
这个if语句填充了ToDoCell中的子任务。自定义ToDoCell上的另一个集合视图已经为你设置好了,所以不需要在那里做任何改变。
接下来,对于位于视图控制器中的集合视图,你希望能够根据你的todo项目的检查表上有多少个子任务来改变单元格的高度。
向下滚动到collectionView(_:layout:sizeForItemAt:),用下面的内容替换它。
let width = collectionView.frame.width
let currentToDo = toDos[indexPath.row]
let heightVariance = 60 * (currentToDo.subtasks.count) let addedHeight = CGFloat(heightVariance)
let height = collectionView.frame.height * 0.15 + addedHeight
return CGSize(width: width, height: height)
现在,每个单元格的高度将为复合待办事项中的每个子任务增加60。
现在,是时候为用户添加创建ToDoItemWithCheckList的能力了! 在MARK: - Internal extension的末尾添加以下方法:。
一切就绪! 现在你可以添加你喜欢的待办事项。建立并运行该应用程序。试试新的功能,然后去拿那件宝物!
你在本章中了解了组合模式。下面是它的关键点。
组合模式是一种结构模式,它将一组对象组合成一棵树,这样就可以像操作一个对象一样来操作它们。
如果你的应用程序的类层次结构形成了一个分支模式,你可以通过使分支和节点符合一个组件协议,将它们视为几乎相同的对象。该协议为你的模型增加了一层抽象,从而降低了模型的复杂性。
这是一个很好的模式,可以帮助简化具有类似功能的多个类的应用程序。有了它,你可以更经常地重复使用代码,并减少你的类的复杂性。
一个文件的层次结构是组合模式的一个日常例子。所有的.mp3和.jpeg文件,以及文件夹,都有很多共同的功能,如 "打开 "和 "移动到垃圾桶"。你可以移动和存储不同的文件组,即使它们不都是同一类型,因为它们都符合一个组件协议。
由于你的 "打败你的待办事项清单 "应用程序现在使用组合模式,你可以在ToDoItem和ToDoItemWithCheckList上重复使用同一个自定义单元格,这真的很方便。另外,由于一个ToDoItemWithCheckList可以容纳另一个ToDoItemWithCheckList,你实际上可以写这个应用程序,在检查列表中拥有无数个检查列表!(我们不建议你在ToDoItemWithCheckList上这样做。(不过,我们不建议在这么小的屏幕上这样做!)
上一章 | 目录 | 下一章 |
---|