结构型模式包括:适配器、桥接、组合、装饰器、外观、享元、代理

这类模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。

第六节、享元模式

一、享元模式介绍

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

另外享元模式可以分为在服务端和客户端,⼀般互联⽹H5和Web场景下⼤部分数据都需要服务端进⾏处理,⽐如数据库连接池的使⽤、多线程线程池的使⽤,除了这些功能外,还有些需要服务端进⾏包装后的处理下发给客户端,因为服务端需要做享元处理。但在⼀些游戏场景下,很多都是客户端需要进⾏渲染地图效果,⽐如;树⽊、花草、⻥⾍,通过设置不同元素描述使⽤享元公⽤对象,减少内存的占⽤,让客户端的游戏更加流畅。

在享元模型的实现中需要使用到享元工厂来进行管理这部分独立的对象和共享的对象,避免出现线程安全问题。

二、案例场景模拟

在这个案例中我们模拟在商品秒杀场景下使用享元模式查询优化

商品下单的项目从日均十几单到某个时段秒杀量破十万的项目。需要使用redis的分布式锁来控制商品库存。

同时在查询的时候也不需要每一次对不同的活动查询都从库中获取,因为出了库存,别的活动商品信息是不变的,所以可以存储到内存当中。

这里我们模拟使用享元模式工厂结构,提供活动商品的查询。活动商品相当于不变的信息,而库存部分属于变化的信息。

三、直接实现

这一部分逻辑的查询常见为先查询固定信息,再使用过滤的或者添加if判断的方式来补充变化的信息,也就是库存。

1.工程结构

2.代码实现

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;
}
}
  1. 这里模拟的是从接口中查询活动信息,基本也就是从数据库中获取所有的商品信息和库存。
  2. 当后续因为业务的发展需要扩展代码将库存部分交给redis处理,那么就需要从redis中获取活动的库存,而不是从库中,否则将造成数据不统一问题。

四、享元模式重构代码

享元模式适配场景:线程池、数据库连接池。这个设计模式的核心思想就是减少内存的使用提升效率,与之前使用的原型模式思想类似。

1.工程结构

享元模式模型结构

  1. 以上是我们模拟查询活动场景的类图结构,左侧构建的是享元工厂,提供固定活动数据的查询,右侧是Redis存放的库存数据。
  2. 最终交给活动控制类来处理查询操作,并提供活动的所有信息和库存。因为库存是变化的,所以我们模拟的RedisUtils中设置了定时任务使用库存。

2.代码实现

2.1活动信息
public class Activity {
private Long id; // 活动ID
private String name; // 活动名称
private String desc; // 活动描述
private Date startTime; // 开始时间
private Date stopTime; // 结束时间
private Stock stock; // 活动库存

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

public Date getStartTime() {
return startTime;
}

public void setStartTime(Date startTime) {
this.startTime = startTime;
}

public Date getStopTime() {
return stopTime;
}

public void setStopTime(Date stopTime) {
this.stopTime = stopTime;
}

public Stock getStock() {
return stock;
}

public void setStock(Stock stock) {
this.stock = stock;
}
}

此对象类只是一个活动的基础信息:id、名称、描述、时间和库存。

2.2库存信息
public class Stock {

private int total; // 库存总量
private int used; // 库存已用

public Stock(int total, int used) {
this.total = total;
this.used = used;
}

public int getTotal() {
return total;
}

public void setTotal(int total) {
this.total = total;
}

public int getUsed() {
return used;
}

public void setUsed(int used) {
this.used = used;
}
}

这里是库存数据,我们单独提供一个类进行保存数据

2.3享元工厂
public class ActivityFactory {
static Map<Long, Activity> activityMap = new HashMap<>();
public static Activity getActivity(Long id){
Activity activity = activityMap.get(id);
if (null == activity){
// 模拟从实际业务引用从接口中获取活动信息
activity = new Activity();
activity.setId(10001L);
activity.setName("手机抢购");
activity.setDesc("狂撸手机促销大活动第二期");
activity.setStartTime(new Date());
activity.setStopTime(new Date());
activityMap.put(id, activity);
}
return activity;
}
}

