文章目录
- 1.概述
- 2.享元模式
- 2.1.核心概念
- 2.2.实现案例
- 2.2.1.内部状态实现
- 2.2.2.外部状态实现
- 2.3.更多场景
- 3.享元模式的一些对比
- 3.1.与缓存的区别
- 3.2.与池化技术的区别
- 4.总结
1.概述
享元模式(Flyweight Pattern)是一种非常常用的结构型设计模式,通过共享对象的方式,减少系统中的重复对象,提高内存使用效率。
2.享元模式
2.1.核心概念
先看一下设计模式中对享元模式的定义:
Use sharing to support large numbers of fine-grained objects efficiently
翻译过来就是享元模式突出对细粒度对象的共享,需要说明一下这里的细粒度对象。
在软件工程中,通常是指那些职责单一、功能细化的小型对象,也就是将大的实体或概念分解为多个小的、独立的对象。
在享元模式中,享元类一般有两种状态,分别是:
- 内部状态(Intrinsic State):不可变部分,通常是作为类的成员变量存储在享元类(Flyweight)的实例中,在创建享元对象时通过构造方法进行初始化,在整个生命周期内保持不变或由享元类自身管理。
- 外部状态(Extrinsic State):可变部分,不由享元对象直接维护,在方法调用时,客户端负责提供当前需要应用的外部状态信息。
注:享元模式中的外部状态并不是必须存在的。
2.2.实现案例
下面以扑克牌为例子来解释一下这两种状态,在常规的扑克牌游戏中,一共有4种花色和13种点数。除了花色与点数之外,扑克牌还有一些属性,例如:牌的大小和价值、牌在哪张牌桌上、在哪个玩家手上、是否在牌堆中,等等。
我们将扑克牌类创建为享元类,按照上述的定义方式,将属性拆解为不同的状态,其中:
- 内部状态:花色、点数,这部分属性恒定不变,可以由扑克牌类自行维护。
- 外部状态:牌桌号、玩家对象、牌的规则价值等,这部分属性会随着游戏的变化而变化,不由扑克牌类维护。
2.2.1.内部状态实现
首先看代码中是如何定义的内部状态的:
- 由于花色和点数是恒定的,此处先定义两个枚举:
@Getter public enum SuitsEnum { HEART("红桃"), SPADE("黑桃"), DIAMOND("方片"), CLUB("梅花"); private final String name; SuitsEnum(String name) { this.name = name; } } @Getter public enum PointEnum { THREE("3"), FOUR("4"), FIVE("5"), SIX("6"), SEVEN("7"), EIGHT("8"), NINE("9"), TEN("10"), J("J"), Q("Q"), K("K"), A("A"), TWO("2"); private final String name; PointEnum(String name) { this.name = name; } }
- 定义扑克牌的享元类,里面只有花色和点数两个属性:
/** * 扑克享元类 */ @Getter public class Poker { private SuitsEnum suitsEnum; private PointEnum pointEnum; public Poker(SuitsEnum suitsEnum, PointEnum pointEnum) { this.suitsEnum = suitsEnum; this.pointEnum = pointEnum; } }
- 最后我们定义一个扑克牌工厂,用于共享已生成的扑克牌对象
其实,所谓的共享,就是用一个数据结构将已生成的对象缓存起来的,数据结构可以是/** * 扑克享元工厂 */ public class PokerFactory { private static final Poker[][] pokers = new Poker[13][4]; static { init(); } public static void init() { for (int i = 0; i < 13; i++) { for (int j = 0; j < 4; j++) { pokers[i][j] = new Poker(SuitsEnum.values()[j], PointEnum.values()[i]); } } } public static Poker getPoker(int point, int suit) { return pokers[point][suit]; } /** * 创建牌堆 */ public static List<Poker> createPokers() { List<Poker> pokerList = new ArrayList<>(); for (int i = 0; i < 13; i++) { pokerList.addAll(Arrays.asList(pokers[i])); } return pokerList; } }
数组
,也可以是的Map
,List
等等,由于扑克牌的数量和花色、点数是恒定的,所以使用了一个二维数组存储并做了初始化,客户端可以通过点数+花色的方式来获取扑克对象。
2.2.2.外部状态实现
上面我们提到了,外部状态不由享元对象直接维护,说的更具体一点就是指那些与享元对象关联但不由该对象控制的信息,例如一个扑克牌游戏的玩家需要持有某张牌,需要经过发牌器发到玩家的手上,这里的发牌器对象与玩家对象都可以视为扑克牌对象的外部状态。
- 玩家类
public class Player { private String name; public Player(String name) { this.name = name; } private List<Poker> pokers = new ArrayList<>(); public void addPoker(Poker poker) { pokers.add(poker); } public void showPokers() { String msg = name + ":"; for (Poker poker : pokers) { msg += poker.getSuitsEnum().getName() + poker.getPointEnum().getName() + " "; } System.out.println(msg); } }
- 发牌器,假设当前是个炸金花的游戏,给每个玩家发三张牌
public class Shuffler { public static void deal(List<Player> playerList) { List<Poker> pokers = PokerFactory.createPokers(); // 打乱牌堆 Collections.shuffle(pokers); // 每人发3张牌 for (int i = 0; i < 3; i++) { for (Player player : playerList) { player.addPoker(pokers.remove(0)); } } } }
- 游戏服务
public class GameServer { public static void main(String[] args) { List<Player> list = Arrays.asList(new Player("张三"), new Player("李四"), new Player("王五")); Shuffler.deal(list); for (Player player : list) { player.showPokers(); } } }
执行之后的结果,很明显李四以一对J获得胜利。
张三:梅花3 梅花9 梅花6
李四:红桃4 梅花J 黑桃J
王五:黑桃5 红桃3 方片A
发牌器获取到牌堆时,扑克牌对象属于发牌器对象,而在发牌的过程中,某一些牌对象的关联关系由发牌器对象转移到了玩家对象。
通过上面的例子,可以感受到外部状态并不是在指某个具体的属性,而是享元对象与其他对象之间的关联关系,这部分关系随时可能发生变化。
2.3.更多场景
看到这里,如果熟悉工厂和单例模式的话就很容易发现,享元模式的这种实现方式其实就是工厂模式+单例模式的一种拓展实现,在之前的博客《SpringBoot优雅使用策略模式》中关于选择器的实现思路,结合Spring
的依赖注入,注入全局唯一的处理器,也可以看作是享元模式。
此外,有一道关于Integer的经典面试题,如下代码中,分别会打印出什么:
public static void main(String[] args) {
Integer i = 100;
Integer j = 100;
System.out.println(i == j);
Integer i1 = 300;
Integer j1 = 300;
System.out.println(i1 == j1);
}
分别打印出:
true
false
这是因为给Integer赋值的时候,会自动装箱,即Integer i = 100
等价于Integer i = Integer.valueOf(100);
在源码中:
这里有个IntegerCache
,默认会将-128到127
之间的值创建为Integer
对象,放入到池中,使用这个区间内的值获取到的是同一个Integer
对象,这也是享元模式的一种体现。
3.享元模式的一些对比
相信大家已经发现了,享元模式的实现方式与缓存、池化技术是高度类似的,那么它们之间有什么样的差别呢?
3.1.与缓存的区别
两者之间主要是使用目的上的区别,可以通过以下的判断方式做区分。
- 享元模式的存在主要是为了复用对象、减少内存的消耗。
- 缓存的主要目的是针对常用对象做更细粒度的存储,从而提高访问的效率,降低查询时间等。
3.2.与池化技术的区别
两者的使用目的似乎都是为了复用,是的,在一部分资料中确实是将享元模式与池化技术画等号的,但两者之间的复用还有一定的区别。
- 享元模式的复用,是让服务中的不同对象,都可以同时使用到享元对象,是一种共享的概念。
- 池化技术的复用,更多的是讲究重复使用,即在使用了一部分连接后,可以放回池中让其他对象可以获取到,而不是断开连接,让后面的对象重新做一次连接操作。
从上面的角度来讲,池化技术中的每一个重复使用的对象,同时只会让一个对象持有。例如下面这个简单的jdbc
连接池Demo,在getConnection
时会获取到连接并从池中移除,在release
时又会将之前获取到的链接重新放回到池子中。
public class CollectionPool {
private Vector<Connection> pool = new Vector<>();
private String driverClassName = "com.mysql.cj.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/pattern";
private String userName = "xxx";
private String password = "xxx";
private CollectionPool() {
try {
Class.forName(driverClassName);
for (int i = 0; i < 2; i++) {
Connection conn = DriverManager.getConnection(url, userName, password);
pool.add(conn);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static CollectionPool getInstance() {
return InnerClass.POOL;
}
public Connection getConnection() {
if (pool.size() > 0) {
Connection conn = pool.get(0);
pool.remove(conn);
return conn;
}
return null;
}
public synchronized void release(Connection conn) {
pool.add(conn);
}
private static class InnerClass {
private static CollectionPool POOL = new CollectionPool();
}
}
4.总结
本文主要讲了享元模式的概念、使用场景以及与其他技术的对比。
在使用方式上,与缓存、池化技术是高度类似的,都是创建好对象并存储起来,在后续想要使用的时候直接从存储的数据结构中获取,而不用重新创建。
它与缓存、池化技术之间的区别,更多的是在于使用目的上的区别,只要能判断出,当前的对象是在通过共享对象的方式,减少系统中的重复对象,提高内存使用效率,就可以判断这是一个享元模式的实现。