文章目录
- 从一个 MP3 案例谈起
- flyweight 模式解决
- flyweight pattern 的组件
- 拆解定义与逐步实现
- 完整代码
- 未讨论问题
享元模式(
flyweight pattern
)属于结构型设计模式,主要用于解决系统中大量创建同一个类的实例时导致的内存激增的问题,它的解决思路是将类的实例属性拆分成外部属性和内部属性。
- 外部属性:会被外部修改的属性
- 内部属性:实例创建之后就不会变更的属性
从一个 MP3 案例谈起
现编案例,如有不恰当可以指正
案例
假设我们现在有一个系统用于管理每个人的 MP3 设备,这些 MP3 有不同的颜色、牌子、型号…还有每个人在设备里的个性化配置、自己导入的音乐。
代码实现
系统的早期实现版本如下:
import tracemalloc
from collections import namedtuple
def parse_comma_data(raw_data:str, char=","):
lines = raw_data.split("\n")
return (l.split(char) for l in lines)
class MP3:
def __init__(self, brand, model, memory_size, color, settings, music_list=None):
self.brand = brand
self.model = model
self.memory_size = memory_size
self.color = color
self.settings = settings
self.music_list = music_list
self.equipment_parameters = bytes(10000000)
self.system = bytes(10000000)
def __str__(self):
return f"{self.model} {self.color} {self.memory_size} with id:{id(self)}"
def __repr__(self):
return self.__str__()
def add_music(self, new_music:str):
if self.music_list is None:
self.music_list = []
self.music_list.append(new_music)
# 系统需要管理的 MP3 设备
mp3_raw_list = """john,sony,S1001,4G,red
cindy,sony,S1001,4G,red
simon,sony,S1001,4G,red
lucy,sony,S1001,4G,red
babala,sony,S1002,4G,red
tom,sony,S1002,4G,red
dikaer,sony,S1005,16G,red"""
def main():
mp3_devices = {}
for data in parse_comma_data(mp3_raw_list):
name,brand,model,memory_size,color = data
mp3_devices[name] = MP3(
brand, model, memory_size, color, settings=name
)
return mp3_devices
if __name__ == "__main__":
tracemalloc.start()
mp3_list = main()
print("内存占用:", tracemalloc.get_traced_memory()[0])
tracemalloc.stop()
for name, mp3 in mp3_list.items():
print(name, mp3)
上面这段代码将mp3_raw_list
中记录的每个独立的 MP3 导入系统中管理,系统可以根据指定用户调用他们对应的 mp3 对象 (mp3_list[name]
)。
值得注意的是 MP3
class 中有两个属性比较大,比较占内存空间:
self.equipment_parameters = bytes(10000000)
self.system = bytes(10000000)
同时,我们调用了 tracemalloc
库来监测内存的使用情况,并在代码最后打印每个设备的 id
, 下面是它的控制台输出
内存占用: 140004589
john S1001 red 4G with id:1658049569424
cindy S1001 red 4G with id:1658049567568
simon S1001 red 4G with id:1658049573200
lucy S1001 red 4G with id:1658049567504
babala S1002 red 4G with id:1658049561936
tom S1002 red 4G with id:1658049561808
dikaer S1005 red 16G with id:1658049562576
从这段输出我们可以看到三个点:
- 这些 mp3 设备有一些是同一款产品,比如 john、cindy、simon、lucy 他们四个人都是
sony,S1001,4G,red
, 但也注意到他们每个人的设置都不一样,这点可以看mp3.settings
这个属性都不同就知道 - 这些 mp3 对象在系统中都是独立的,这点可以从它们的
id
都不同看出来 - 每个 mp3 对象的内存开销大部分消耗在设备基础属性(
equipment_parameters
)和操作系统(system
), 每增加一个在线用户就需要增加20000000
字节的空间消耗:# self.equipment_parameters = bytes(10000000) # self.system = bytes(10000000) 上面有 7 个设备,则消耗: 10000000 * 2 * 7 = 140000000 这个数值接近上面的控制台输出 140004589
flyweight 模式解决
flyweight pattern 的组件
首先我们来看下 flyweight 模式有哪些关键组件,然后再按些组件的功能定义来实现代码。
Flyweight
: 包含多个对象共享的固有状态,同一个flyweight
对象可在不同的上下文中使用。它存储内在状态,并从上下文中接收外在状态。Context
: 保存所有原始对象独有的外在状态。当与一个 flyweight 对象配对时,它代表了原始对象的全部状态。Flyweight Factory
: 管理现有的flyweight
实例池,处理flyweight
实例的创建和重用。Client
通过与factory
交互来获取flyweight
实例,并传递内部属性
以进行检索或创建。Client
: 计算或存储flyweight
对象的外部属性。 它将flyweight
视为模板对象,并在运行时通过向其方法中传递上下文数据对其进行配置。
拆解定义与逐步实现
这段定义显然不够清晰,那我们配合着图形与案例看它们的关系:
对于我们的 MP3 实例而言,它应该是由内在状态(属性)和外在状态(属性)组成
显然除了
self.settings
和self.music_list
两个属性是用户在拥有设备后可以更改的属性,其他的属性无法修改(这里指的是现实商品层面的无法修改,我们并没有在代码实现上阻止调用者修改这些实例属性)
根据上面的定义知道两点:
flyweight
对象具有 MP3 实例的内部属性
context
对象具有 MP3 实例的内部属性
和外部属性
根据这两点,我们继续演进上面的关系图:
从这里我们可以看到 MP3 obj
和 context obj
是指代一种东西,即一个完整的 MP3 对象,实现的过程中命名为 context
还是 MP3
都是一样的,区别在于
mp3_obj = 内部属性 + 外部属性
context_obj = flyweight_obj + 外部属性
现在我们可以完成代码中 flyweight
和 context
部分的实现了:
# 此时 MP3 是一个 flyweight 对象
class MP3:
def __init__(self, brand, model, memory_size, color):
self.brand = brand
self.model = model
self.memory_size = memory_size
self.color = color
# 比较占内存空间的变量
self.equipment_parameters = bytes(1000000)
self.system = bytes(1000000)
def __str__(self):
return f"{self.model} {self.color} {self.memory_size} with id:{id(self)}"
def __repr__(self):
return self.__str__()
# context 对象,或者说具体的 mp3 对象,它具备了其内部属性和外部属性
class PersonalMP3:
def __init__(self, brand,model,memory_size,color, settings="default setting", music_list=None):
self.mp3 = MP3(brand,model,memory_size,color)
self.settings = settings
self.music_list = music_list
def add_music(self, new_music:str):
if self.music_list is None:
self.music_list = []
self.music_list.append(new_music)
上面这段代码我们让 MP3 class
只保留内部属性;而 PersonalMP3 class
则由这个 MP3 class
和外部属性组成。
需要注意的是,此时我们的 PersonalMP3.mp3
并没有复用,它根据入参实例化一个对象!
现在回过头来看 flyweight factory
的定义:
- 管理现有的
flyweight
实例池 - 传递
内部属性
以进行检索或创建
基于这段定义,我们需要继续修改上面的关系图:
根据第二点(传递 内部属性
以进行检索或创建),flyweight factory
返回实例的逻辑图应该是:
再次修改代码,增加 flyweight factory
, 并修改上面 PersonalMP3
属性中 MP3
实例的获取逻辑。
from collections import namedtuple
Device = namedtuple("Device", ["brand", "model", "memory_size", "color"])
class MP3:
# 省略
...
# flyweight factory 用于提供 flyweight obj
class MP3Factory:
cache = {}
@classmethod
def get_mp3(cls, device: Device):
if device not in cls.cache:
cls.cache[device] = MP3(
brand=device.brand,
model=device.model,
memory_size=device.memory_size,
color=device.color
)
return cls.cache[device]
# context 对象,或者说具体的 mp3 对象,它具备了其内部属性和外部属性
class PersonalMP3:
def __init__(self, device:Device, settings="default setting", music_list=None):
self.mp3 = MP3Factory.get_mp3(device)
# 省略
...
这段代码中我们在 MP3Factory
中维护了一个字典,用内部属性(Device
具名元组 )作为键索引其对应的 flyweight
实例。
- 若不存在则新建后放进字典里并返回
- 若存在则直接返回
完整代码
现在我们将所有代码合并在一起:
from collections import namedtuple
import tracemalloc
Device = namedtuple("Device", ["brand", "model", "memory_size", "color"])
def parse_comma_data(raw_data:str, char=","):
lines = raw_data.split("\n")
return (l.split(char) for l in lines)
# 此时 MP3 是一个 flyweight 对象
class MP3:
def __init__(self, brand, model, memory_size, color):
self.brand = brand
self.model = model
self.memory_size = memory_size
self.color = color
# 比较占内存空间的变量
self.equipment_parameters = bytes(1000000)
self.system = bytes(1000000)
def __str__(self):
return f"{self.model} {self.color} {self.memory_size} with id:{id(self)}"
def __repr__(self):
return self.__str__()
# flyweight factory 用于提供 flyweight obj
class MP3Factory:
cache = {}
@classmethod
def get_mp3(cls, device: Device):
if device not in cls.cache:
cls.cache[device] = MP3(
brand=device.brand,
model=device.model,
memory_size=device.memory_size,
color=device.color
)
return cls.cache[device]
# context 对象,或者说具体的 mp3 对象,它具备了其内部属性和外部属性
class PersonalMP3:
def __init__(self, device:Device, settings="default setting", music_list=None):
self.mp3 = MP3Factory.get_mp3(device)
self.settings = settings
self.music_list = music_list
def add_music(self, new_music:str):
if self.music_list is None:
self.music_list = []
self.music_list.append(new_music)
mp3_list = """john,sony,S1001,4G,red
cindy,sony,S1001,4G,red
simon,sony,S1001,4G,red
lucy,sony,S1001,4G,red
babala,sony,S1002,4G,red
tom,sony,S1002,4G,red
dikaer,sony,S1005,16G,red"""
def main():
mp3_devices = {}
for data in parse_comma_data(mp3_list):
name, *device = data
mp3_devices[name] = PersonalMP3(Device(*device), settings=name)
return mp3_devices
tracemalloc.start()
mp3_list = main()
print("内存占用:", tracemalloc.get_traced_memory())
tracemalloc.stop()
for name, mp3 in mp3_list.items():
print(name, mp3.mp3)
控制台输出:
内存占用: 6004821
john S1001 red 4G with id:1658046368208
cindy S1001 red 4G with id:1658046368208
simon S1001 red 4G with id:1658046368208
lucy S1001 red 4G with id:1658046368208
babala S1002 red 4G with id:1658046370512
tom S1002 red 4G with id:1658046370512
dikaer S1005 red 16G with id:1658046378256
注:这里打印出来的 id 不是
PersonalMP3
的 id, 而是PersonalMP3.mp3
的 id
相比上一个版本的实现,内存的开销缩小了 23 倍(140004589/6004821
),尽管每个 PersonalMP3
的 id
还是独立的,但是每个实例下面的 mp3
属性指向的实例对象的 id
则存在相同的现象,即有些是共用的,比如上面的 john、cindy、simon、lucy
。
读到这里,你或许会问组件定义中的 client
怎么没提及,这段实际上是代码中的 main
函数。
未讨论问题
- 上面的
add_music
这里对象本身的行为是否一定要从flyweight
对象中拆分出来? - 这个模式引入什么弊病?
先吃饭,下次再补全