【设计模式】享元模式(Flyweight Pattern)

在这里插入图片描述

 

🔥 核心

享元模式通过共享多个对象所共有的相同状态,减少对象占用的内存。

享元模式的本质其实是缓存。

 

🙁 问题场景

你是一名游戏地图建模师,复杂搭建游戏地图。

现在,你需要搭建一座森林。森林中有成千上万个棵 树(Tree) ,没棵树具有的属性有 位置(x, y)高度(h)颜色(color)材质(texture) 。你做出了成千上万棵树的对象,搭建出了一座美丽而壮观的森林。

你迫不及待的把地图工程发送给你的朋友,让他体验一下这座森林。尽管地图工程在你的电脑上完美运行,但是,他的电脑在进入地图的一瞬间崩溃了。你连忙查看日志,发现崩溃的原因是「内存容量不足」。当然,一名普通游戏玩家的电脑,可比不上一名专业的游戏地图建模师。

你既不想缩减这座森林的一毫一木,又必须让地图工程能在有限内存中运行。怎么办呢?

 

🙂 解决方案

享元模式可能会比你想象中的复杂,所以请集中注意力。3.2.1,开始。

享元模式提出了两个概念:「内部状态」与「外部状态」,这是对成员变量的分类。

内部状态:相对固定的信息,它们并不能标识对象,存储在享元对象中。
外部状态:可以改变的信息,它们是每个对象独特的属性,存储在每个原对象中。

我知道这个概念非常抽象,难以理解。不妨动动手,对游戏地图中 树(Tree) 的成员变量进行分类:

位置(x, y):每个树对象特有的,是外部状态;
高度(h) :每个树对象特有的,是外部状态;
颜色(color):许多树木会具有相同的颜色,相对固定,是内部状态;
材质(texture):许多树木会具有相同的材质,相对固定,是内部状态。

因此,位置与高度(x, y, h) 作为成员变量依旧存储在每个对象中,颜色与材质(color, texture) 则封装到享元类中。

思考一下,你不想减少树对象的数量,那么只能从哪方面入手优化呢?每个对象的大小。现在的每个树对象,都拥有着这么多成员变量,内存能不大吗?而现在,我们把其中的部分成员变量封装到享元类中,就可以直接将原对象中的这几个成员变量替换为享元对象的引用,从而减小了每个对象占用的内存;从整体的角度看,有限个享元对象被成千上万个对象复用,使得享元对象的这部分数据无需重复存储。这就是享元模式优化内存的本质。

理解并消化一下。好了,我们继续引入又一个概念:「享元工厂」。

享元工厂就是一个管理享元对象的缓存池。如果能在缓存池中找到所需享元,则直接返回复用;如果没有找到,就新建一个享元,并将其加入缓存池中。

至此,享元模式已经介绍完毕,或者说,复合享元模式已经介绍完毕。下面接着介绍单纯享元模式。在此之前,我还是建议你读明白上面的内容。

好了,现在假设你已经基本理解了上面的内容。

单纯享元模式的思路是,不改变每个对象所占用的内存大小,而是减少对象的数量——内部状态相同的对象,其实是同一个对象的复用,只是这个对象的外部状态不断被更改罢了。

MOBA游戏对英雄地位进行了分类,如刺客、法师、辅助、战士等。现在,英雄工厂中已经有一个刺客英雄对象,你将其拿出来,对其属性进行了相应的修改,便得到了一个新刺客英雄。虽然新刺客英雄诞生了,但第一个刺客英雄却不复存在了。

因此,单纯享元模式所能应用的场景是比较局限的。比如上面的森林地图,你不能为了节省内存,在一个树对象的基础上修改属性得到另一个树对象,这种复用会使得你的森林中只有光秃秃的几棵树。

 

🌈 有趣的例子

因为上面这个“搭建森林”的例子有点儿复杂,所以我们干脆写出代码,帮助我们彻底理解享元模式。

在这里插入图片描述


class Tree {
    private int x;
    private int y;
    private int h;
    private Type type;

    public Tree(int x, int y, int h, Type type) {
        this.x = x;
        this.y = y;
        this.h = h;
        this.type = type;
    }

    public void plant() {
        System.out.println("种下一棵树~");
        System.out.println("坐标x: " + x);
        System.out.println("坐标y: " + y);
        System.out.println("高度: " + h);
        System.out.println("颜色: " + type.getColor());
        System.out.println("材质: " + type.getTextrue());
    }
}

 享元类
