Swift解决方案:Flyweight模式

Swift Solutions是涵盖设计模式的一系列文章。 在每篇文章中,我们讨论该模式是什么,何时应用以及如何以Swifty方式实现它。

飞量模式是一种节省内存的模式,当有许多要实例化的对象共享相似性时使用。 在本文中,我们将说明并编写Swift中的flyweight模式。 阅读本文之后,您将知道在必须创建大量相关对象时如何节省内存。

插图

首先,让我们使用文本编辑器的经典示例。 文本编辑器实例化并重复使用所有26个字母。 例如,当键入“ HELLO WORLD”时,我们将在三个不同的时间重新创建“ L”字符。 这很浪费,因为我们创建了三个字符对象来表示相同的字母。 flyweight模式的目标是共享可重用的对象,而不是不必要地复制它们,从而使我们的文本编辑器轻巧。

我们通过首先将对象分为两部分来重用对象:外部状态和内部状态。 外在的是指对象的根据上下文而改变的部分,因此无法共享。 例如,一个字符可能被加粗,着色或具有较大的字体大小。 这类数据不可重用,因为我们不希望给定字符的所有实例都共享这些属性。

另一方面,固有数据表示跨字符保持相同的内容。 固有数据的一个示例是给定字符的形状。 所有重复的字符都是渲染的形状,并且该形状不会从一次出现更改为下一次出现。 每当角色出现在我们的整个作品中时,我们都可以使用相同的“ L”形,然后对其应用外在属性。

总结一下:

  • 内部数据是不可变的,相同的,没有上下文的,因此可以重用。
  • 外在数据是可变的和上下文的,因此,并非在所有情况下都可以重用。

通过分离内部数据和外部数据,我们能够确定我们可以在对象中重用的内容。 考虑到这一点,让我们跳入代码示例。

实作

我们将在我们的代码中模拟一支装满步兵的军队。 我们还可以有弓箭手,将军和许多其他类型的士兵,并且我们想尽可能地重复使用,因为这些士兵实体中的每一个都会大量存在。

protocol Soldier { func render(from location: CGPoint, to newLocation: CGPoint) } 

首先,我们创建一个Soldier协议,该协议具有将士兵在网格上的原始位置以及士兵将要移动到的新位置的功能。 我们代码的目标是随着战斗的进行在网格上绘制步兵部队。

由于每个士兵都有一个唯一的位置,因此位置被认为是外部状态。 Flyweight对象不会存储位置,但是它们仍需要通过其功能输入来处理外部数据。 不久之后会更多。

飞行重量

