享元模式(结构型模式)
模式概述
享元模式是一种结构型设计模式,它将在每个对象中都保存所有数据的方式,改为共享多个对象的状态,从而实现让有限的资源处理更多的任务。
也就是说,享元模式通过共享技术实现相同或相似对象的复用,从而节省宝贵的计算资源。
问题
有一款极具特色的移动RPG游戏:大量且炫丽的粒子流会在整个地图上穿行,为玩家提供紧张刺激的体验。但游戏总是会在电脑上运行几分钟后崩溃。
当研究了几个小时的错误日志后,发现导致游戏崩溃的原因竟然是内存容量不足所引起。
移动设备的性能一般来说远比不上电脑,因此游戏运行在电脑上没问题而到了手机上却频频崩溃。
真正的问题与粒子系统有关:每个粒子都代表一种特效,都由包含完整数据的独立对象表示。
当玩家进入游戏高潮的某一时刻,由于系统无法在剩余内存中保存更多新建的粒子,所以程序就崩溃了。
方案
通过更细致地观察粒子(Particle)类,发现颜色(color)和形象(profile)这两个成员变量所消耗的内存要比其他变量多得多。而且对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样。
而每个粒子的另一些状态,如坐标、 移动矢量和速度则是不同的,因为这些成员变量的数值会不断变化。

内在状态与外在存储
对象的常量数据位于对象中,通常被称为内在状态,其他对象只能读取但不能修改。而对象的其他状态由于能被其他对象从外部
改变,因此被称为外在状态。
良好的设计应该只是在对象中保存内在状态,以方便在不同上下文中重用。这样一来所需的对象数量将会大为减少。

将这些外在状态,如如坐标、 移动矢量和速度存储容器对象中,也就是应用享元模式前的聚合对象中。
这里的容器对象就是Game对象,它会存储每个粒子的坐标、 方向矢量和速度。另外,还需要另一个数组来存储粒子特定的享元引用。
这些数组必须保持同步,这样才能使用同一个索引来获取关于某个粒子的所有数据。
更优雅的解决方案是创建独立的场景类(Scene)来存储外在状态和对享元对象的引用,在该方法中,容器类只需包含一个数组。
但这样一来,上下文对象数量就会和不采用该模式时的对象数量一样多,但这些对象要比之前小很多,因为消耗内存最多的成员变量已经被移动到很少的几个享元对象中了。
现在,一个大的享元对象会被上千个情境小对象复用,而无需再重复存储数千个大对象的数据。
不可变性
由于享元对象可在不同的上下文中使用,所以必须确保其状态不能被修改。
享元类的状态只能由构造函数的参数进行一次性初始化,而不能对其他对象公开或成为公有成员变量。
享元工厂
为了能更方便地访问各种享元,可以创建一个工厂方法来管理已有的享元对象。
工厂方法从客户端接收目标享元对象的内在状态作为参数,如果能在缓存池中找到所需享元,则将其返回给客户端;如果没有找到,就新建一个享元,并将其添加到缓存池中——这个工厂也是典型的单例模式。
也可以选择在程序的不同地方放入该函数,最简单的选择就是将其放置在享元容器中,如Game。除此之外,还可以新建一个工厂类,或者创建一个静态的工厂方法并将其放入实际的享元类中。
结构

