享元模式就是找到被共享的单元。一般来说就是复用不可变对象,减少创建对象所需要的内存。

1 解决的问题

节省内存,复用不可变对象。《超级玛丽》更改颜色、增加装饰后就变成了不同的对象,复用操作使代码很小。

将对象分为不变部分+改变部分。不变部分,可以是一个单例对象,也可以是一个常量池。

与单例模式不同在于,单例模式可以是个可变对象,享元一定不是个可变对象。

享元模式的经典应用就是 Integer、Long、字符串常量池。

2 享元模式

享元模式,就是被共享的单元;找出相似对象间的共有特征,然后复用这些特征。享元模式存在 内部状态外部状态 两个概念。

  • 内部状态固定不变可共享 的部分,存储在享元对象内部,比如这里的花色;
  • 外部状态可变不可共享 的部分,一般由客户端传入享元对象内部,比如这里的大小;

  1. 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为 “内在状态”。传递给享元方法的状态被称为 “外在状态”。
  2. 情景 (Context) 类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
  3. 通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
  4. 客户端 (Client) 负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
  5. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。

举个例子,扑克牌享元工厂包含黑桃扑克牌梅花扑克牌方片扑克牌红桃扑克牌共四种享元。在这四种享元基础上,给定扑克牌的数字,就可以生成一张扑克牌。例如,给黑桃扑克牌传入数字 ‘1’,就可以得到黑桃 1.

3 实现代码

抽取下扑克牌的共有属性:花色和大小,花色固定四种,大小变化,写一个卡牌的父类,写四个花色子类继承:

// 享元类(抽象类或接口)
abstract class AbstractCard {
    // 共享对象需实现的公共操作方法,使用一个外部状态作为输入参数(客户端保存,运行时改变)
    abstract String printMsg(String num);
}
 
// 具体享元类
class SpadeCard extends AbstractCard {
    @Override String printMsg(String num) { return "黑桃" + num; }
}
 
class HeartCard extends AbstractCard {
    @Override String printMsg(String num) { return "红心" + num; }
}
 
class SpadeCard extends AbstractCard {
    @Override String printMsg(String num) { return "黑桃" + num; }
}
 
class DiamondCard extends AbstractCard {
    @Override String printMsg(String num) { return "方块" + num; }
}
 
// 享元工厂
public class PokerFactory {
    public static final int SPADE = 0;    // 黑桃
    public static final int HEART = 1;    // 红心
    public static final int CLUB = 2;     // 梅花
    public static final int DIAMOND = 3;  // 方块
    public static Map<Integer, AbstractCard> pokers = new HashMap<>();
 
    public static AbstractCard getPoker(int color) {
        // 直接拿,不用再调一次containsKey
        AbstractCard card = pokers.get(color);
        if(card == null) {
            System.out.println("花色对象不存在,新建对象...");
            switch (color) {
                case SPADE: card = new SpadeCard(); break;
                case HEART: card = new HeartCard(); break;
                case CLUB: card = new ClubCard(); break;
                default: card = new DiamondCard(); break;
            }
            pokers.put(color, card);
        } else {
            System.out.println("花色对象已存在,复用对象...");
        }
        return card;
    }
}
 
// 测试用例
public class Player {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            AbstractCard card;
            // 随机花色
            switch ((int) (Math.random() * 4)) {
                case 0: card = PokerFactory.getPoker(PokerFactory.SPADE); break;
                case 1: card = PokerFactory.getPoker(PokerFactory.HEART); break;
                case 2: card = PokerFactory.getPoker(PokerFactory.CLUB); break;
                default: card = PokerFactory.getPoker(PokerFactory.DIAMOND); break;
            }
            // 随机大小
            int num = (int)(Math.random() * 13 + 1);
            switch (num) {
                case 11: System.out.println(card.printMsg("J")); break;
                case 12: System.out.println(card.printMsg("Q")); break;
                case 13: System.out.println(card.printMsg("K")); break;
                default: System.out.println(card.printMsg(num + "")); break;
            }
        }
    }
}

状态的区分也不是绝对的,要看场景,比如扩展到斗地主的对局,内部状态就变成了 54 张牌 (怎么发都不会超过 54 张),外部状态变成了牌的持有人。扩展到象棋棋局,内部状态 (颜色、文字),外部状态 (位置信息等)。

4 优缺点

优点

  • 通过创建更多的可复用对象的共有特征,来尽可能减少创建重复对象的内存消耗。

缺点

  • 时间换空间,每次调用都需要重新计算部分情景数据,对于需要快速响应的系统并不合适;
  • 需分离出内部和外部状态,难统一,增加了系统设计实现的复杂度;
  • 团队新成员不了解。

5 享元模式 VS 多例、缓存、对象池

多例:

多例是为了限制对象的个数,享元模式则是为了对象复用,节省内存。

与缓存的区别:

享元模式强调的是空间效率(大数据模型对象复用),而缓存模式强调的是 时间效率(如缓存秒杀的活动数据和库存数据等,数据可能会占用大量空间,目的是为了及时响应)。

还有对象池:

为了避免对象频繁创建和释放导致内存碎片,预先申请一块连续的内存空间,每次创建对象时,直接从对象池里取出一个空闲对象来使用,使用完再重新放回对象池中以后后续使用,而非直接释放。

池化技术里的复用,可以理解为重复使用,主要目的是节省时间,任意时刻,每个对象都是被一个使用者独占。 而享元模式的复用,可以理解为共享使用,主要目的是节省空间,在整个生命周期中,都是被所有使用者共享