让我们看看Flyweight对象的外观:

 class Infantry: Soldier { private let modelData: Data init(modelData: Data) { self.modelData = modelData } func render(from location: CGPoint, to newLocation: CGPoint) { // Remove rendering from original location // Graphically render at new location } } 

我们的InfantrySoldier ,是我们的轻量化目标。 作为我们的重量级modelData ,它将内部数据存储在modelData属性中。 此虚拟属性包含用于渲染步兵的图形。 由于我们部队中的所有步兵看上去都是一样的,因此我们使用一种模型来渲染所有这些。

关于Flyweight对象的两件事要注意:

  1. 它们仅包含对固有状态的引用
  2. 他们必须与外部状态互动

由于Flyweight是要重用的,因此它们只能包含内部数据。 但是,我们仍然需要在特定位置渲染士兵,因此,我们无需将位置存储在我们的举重装置中,而是将它们分别存储在客户端对象中,然后将其传递举重装置中以临时用于渲染。 这样,Flyweight可以与外部数据交互,但从不存储上下文信息。

客户

我们仍然缺少一个客户来存储我们所有的外部数据。

 class SoldierClient { // 1 var currentLocation: CGPoint // 2 let soldier: Soldier init(currentLocation: CGPoint, soldier: Soldier) { self.currentLocation = currentLocation self.soldier = soldier } // 3 func moveSoldier(to nextLocation: CGPoint) { soldier.render(from: currentLocation, to: nextLocation) currentLocation = nextLocation } } 

上面的代码完成以下任务:

  1. 存储代表士兵当前位置的CGPoint。
  2. 存储对flyweight对象的引用。
  3. 创建一个包装器函数,将新位置传递给士兵飞重。 士兵飞重将士兵从其初始位置移开,然后在新位置重新绘制。 随后, currentLocation被更新以反映新位置。

所有外部数据都存储在客户端中,但需要由举重装置使用。 换句话说,多个客户可以重复使用轻型配重,以在其唯一所需的位置渲染步兵。

我们的客户指的是符合Soldier的对象,但是我们如何确保多个客户不创建重复的引用? 如果所有Infantry类型都相同且可共享,那么如果在我们的应用程序中实例化了多个步兵对象,则将破坏模式的目的。 有必要确保所有客户仅共享一个步兵对象。

这是工厂对象变得有用的地方:

 class SoldierFactory { // 1 enum SoldierType { case infantry } // 2 private var availableSoldiers = [SoldierType: Soldier]() // 3 static let sharedInstance = SoldierFactory() private init(){} private func createSoldier(of type: SoldierType) -> Soldier { switch type { case .infantry: let infantry = Infantry(modelData: Data()) availableSoldiers[type] = infantry return infantry } } // 4 func getSoldier(type: SoldierType) -> Soldier { if let soldier = availableSoldiers[type] { return soldier } else { let soldier = createSoldier(of: type) return soldier } } } 

工厂目标是确保仅实例化给定类型的一名混凝土士兵。 如果对象不存在,则会创建该对象,将其添加到availableSoldiers词典中,然后返回给调用方。 下次另一个客户需要步兵时,它将简单地重用现有的步兵。 以下是分步细分:

  1. 创建所有可能的具体士兵的枚举。 我们只有一个,但是随着时间的推移,您可以想象会添加更多(弓箭手,有人吗?)
  2. 存储包含所有实例化士兵的私人词典。
  3. 我们确保我们的工厂是单身人士。 我们希望所有调用者都引用相同的对象池,因此它们也共享一个工厂。 要正确处理单例,请阅读Swift解决方案:单例。
  4. 每当客户请求士兵时,我们都会检查availableSoldiers是否已经存在。 如果没有,我们实例化并将其存储在字典中,然后将其返回。 以后所有对步兵的请求都将被重用,而不是重新创建。

用法

现在我们已经设置了模式,让我们简要地看看它是如何使用的。

 let soldierFactory = SoldierFactory.sharedInstance let infantry = soldierFactory.getSoldier(type: .infantry) let firstSoldier = SoldierClient(currentLocation: CGPoint.zero, soldier: infantry) let secondSoldier = SoldierClient(currentLocation: CGPoint(x: 5, y: 10), soldier: infantry) firstSoldier.moveSoldier(to: CGPoint(x: 1, y: 5)) 

我们首先创建对SoldierFactory的引用,然后获得对步兵对象的引用。 由于这是我们第一次请求步兵,因此将创建一个新实例并将其存储在工厂中。 然后,我们创建了两个位于不同位置的步兵,然后继续将我们的第一个士兵转移到另一个位置。

最后的想法

我应该提到这种模式的变体。 一些版本将工厂存储在客户端中,并且具有所有外部状态。 这种方法有很多好处,但为简单起见,它们不在本文讨论范围之内。

相反,为了简洁起见,我给出了一个替代实现。 即使每个渲染士兵增加了客户数量,但由于共享了内在状态,因此成本也大大降低了。 举重的核心概念已得到证明。

因此,在将来,只要您发现需要实例化大量共享可以被分离为固有数据的对象的对象,就可以使用这种模式。 性能对用户很重要,这种模式一定可以实现!

最初于 2017 年9月16日 发布在 swiftcraft.io 上。