享元模式介绍

享元模式主要用于共享通用对象,减少内存的使用,提升系统的访问效率。较大的对象通常比较耗费内存,需要查询大量的接口或使用数据库资源,因此有必要统一抽离出来作为共享对象使用。

享元模式简单使用

使用场景:缓存优化查询场景

模拟商品秒杀场景中使用场景中享元模式优化查询。

针对不同商品信息的查询,不需要每次都从数据库中获取,因为除了商品库存,其他商品信息都是固定不变的,所以一般会缓存到Redis中,这里模拟使用享元模式搭建工厂结构,提供活动商品的查询服务。商品信息相当于不变的信息,商品库存相当于变化的信息。

商品信息:

1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Activity {
private Long id;
private String name;
private String desc;
private Date startTime;
private Date stopTime;
private Stock stock;
}

库存信息:

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Stock {
private int total;
private int used;
}

违背设计模式实现

就是简单的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ActivityController {

public Activity queryActivityInfo(Long id) {
// 模拟从实际业务应用从接口中获取活动信息
Activity activity = new Activity();
activity.setId(10001L);
activity.setName("图书嗨乐");
activity.setDesc("图书优惠券分享激励分享活动第二期");
activity.setStartTime(new Date());
activity.setStopTime(new Date());
activity.setStock(new Stock(1000,1));
return activity;
}
}

这里模拟的是接口中查询活动信息,基本是从数据库中获取所有的商品信息和商品库存。随着业务不断发展,我们可以将商品的库存交给Redis处理,需要从Redis中获取活动的商品库存,而不是从数据库中获取。

享元模式重构代码

其实在日常的开发当中,使用享元模式的情况并不多。享元模式的设计思想是减少内存的使用,与原型模式通过复制对象的方式生成复杂对象、减少RPC调用的思量类似。

享元工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ActivityFactory {
static Map<Long,Activity> activityMap = new HashMap<Long, Activity>();
public static Activity getActivity(Long id){
Activity activity = activityMap.get(id);
if (activity == null){
//模拟从数据库查询
activity = new Activity();
activity.setId(10001L);
activity.setName("iphone13");
activity.setDesc("坑你没商量");
activity.setStartTime(new Date());
activity.setStopTime(new Date());
activityMap.put(id,activity);
}
return activity;
}
}

这里提供的是一个享元工厂,通过Map结构存放已经从库表或接口中查询到的数据并存放到内存中,方便下次直接获取。这种结构在编程开发中是比较常见的,有时也为了保证分布式系统部署能获取到信息,会把数据存放到Redis中。

模拟Redis服务
1
2
3
4
5
6
7
8
9
10
11
12
public class RedisUtil {
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
private AtomicInteger stock = new AtomicInteger(0);
public RedisUtil(){
scheduledExecutorService.scheduleAtFixedRate(() -> {
stock.addAndGet(1);
},0,100000, TimeUnit.MICROSECONDS);
}
public int getStockUsed(){
return stock.get();
}
}

这里除了模拟Redis的操作工具类,还提供了一个定时任务,用于模拟库存消耗,这样可以在测试观察商品库存的变化。

活动控制类
1
2
3
4
5
6
7
8
9
public class ActivityController {
private RedisUtil redisUtil = new RedisUtil();
public Activity queryActivityInfo(Long id){
Activity activity = ActivityFactory.getActivity(id);
Stock stock = new Stock(1000, redisUtil.getStockUsed());
activity.setStock(stock);
return activity;
}
}

在活动控制类中使用了享元工厂获取活动信息,查询后将商品库存信息再补充到商品活动库存信息对应的库存属性中。因为商品库存信息是变化的,而商品活动信息是固定不变的。最终通过统一的控制类,就可以把完整包装后的商品活动信息返回给调用方。

总结

可以着重学习享元工厂的实现方式,在一些有大量重复对象可复用的场景中,在服务端减少接口的调用,在客户端减少内存的占用,是享元模式的主要特点。另外,通过Map结构,使用一个固定ID存放和获取对象是非常关键的。不只是在享元模式中,在工厂模式、适配器模式和组合模式中都可以通过Map结构存放服务,供外部获取,减少if…else的判断使用。当然享元模式虽然可以减少内存的占用,但也有缺点:在一些复杂的业务处理场景中,不容易区分内部状态和外部状态。就想活动信息部分与库存变化部分如果不能很好地拆分,会把享元模式设计得非常混乱,难以维护。

评论