今天要跟大家聊的是享元模式(Flyweight Pattern),顾名思义享元就是共享元信息。为什么要共享元信息?有两个原因,第一是创建元信息的过程可能比较耗时、耗费资源;第二是元信息内容重复率较高,每次都创建新的内容一致的资源并没有意义。
在这种情况下,就需要用到享元模式,将那些相似甚至相同的对象提取出来进行共享,这样下次再用到的时候只需要从共享池中进行获取就可以了。
是不是很熟悉?没错,单例的 Spring Bean 也是应用了这种思想!
1. 定义
享元模式是一种结构型设计模式,它的定义是:“运用共享技术来有效地支持大量细粒度对象的复用。”
由此可见,享元模式是通过共享已创建的大量相似或相同小对象来减少后续这些小对象的创建过程,避免无意义的对象创建动作,从而提高系统资源的利用率。
2. 类图
在享元模式中,有这样几种角色:
- 抽象享元角色(Flyweight):被共享的元信息的抽象,描述了一批相似或相同对象所具备的基础信息,一般是一个接口
- 具体享元角色(ConcreteFlyweight):实现抽象享元角接口的具体享元对象
- 享元工厂(FlyweightFactory):构造一个享元池,保存共享的元信息,同时提供从池中获取对象的方法
- 非享元角色(UnsharedFlyweight):具体业务实际使用到的角色,它聚合的享元角色将从享元工厂中获取
享元模式的类图如下:
3. 示例
关于享元模式的最佳实践我将以 C 端会员个人中心页面进行演示。
上图是淘宝 APP 的“我的淘宝”页面,页面上的重要内容可以概括为以下信息:
- 会员名(一旦设定不可更改)、昵称等基础信息
- 我的订单-各个状态订单数信息
在这三类信息中,会员名由于其不可更改的特殊性,于是就变成了享元角色,头像昵称等基础信息由于其变更频率较低,也可以使用享元角色来描述。另外订单数信息就是非享元角色,将从数据库中获取。
下面是这两类信息的定义,代码如下:
public class BaseInfo {
// 会员名,不可更改,将作为享元工厂的 key
private String memberName;
// 昵称
private String memberNickname;
public BaseInfo(String memberName, String memberNickname) {
this.memberName = memberName;
this.memberNickname = memberNickname;
}
// 省略 setter/getter 方法
}
public class OrderInfo {
// 待付款订单数
private Integer unPaidOrderCount;
// 待发货订单数
private Integer unDeliverOrderCount;
// 待收货订单数
private Integer unReceiveOrderCount;
// 待评价订单数
private Integer unEvaluateOrderCount;
// 省略 setter/getter 方法
}
public class Member {
// 会员基础信息-享元角色
private BaseInfo baseInfo;
// 会员订单数-非享元角色
private OrderInfo orderInfo;
// 省略 setter/getter 方法
}
然后定义享元工厂,其代码如下:
public class BaseInfoFactory {
private Map<String, BaseInfo> baseInfoMap = new HashMap<>();
public void add(String key, BaseInfo value) {
baseInfoMap.put(key, value);
}
public BaseInfo getBaseInfo(String key) {
return this.baseInfoMap.get(key);
}
}
然后创建模拟与数据库交互,用于获取会员订单数量的持久层类,其代码如下:
public class OrderInfoRepository {
public OrderInfo getOrderInfoByMember(String memberName) {
System.out.println("从数据库中获取用户[" + memberName + "]的各状态订单数量");
return new OrderInfo(0, 0, 0, 0);
}
}
然后在客户端首先定义两个用户大A和小G,向享元工厂注册这两个享元角色,然后获取大A会员的数据,其代码如下:
public class Test {
public static void main(String[] args) {
BaseInfo a = new BaseInfo("A", "大A");
BaseInfo g = new BaseInfo("G", "小G");
// 向享元工厂中注册享元角色
BaseInfoFactory baseInfoFactory = new BaseInfoFactory();
baseInfoFactory.add(a.getMemberName(), a);
baseInfoFactory.add(g.getMemberName(), g);
OrderInfoRepository orderInfoRepository = new OrderInfoRepository();
// 获取用户大A的信息
Member member = new Member();
BaseInfo aBaseInfo = baseInfoFactory.getBaseInfo("A");
OrderInfo aOrderInfo = orderInfoRepository.getOrderInfoByMember("A");
member.setBaseInfo(aBaseInfo);
member.setOrderInfo(aOrderInfo);
System.out.println(member);
}
}
输出结果为:
从数据库中获取用户[A]的各状态订单数量
会员{基础信息={会员名='A', 昵称='大A'}, 订单信息={待付款订单数=0, 待发货订单数=0, 待收货订单数=0, 待评价订单数=0}}
可以看到,会员对象有两部分组成——基础信息与订单信息,基础信息基本不会发生变化,是缓存在享元工厂中的。而订单信息由于变更频率较高,是根据会员名从数据库中获取的。
4. 使用场景
享元模式的使用场景是:
- 系统中存在大量相似或相同的对象,且这些对象的创建会耗费大量资源
- 对象通常是无状态且不会轻易改变的
其他使用到享元模式的场景有:JDK 包装类的缓存池(如 Integer默认将缓存 -128~127对象)、作用域为单例的 Spring Bean 将被缓存到 BeanFactory 对象工厂中(单享元模式)等。
5. 小结
本文讲述了享元模式,它的定义是——运用共享技术来有效地支持大量细粒度对象的复用。
享元模式的优点是:
- 能大量减少对象的创建过程,节省系统资源,提高性能
享元模式的缺点是:
- 提高了系统复杂性,需要将内部状态(享元角色)和外部状态(非享元角色)剥离开来
6. 参考资料
- 《设计模式之禅》(第2版)
最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。
文档信息
- 本文作者:Planeswalker23
- 本文链接:https://planeswalker23.github.io/2022/02/04/%E6%88%91%E6%89%80%E7%90%86%E8%A7%A3%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E7%B3%BB%E5%88%97-%E7%AC%AC11%E7%AF%87-%E5%9F%BA%E4%BA%8E%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F%E8%8E%B7%E5%8F%96%E4%BC%9A%E5%91%98%E4%B8%AA%E4%BA%BA%E4%B8%AD%E5%BF%83%E6%95%B0%E6%8D%AE/
- 版权声明:本作品系原创,作者保留所有权利,未经作者允许,禁止转载和演绎。