享元模式(Flyweight Design Pattern),顾名思义,就是被共享的单元。享元模式的意图是复用对象,节省内存的消耗。前提是享元对象是不可变对象。
享元模式把对象的状态分成内部状态和外部状态,内部状态即是不变的,外部状态是变化的;然后通过共享复用不变的部分,达到减少对象数量并节约内存的目的。
优缺点
优点
- 减少对象频繁重复创建,降低内存消耗 ,提高效率。
缺点
- 提高了系统的复杂度,需要分离出外部状态和内部状态。
享元模式的构成
内部状态类:不变的部分,抽象出来作为享元的基类。
享元工厂类:所有内部状态类的集合,通过工厂类获取。
外部状态类:实际使用的类,可以自定义自己的属性和方法。
通过一个象棋的例子来说明享元模式。
象棋游戏中,有非常多的棋局,如果每个棋局都创建自己的棋子对象,则有大量重复的对象被创建出来。每个棋局中,用到的棋子都是一样的,只是坐标不一样,这些基本的棋子可以作为享元中的内部状态类。
把这些棋子对象使用享元工厂抽象出来复用。
每个棋局不一样的只是具体棋子的坐标,还有所属的棋局,这些每个棋局中具体的棋子就是外部状态类。
我们拿其中的帅和兵列举,其它是同理。
内部状态类
# 要被共享的类就是这些基本的棋子类
class Chess:
pass
class Jiang(Chess):
def __init__(self):
self.text = "将"
def __str__(self):
return "这是一个将"
class Soldier(Chess):
def __init__(self):
self.text = "兵"
def __str__(self):
return "这是一个兵"
我们创建了基本的棋子类,以及基本的将和兵类,它们就是要被共享和复用的内部状态类。
享元工厂类
class FlyWeightFactory:
def __init__(self):
self.chess_map = {
"jiang": Jiang(),
"soldier": Soldier()
}
def get_chess(self, chess_name):
assert chess_name in self.chess_map, "工厂中不存在该名字的类"
return self.chess_map[chess_name]
通过一个享元工厂来缓存所有的基本棋子对象,工厂类获取到的就是享元。
这样就从每个棋局维护三十个多个基本棋子对象,变成了全局维护三十多个基本棋子对象,节省了大量内存。
每当有新的基本棋子类时,只需要在chess_map
中新增该类实例化的映射即可。
外部状态类
class ChessPiece:
def __init__(self, chessboard_id, chess: Chess, color):
self.x = 0
self.y = 0
self.chessboard_id = chessboard_id
self.color = color
self.chess = chess
def get_chess_info(self):
info = f"chessboard_id:{self.chessboard_id};{self.chess} with {self.color} in {self.x}/{self.y}"
return info
具体的棋子类,需要通过chessboard_id
关联到具体的棋盘,并且有自己的坐标 x 和 y 以及颜色信息等,并且使用了基本棋子类的信息。
具体使用
# 享元工厂
fac = FlyWeightFactory()
# 创建一个id为1的棋盘中具体应用的红色小兵
chessboard_id = 1
red_soldier = ChessPiece(chessboard_id, fac.get_chess("soldier"), "red")
print(red_soldier.get_chess_info())
# 结果
chessboard_id:1;这是一个兵 with red in 0/0
总结
享元模式的实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 map 或者 list 来缓存已经创建过的享元对象,来达到复用,减少内存消耗的目的。
享元模式与单例、缓存、线程池这些非常相似,都是为了共享,它们之间的区别主要是:
- 应用单例模式是为了保证对象全局唯一。
- 应用享元模式是为了实现对象复用,节省内存。
- 缓存是为了提高访问效率,而非复用。
- 池化技术中的“复用”理解为“重复使用”,主要是为了节省时间。
根据不同的应用场景,使用不同的共享方式。