每当我们创建新的对象时,需要分配额外的内存。如果系统的所有物理内存被耗尽,它将开始与二级存储交换页面,通常是硬盘驱动器(HDD),由于主内存和HDD之间的性能差异,在大多数情况下,这是不可接受的。固态硬盘(SSD)通常比HDD有更好的性能,但SSD不会在短期内完全取代HDD。
除了内存的使用,性能也是考虑因素。图形软件,包括计算机游戏,应该能够极快地渲染3D信息(例如成千上万棵树的森林,充满士兵的村庄,或者有很多汽车的城市)。如果3D地形中的每个物体都是单独创建的,并且没有使用数据共享,那么性能就会很差。
flyweight设计模式是一种技术,通过在类似的对象之间引入数据共享来最大限度地减少内存的使用并提高性能。flyweight是共享对象,它包含与状态无关的、不可变的(也称为内在的)数据。与状态有关的、可变的(也称为外在的)数据不应该是flyweight的一部分。如果flyweight需要外在数据,它应该由客户端代码显式提供。
假设我们正在创建性能敏感的游戏,例如射击游戏(FPS)。在FPS游戏中,玩家(士兵)共享一些状态,比如说代表和行为。例如,在《反恐精英》中,同一团队的所有士兵(反恐人员对恐怖分子)看起来是一样的(表征)。在同一个游戏中,所有的士兵(两队)都有一些共同的动作,如跳跃、躲避等等(行为)。这意味着我们可以创建一个飞轮,它将包含所有的共同数据。当然,士兵们也有很多数据,每个士兵都是不同的,不会成为飞行重量的一部分,如武器、健康、位置等等。
真实世界的例子
我们可以把flyweight看作是现实生活中的缓存。例如,许多书店都有专门的书架,摆放着最新和最流行的出版物。这就是一个缓存。首先,你可以在专用书架上看一看你要找的书,如果你找不到要求书商协助。
Exaile音乐播放器使用flyweight来重用由相同URL识别的对象(在这里是指音乐曲目)。如果一个新的对象与现有的对象有相同的URL,那么创建新的对象是没有意义的,所以同一对象被重复使用以节省资源。
Peppy,一个用Python实现的类似XEmacs的编辑器,使用flyweight模式来存储一个主要模式状态栏的状态。这是因为除非被用户修改,否则所有的状态栏都共享相同的属性。
使用
Flyweight都是为了提高性能和内存的使用。所有的嵌入式系统(手机、平板电脑、游戏机、微控制器等)和对性能要求很高的应用(游戏、三维图形处理、实时系统等)都能从中受益。
Gang of Four (GoF)书中列出了有效使用flyweight模式需要满足的以下要求。
- 应用程序需要使用大量的对象。
- 对象太多,存储/渲染它们的成本太高。一旦删除了可变的状态(因为如果需要的话,应该由客户端代码明确地传递给flyweight),许多组不同的对象就可以被相对较少的共享对象所取代。
- 对象身份对应用程序来说并不重要。我们不能依赖对象身份,因为对象共享会导致身份比较失败(对客户代码来说显得不同的对象最终会有相同的身份)。
实例
我们将创建一个小的停车场,确保整个输出可以在一个终端页面上阅读。然而,无论你把停车场做得多大,内存分配都是一样的。
首先,我们需要一个Enum参数来描述停车场中的三种不同类型的汽车。
CarType = Enum('CarType', 'subcompact compact suv')
然后,我们将定义我们实现的核心的类:Car。pool变量是对象池(换句话说,我们的缓存)。注意,pool是类的属性(所有实例共享的变量)。
使用 new() 特殊方法,即在 init() 之前调用,我们正在将 Car 类转换为一个支持自我引用的元类。这意味着 cls 引用了 Car 类。当客户端代码创建一个 Car 的实例时,他们将汽车的类型作为 car_type 传递。汽车的类型被用来检查是否已经有相同类型的汽车被创建。如果是这样,先前创建的对象将被返回;否则,新的汽车类型被添加到池中并返回。
import random
from enum import Enum
CarType = Enum('CarType', 'subcompact compact suv')
class Car:
pool = dict()
def __new__(cls, car_type):
obj = cls.pool.get(car_type, None)
if not obj:
obj = object.__new__(cls)
cls.pool[car_type] = obj
obj.car_type = car_type
return obj
def render(self, color, x, y):
type = self.car_type
msg = f'render a car of type {type} and color {color} at ({x}, {y})'
print(msg)
def main():
rnd = random.Random()
#age_min, age_max = 1, 30 # in years
colors = 'white black silver gray red blue brown beige yellow green'.split()
min_point, max_point = 0, 100
car_counter = 0
for _ in range(10):
c1 = Car(CarType.subcompact)
c1.render(random.choice(colors),
rnd.randint(min_point, max_point),
rnd.randint(min_point, max_point))
car_counter += 1
for _ in range(3):
c2 = Car(CarType.compact)
c2.render(random.choice(colors),
rnd.randint(min_point, max_point),
rnd.randint(min_point, max_point))
car_counter += 1
for _ in range(5):
c3 = Car(CarType.suv)
c3.render(random.choice(colors),
rnd.randint(min_point, max_point),
rnd.randint(min_point, max_point))
car_counter += 1
print(f'cars rendered: {car_counter}')
print(f'cars actually created: {len(Car.pool)}')
c4 = Car(CarType.subcompact)
c5 = Car(CarType.subcompact)
c6 = Car(CarType.suv)
print(f'{id(c4)} == {id(c5)}? {id(c4) == id(c5)}')
print(f'{id(c5)} == {id(c6)}? {id(c5) == id(c6)}')
if __name__ == '__main__':
main()
render()方法将被用来在屏幕上渲染汽车。请注意,所有不为flyweight所知的可变信息都需要由客户端代码明确地传递。比如随机的颜色和位置的坐标(形式为x,y)被用于每个汽车。
main()函数展示了我们如何使用flyweight模式。汽车的颜色是从预定义的颜色列表中的一个随机值。坐标使用1到100之间的随机值。虽然渲染了18辆汽车,但只为3辆分配了内存。输出的最后一行证明,在使用flyweight时,我们不能依赖对象的身份。id()函数返回一个对象的内存地址。这不是Python中的默认行为,因为默认情况下,id()为每个对象返回一个唯一的ID(实际上是对象的内存地址,是一个整数)。在我们的例子中,即使两个对象看起来是不同的,但如果它们属于同一flyweight家族(在这个例子中,这个家族是由car_type定义的),它们实际上具有相同的身份。当然,不同的身份比较仍然可以用于不同家族的对象,但这只有在客户端知道实现细节的情况下才可能。
网友评论