这里提供的是一个享元工厂,通过map结构存放已经从库表或者接口中查询到的数据,存放到内存中,用于下次可以直接获取。

2.4模拟Redis类
public class RedisUtils {
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
private AtomicInteger stock = new AtomicInteger(0);
public RedisUtils(){
scheduledExecutorService.scheduleAtFixedRate(() -> {
// 模拟库存消耗
stock.addAndGet(1);
}, 0, 100000, TimeUnit.MICROSECONDS);

}
public int getStockUsed(){
return stock.get();
}
}

这里除了模拟redis的操作工具类外,还提供了一个定时任务用于模拟库存的使用,这样我们在测试的时候可以观察到库存的变化

2.5活动控制类
public class ActivityController {
private RedisUtils redisUtils = new RedisUtils();
public Activity queryActivityInfo(Long id) {
Activity activity = ActivityFactory.getActivity(id);
// 模拟从Redis中获取库存变化信息
Stock stock = new Stock(1000, redisUtils.getStockUsed());
activity.setStock(stock);
return activity;
}
}
  1. 在活动控制类中使用了享元工厂获取活动信息,查询后将库存信息再补上,因为库存信息是变化的,活动信息是固定不变的。
  2. 最终通过统一的控制类就可以把完整包装后的活动信息返回给调用方。

3.测试验证

3.1编写测试类
public class ApiTest {

private Logger logger = LoggerFactory.getLogger(ApiTest.class);

private ActivityController activityController = new ActivityController();

@Test
public void test_queryActivityInfo() throws InterruptedException {
for (int idx = 0; idx < 10; idx++) {
Long req = 10001L;
Activity activity = activityController.queryActivityInfo(req);
logger.info("测试结果:{} {}", req, JSON.toJSONString(activity));
Thread.sleep(1200);
}
}

}

这里我们通过活动查询控制类,在for循环的操作下查询了十次活动信息,同时为了保证库存定时任务的变化,加了睡眠操作,实际开发无睡眠。

3.2测试结果
14:28:48.227 [main] INFO  org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":1},"stopTime":1696832928166}
14:28:49.439 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":13},"stopTime":1696832928166}
14:28:50.649 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":25},"stopTime":1696832928166}
14:28:51.858 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":37},"stopTime":1696832928166}
14:28:53.058 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":49},"stopTime":1696832928166}
14:28:54.272 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":61},"stopTime":1696832928166}
14:28:55.472 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":73},"stopTime":1696832928166}
14:28:56.686 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":86},"stopTime":1696832928166}
14:28:57.888 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":98},"stopTime":1696832928166}
14:28:59.100 [main] INFO org.levtio.demo.design.test.ApiTest - 测试结果:10001 {"desc":"狂撸手机促销大活动第二期","id":10001,"name":"手机抢购","startTime":1696832928166,"stock":{"total":1000,"used":110},"stopTime":1696832928166}

进程已结束,退出代码为 0

从测试结果来看,stock部分的库存信息是在一直变化的,其他部分是活动信息,是固定的,所以我们使用享元模式将这样的结构进行拆分。

五、总结

  1. 关于享元模式的设计可以着重学习享元工厂的设计,在一些有大量重复对象可复用的场景下,使用此场景在服务端减少接口的调用,在客户端减少内存的占用。
  2. 另外通过map的使用方式可以看到,使用一个固定id来存放和获取对象,是非常关键的点。而且不只是在享元模式中使用,一些其他工厂模式、适配器模式、组合模式中都可以通过map结构存放服务供外部获取,减少ifelse的判断使用。
  3. 优点是减少内存的使用,缺点是在一些复杂的业务处理场景中,不容易区分出内部状态和外部状态。如果不能很好的拆分,就会把享元工厂设计的非常混乱,难以维护。