实现
Java
/**
* 每棵树独特的状态
*
*/
public class Tree {
private int x;
private int y;
private TreeType type;
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void draw(Graphics g) {
type.draw(g, x, y);
}
}
/**
* 多棵树共享的状态
*
*/
public class TreeType {
private String name;
private Color color;
private String otherTreeData;
public TreeType(String name, Color color, String otherTreeData) {
this.name = name;
this.color = color;
this.otherTreeData = otherTreeData;
}
public void draw(Graphics g, int x, int y) {
g.setColor(Color.BLACK);
g.fillRect(x - 1, y, 3, 5);
g.setColor(color);
g.fillOval(x - 5, y - 10, 10, 10);
}
}
/**
* 封装创建享元的机制
*
*/
public class TreeFactory {
static Map<String, TreeType> treeTypes = new HashMap<>();
public static TreeType getTreeType(String name, Color color, String otherTreeData) {
TreeType result = treeTypes.get(name);
if (result == null) {
result = new TreeType(name, color, otherTreeData);
treeTypes.put(name, result);
}
return result;
}
}
/**
* 绘制森林
*
*/
public class Forest extends JFrame {
private List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, Color color, String otherTreeData) {
TreeType type = TreeFactory.getTreeType(name, color, otherTreeData);
Tree tree = new Tree(x, y, type);
trees.add(tree);
}
@Override
public void paint(Graphics graphics) {
for (Tree tree : trees) {
tree.draw(graphics);
}
}
}
/**
* 客户端
*
*/
public class Client {
// 画布大小
static int CANVAS_SIZE = 500;
// 树木的数量
static int TREES_TO_DRAW = 1_000_000;
// 树木的种类
static int TREE_TYPES = 2;
public static void main(String[] args) {
Forest forest = new Forest();
for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) {
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Summer Oak", Color.GREEN, "Oak texture stub");
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Autumn Oak", Color.ORANGE, "Autumn Oak texture stub");
}
forest.setSize(CANVAS_SIZE, CANVAS_SIZE);
forest.setVisible(true);
System.out.println("有 " + TREES_TO_DRAW + " 颗树");
System.out.println("---------------------");
System.out.println("内存使用:");
System.out.println("每棵树大小 (8 bytes) * " + TREES_TO_DRAW);
System.out.println("+ 树木类型大小 (~30 bytes) * " + TREE_TYPES + "");
System.out.println("---------------------");
System.out.println("总计:" + ((TREES_TO_DRAW * 8 + TREE_TYPES * 30) / 1024 / 1024) +
"MB (instead of " + ((TREES_TO_DRAW * 38) / 1024 / 1024) + "MB)");
}
private static int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}
}
Go
package main
import "fmt"
/**
* 享元工厂
*
*/
const (
// 男模衣着类型
MaleDressType = "mDress"
// 女模衣着类型
FemaleDressType = "fDress"
)
var (
dressFactorySingleInstance = &DressFactory{
dressMap: make(map[string]Dress),
}
)
type DressFactory struct {
dressMap map[string]Dress
}
func (d *DressFactory) getDressByType(dressType string) (Dress, error) {
if d.dressMap[dressType] != nil {
return d.dressMap[dressType], nil
}
if dressType == MaleDressType {
d.dressMap[dressType] = newMaleDress()
return d.dressMap[dressType], nil
}
if dressType == FemaleDressType {
d.dressMap[dressType] = newFemaleDress()
return d.dressMap[dressType], nil
}
return nil, fmt.Errorf("错误的衣着类型")
}
func getDressFactorySingleInstance() *DressFactory {
return dressFactorySingleInstance
}
/**
* 享元接口
*
*/
type Dress interface {
getColor() string
}
/**
* 具体享元对象
*
*/
type MaleDress struct {
color string
}
func (t *MaleDress) getColor() string {
return t.color
}
func newMaleDress() *MaleDress {
return &MaleDress{color: "blue"}
}
type FemaleDress struct {
color string
}
func (c *FemaleDress) getColor() string {
return c.color
}
func newFemaleDress() *FemaleDress {
return &FemaleDress{color: "red"}
}
/**
* 场景
*
*/
type Scene struct {
dress Dress
modelType string
stagelat int
stagelong int
}
func newScene(modelType, dressType string) *Scene {
dress, _ := getDressFactorySingleInstance().getDressByType(dressType)
return &Scene{
playerType: playerType,
dress: dress,
}
}
func (p *Scene) newLocation(stagelat, stagelong int) {
p.stagelat = stagelat
p.stagelong = stagelong
}
/**
* 表演
*
*/
type Perform struct {
males []*Scene
females []*Scene
}
func newPerform() *Perform {
return &Perform{
males: make([]*Scene, 1),
females: make([]*Scene, 1),
}
}
func (c *Perform) addMale(dressType string) {
scene := newScene("m", dressType)
c.males = append(c.males, scene)
return
}
func (c *Perform) addFemale(dressType string) {
scene := newScene("f", dressType)
c.females = append(c.females, scene)
return
}
/**
* 客户端
*
*/
func main() {
perform := newPerform()
perform.addMale(MaleDressType)
perform.addMale(MaleDressType)
perform.addMale(MaleDressType)
perform.addMale(MaleDressType)
perform.addFemale(FemaleDressType)
perform.addFemale(FemaleDressType)
perform.addFemale(FemaleDressType)
dressFactoryInstance := getDressFactorySingleInstance()
for dressType, dress := range dressFactoryInstance.dressMap {
fmt.Printf("DressColorType: %s\nDressColor: %s\n", dressType, dress.getColor())
}
}
适用场景
享元模式的应用场景不多,仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。
优缺点
享元模式的优点。
它可以极大地减少对象所消耗的内存大小,使得相同对象或相似对象在内存中只保存一份。
它的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
享元模式的缺点。
可能需要牺牲执行速度来换取内存,因为每次调用享元方法时都要重新计算部分上下文数据。
使得系统更加复杂,需要分离出内部状态和外部状态,使得程序的逻辑复杂化。
相关性
可以使用
享元模式
实现组合模式
树的共享叶节点以节省内存。享元模式
展示了如何生成大量的小型对象,而外观模式
则展示了如何用一个对象来代表整个子系统。如果能将对象的所有共享状态简化为一个享元对象,那么
享元模式
就和单例模式
类似了。
感谢支持
更多内容,请移步《超级个体》。