class Type {
    // 依次为绿色、黄色、褐色、黑色
    static final String[] COLORS = {"green", "yellow", "brown", "black"};
    // 依次为杉木、橡木、桦木、槐木
    static final String[] TEXTURES = {"fir", "oak", "birch", "locust"};

    private String color;
    private String textrue;

    public Type(String color, String textrue) {
        this.color = color;
        this.textrue = textrue;
    }

    public String getColor() { return color; }
    public String getTextrue() { return textrue; }
}

 享元工厂
class TypeFactory {
    private Map<String, Type> typeCache = new HashMap<>();

    public Type getType(String color, String textrue) {
        String key = color + textrue;
        if (!typeCache.containsKey(key)) {
            typeCache.put(key, new Type(color, textrue));
        }
        return typeCache.get(key);
    }
}


 森林
class Forest {
    public List<Tree> trees = new ArrayList<>();

    public void init() {
        TypeFactory typeFactory = new TypeFactory();
        // 搭建1000棵树
        // 每棵树具有随机的位置,随机的高度,随机的颜色,随机的材质
        for (int i = 0; i < 1000; i++) {
            Type type = typeFactory.getType(initColor(), initTexture());
            Tree tree = new Tree(initX(), initY(), initH(), type);
            tree.plant();
            trees.add(tree);
        }
    }

    private int initX() {
        return (int) (Math.random() * 10000);
    }

    private int initY() {
        return (int) (Math.random() * 10000);
    }

    private int initH() {
        return (int) (Math.random() * 10);
    }

    private String initColor() {
        return Type.COLORS[(int) (Math.random() * Type.COLORS.length)];
    }

    private String initTexture() {
        return Type.TEXTURES[(int) (Math.random() * Type.TEXTURES.length)];
    }
}
public class FlyweightPatternDemo {
    public static void main(String[] args) {
        // 初始化森林
        // 即种植树木
        new Forest().init();
    }
}
种下一棵树~
坐标x: 9198
坐标y: 8446
高度: 6
颜色: green
材质: oak

种下一棵树~
坐标x: 6862
坐标y: 9088
高度: 1
颜色: green
材质: birch
种下一棵树~

坐标x: 7010
坐标y: 6307
高度: 0
颜色: black
材质: locust

种下一棵树~
坐标x: 3470
坐标y: 9363
高度: 5
颜色: brown
材质: birch

种下一棵树~
坐标x: 1287
坐标y: 9743
高度: 2
颜色: black
材质: fir

种下一棵树~
坐标x: 5547
坐标y: 403
高度: 8
颜色: black
材质: oak

...1000 more

 

☘️ 使用场景

◾️仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。

应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:程序需要生成数量巨大的相似对象;这将耗尽目标设备的所有内存;对象中包含可抽取且能在多个对象间共享的重复状态。

 

🧊 实现方式

(1)将需要改写为享元的类成员变量拆分为两个部分:

  • 内在状态:包含不变的、可在许多对象中重复使用的数据的成员变量。
  • 外在状态:包含每个对象各自不同的情景数据的成员变量

(2)保留类中表示内在状态的成员变量,并将其属性设置为不可修改。这些变量仅可在构造函数中获得初始数值。

(3)找到所有使用外在状态成员变量的方法,为在方法中所用的每个成员变量新建一个参数,并使用该参数代替成员变量。

(4)你可以有选择地创建工厂类来管理享元缓存池,它负责在新建享元时检查已有的享元。如果选择使用工厂,客户端就只能通过工厂来请求享元,它们需要将享元的内在状态作为参数传递给工厂。

(5)客户端必须存储和计算外在状态(情景)的数值,因为只有这样才能调用享元对象的方法。为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。

 

🎲 优缺点

  ➕ 如果程序中有很多相似对象,那么你将可以节省大量内存。

  ➕ 需要缓冲池的场景。

  ➖ 你可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。

  ➖ 代码会变得更加复杂。团队中的新成员总是会问:“为什么要像这样拆分一个实体的状态?”。

 

🌸 补充

 复合享元模式/单纯享元模式是设计模式中的难点。

 所以请耐心理解与体会 >_<。

 

🔗 参考网站

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值