设计模式 | 结构型 | 享元模式

2023-02-25 19:10:25 浏览数 (1)

享元模式(Flyweight)

享元模式(Flyweight)

介绍

享元模式是一种结构型设计模式。

享元模式顾名思义就是被共享的单元,意图是复用对象节省内存

适用场景

  • 存在大量重复对象(重复状态)且没有足够的内存容量时使用享元模式。
  • ...

优缺点

优点:

  • 节省大量内存。

缺点:

  • 代码复杂度提升。
  • 如果对象有不同的情景数据(外在状态),调用者需要耗费时间来重新计算。

与其他模式的关系

  • 可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 如果能将对象的所有共享状态简化为一个享元对象,那么享元就和单例类似了。但这两个模式有两个根本性的不同。
    • 单例只会有一个单例实体。
    • 享元可以有多个实体,各实体的内在状态也可以不同。
    • 单例对象可以是可变的。
    • 享元对象是不可变的。

实现方式

  1. 将享元对象划分为2部分:
    • 内在状态(不变的部分)
    • 外在状态(改变的部分)
  2. 保留类中表示内在状态的成员变量,并将其设置为不可修改
  3. 客户端必须存储和计算外在状态(情景)的数值,因为只有这样才能调用享元对象的方法。
    • 为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。
  4. 可以创建工厂类来管理享元缓存池,如果使用了工厂,那么客户端就只能通过工厂来获取享元对象。

示例

享元对象:抽取出来的骰子类(只包含不变的内在属性)

代码语言:javascript复制
public class DiceType {

    // 内在属性:面数
    private int faceValue;
    private SecureRandom random = new SecureRandom();

    public DiceType(int faceValue) {
        this.faceValue = faceValue;
    }

    public int roll() {
        int i = random.nextInt(faceValue);
        return i   1;
    }
}

情景类:存储着外在状态的骰子类(包含变化的部分以及享元对象)

代码语言:javascript复制
public class Dice {

    // 外在属性:幸运数字
    private int luckyNumber;
    // 外在属性:不幸数字
    private int unluckyNumber;

    // 共享的骰子
    private DiceType diceType;

    // 这一次 roll 出来的数字
    private int rollNumber;
    // 这一次 roll 出来的幸运类型: 0普通 1幸运 -1不幸
    private int luckyType;

    public Dice(int luckyNumber, int unluckyNumber, DiceType diceType) {
        this.luckyNumber = luckyNumber;
        this.unluckyNumber = unluckyNumber;
        this.diceType = diceType;
    }

    // 掷骰子
    public int roll() {
        int roll = diceType.roll();
        rollNumber = roll;
        if (roll == luckyNumber) {
            System.out.println("投出了幸运数字:"   roll);
            luckyType = 1;
        } else if (roll == unluckyNumber) {
            System.out.println("投出了不幸数字:"   roll);
            luckyType = -1;
        } else {
            System.out.println("投出了数字:"   roll);
            luckyType = 0;
        }
        return roll;
    }

    // 获取幸运状态:0普通 1幸运 -1不幸
    public int getLuckyType() {
        return luckyType;
    }
}

享元缓存工厂

代码语言:javascript复制
public class DiceFactory {

    private static final Map<Integer, DiceType> diceTypeMap = new HashMap<>();

    /**
     * 获取享元对象:获取 N面骰子 实例
     */
    public static DiceType getDiceType(int faceValue) {
        DiceType result = diceTypeMap.get(faceValue);
        if (result == null) {
            DiceType diceType = new DiceType(faceValue);
            diceTypeMap.put(faceValue, diceType);
            result = diceType;
        }
        return result;
    }
}

测试代码

代码语言:javascript复制
public class FlyweightTest {

    @Test
    public void test() {

        // 获取享元对象:6面骰子
        DiceType diceType1 = DiceFactory.getDiceType(6);

        // 幸运6 不幸1 其他正常
        int luckyNumber = 6;
        int unluckyNumber = 1;

        Dice dice1 = new Dice(luckyNumber, unluckyNumber, diceType1);
        for (int i = 0; i < 100; i  ) {
            int rollNumber = dice1.roll();;
            int luckyType = dice1.getLuckyType();
            if (rollNumber==luckyNumber) {
                Assertions.assertEquals(1,luckyType);
            }else if(rollNumber==unluckyNumber){
                Assertions.assertEquals(-1,luckyType);
            }else{
                Assertions.assertEquals(0,luckyType);
            }
        }

        // 再次获取享元对象 ,一样是6面骰子
        DiceType diceType2 = DiceFactory.getDiceType(6);

        // 获取的对象应该是同一个
        Assertions.assertEquals(diceType1,diceType2);

        // 改一下外在状态
        luckyNumber = 3;
        unluckyNumber = 2;

        Dice dice2 = new Dice(luckyNumber, unluckyNumber, diceType2);
        for (int i = 0; i < 100; i  ) {
            int rollNumber = dice2.roll();;
            int luckyType = dice2.getLuckyType();
            if (rollNumber==luckyNumber) {
                Assertions.assertEquals(1,luckyType);
            }else if(rollNumber==unluckyNumber){
                Assertions.assertEquals(-1,luckyType);
            }else{
                Assertions.assertEquals(0,luckyType);
            }
        }
    }
}

实例

JDK

包装器类型

java.lang.Integer#valueOf(int)

Long、Short、Byte、Boolean、Character、BigDecimal 等。

代码语言:javascript复制
public final class Integer extends Number implements Comparable<Integer> {
    // ... 省略

    /**
     * 默认缓存 -128~127 
     * 可通过以下两者方法之一 改掉最大值127
     * -Djava.lang.Integer.IntegerCache.high=255
     * -XX:AutoBoxCacheMax=255
     */
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                    VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low)   1];
            int j = low;
            for(int k = 0; k < cache.length; k  )
                cache[k] = new Integer(j  );

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

    // 转为包装对象
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i   (-IntegerCache.low)];
        return new Integer(i);
    }
    // ... 省略
}
java.lang.String

String 类利用享元模式来复用相同的字符串常量。

JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。

以上代码与文章会同步到 github 仓库:

/chenbihao/Design-Patterns

0 人点赞