把书读薄 | 《设计模式之美》设计模式与范式(结构型-享元模式)

0x0、引言

?提测了等改BUG,摸鱼时啃下《设计模式之美》,本文对应设计模式与范式:结构型(54-55),享元模式 (Flyweight Pattern),结构型设计模式最后一个~

  • 很好理解 → 享元,被共享的单元
  • 意图 → 节省内存,复用不可变对象
  • 换句话说 → 找出相似对象间的共有特征,然后复用这些特征

一个形象的例子:游戏开发中,一个森林场景,成千上万种树,每棵树都实例化不同的模型,内存直接爆炸,可以抽取出所有树对象的共有属性,转移到一个单独的类中,然后只需要一个类实例,然后森林里的每一颗树对这个实例做一次引用(图摘自:Flyweight):

Tips:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。


0x1、写个简单的扑克牌例子

如果让你实现一个简单的扑克牌程序,代码可能是这样的(此处假定没有王,只有52张牌,4种花色):

// 牌类,有花色和大小两个属性
public class Card {
    private String color;
    private String num;

    public Card(String color, String num) {
        this.color = color;
        this.num = num;
    }

    public String printMsg() { return "扑克牌【" + color + num +"】"; }
}

// 测试用例
public class Player {
    public static void main(String[] args) {
        String[] colors = new String[]{"黑桃", "红心", "梅花", "方块"};
        List<Card> cards = new ArrayList<>();
        for (String color : colors) {
            for (int i = 1; i <= 13; i++) {
                switch (i) {
                    case 11: cards.add(new Card(color, "J")); break;
                    case 12: cards.add(new Card(color, "Q")); break;
                    case 13: cards.add(new Card(color, "K")); break;
                    default: cards.add(new Card(color, i + "")); break;
                }
            }
        }
        System.out.println("扑克牌初始完毕,共" + cards.size() + "张");
        System.out.println("随机派五张牌:");
        for (int i = 0; i < 5; i++) System.out.println(cards.get((int) (Math.random() * 52)).printMsg());
    }
}
复制代码

运行输出结果如下

正常输出,但却初始化了52个Card对象,?如果用享元模式需要创建几个呢?

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

// 享元类(抽象类或接口)
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;
            }
        }
    }
}
复制代码

运行结果输出如下

使用享元模式只创建了4个卡牌对象,提提内部状态和外部状态的概念:

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

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

顺带带出UML类图和组成角色:

  • Flyweight (享元类) → 抽象类或接口,定义享元对象要实现的公共操作方法,方法会传入外部状态参数;
  • ConcreteFlyweight (具体享元类) → 实现享元接口,并为内部状态添加存储空间;
  • FlyweightFactory (享元工厂) → 创建并管理共享的享元对象,对外提供访问接口;

享元模式的本质

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

缺点

时间换空间,对于需要快速响应的系统并不合适,需分离出内部和外部状态,难统一,增加了系统设计实现的复杂度;


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

从代码实现上看,享元模式和前面学的 单例中的多例 很相似,但从设计意图上看,是完全不同的。

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

在来看看与缓存的区别:

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

还有对象池:

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

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


0x3、享元模式在Java Integer、String中的应用

先说下Integer的,下面的代码:

Integer i1 = 12;
Integer i2 = 12;
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i1 == i2);   // 输出:true
System.out.println(i3 == i4);   // 输出: false
复制代码

学过Java的都知道:**双等号(==)**区别对待两类数据类型:

  • 基本数据类型(byte、char、int等) → 比较值是否相同;
  • 引用数据类型(对象实例) → 比较变量指向的对象内存地址是否相同;

既然i1和i2相等,说明指向同一对象内存地址,存在对象重用,按理来说,i3和i4应该也相等啊?且听我娓娓道来:

执行到这样的语句:Integer i1 = 12,12是基本数据类型int,赋值给包装器类型Integer时,会触发自动装箱机制,底层相当于执行了这样的语句:

// 创建Integer类型的实例,并赋值
Integer i1 = Integer.valueOf(12);

// 附:自动拆箱 int j = i1 底层执行的是:
int j = i1.intValue();
复制代码

行,那就来跟下 valueOf() 方法都做了啥:

判断是否大于low小于high,是返回缓存中的Integer实例,否则返回新的Integer实例,看下 IntegerCache

呕吼,当IntegerCache类被加载是,初始化了值在 -128-127 间的Integer实例,并加入到缓存数组中。而只缓存这个范围的原因:

不可能预先缓存好所有的整型值,只能选择对于大部分应用来说或最常用的整形值,即一个字节的大小。

最大值也可以通过修改JVM配置进行修改 (二选一):

-Djava.lang.Integer.IntegerCache.high=255
-XX:AutoBoxCacheMax=255
复制代码

这就是上面的代码输出true和false的原因了(-128-127范围内复用,不在范围外的直接new),这里的IntegerCache就是享元模式中的享元工厂。

再接着看下String,下面这样的代码:

String s1 = "杰哥";
String s2 = "杰哥";
String s3 = "杰" + "哥";
String s4 = new String("杰哥");
System.out.println(s1 == s2);   // 输出: true
System.out.println(s1 == s3);   // 输出: true
System.out.println(s1 == s4);   // 输出: false
复制代码

和Integer类的设计思想相似,JVM会专门开辟一块存储区来存储字符串常量(字符串常量池),跟Integer不同,无法事先知道要共享哪些字符串常量,只能第一次用到的时候存储到常量池中,后续用到直接引用,就无需重新创建了。解释下上述代码:

  • s1 == s2 → 编译时,”杰哥”被存储在常量池中,s1和s2的引用都指向常量池里的”杰哥”;
  • s1 == s3 → 编译器优化,先把字符串拼接,再去常量池里查找字符串是否存在,存在让s3直接指向该字符串;
  • s1 != s4 → 不是显式赋值,编译器会在堆中重新分配一个区域来存储它的对象数据;

以上就是本节的全部内容,下节开始啃11种行为型设计模式,谢谢~


© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享