一般做业务开发,不太容易有大量使用设计模式的场景。这里总结一下在业务开发中使用较为频繁的设计模式。当然语言为Java,基于Spring框架。
1 享元模式(Flyweight Pattern)
如果系统中存在大量的重复对象,或者需要不断地创建和销毁大量的重复对象。这时就可以使用该模式,缓存这些重复的对象,达到共享的目的,从而大大减少内存占用。
享元模式一般有3个角色:
- Flyweight: 享元类(主要定义内部状态与外部状态)
- FlyweightFactory: 享元工厂类(主要用于创建和管理享元类)
1.1 实际业务场景
这是由实际生产事故引发,反向推动使用的设计模式。因为做营销相关,很多用户留资页(落地页)都会使用到国家地区及电话国家码。作为基础数据,我们这边并没有做存储,甚至当时连缓存也没做。完全由基础服务组提供相关Feign
接口调用,说白了我们就是个中间商。唯一做的就是要根据用户IP来识别当前国家地区、以及存储Top15热点国家地区数据。
这一份数据,我记得当时统计的有30KB
左右。也就是说每个请求,都会在堆中创建这么一个对象,紧接着就成为了垃圾对象。当时是在做一个问卷调查,会给所有APP用户推送。服务器最开始只有一台无响应,紧接着又第二台,运维边重启机器边在边在告警群通知。
经排查,并结合最近上线内容,及接口请求监控数据。认定是由于国家地区接口访问量突增引起的。但具体原因及解决方案还需要看具体代码逻辑才能确定。
介绍完业务背景,咱们来说下大概的数据结构。
地区元数据
{
"code": "US",
"nameZh": "美国",
"nameEn": "United States",
"tel": "+1",
"pyName": "mg",
"sortNo": 1,
"areaId": null
}
整体结构
{
"allCountry": [],
"commonCountry": [],
"currentCountry": {}
}
在这份数据中,很容易看到。allCountry
基本不会改变,可以认为是内部状态,当然这也是占用空间最大的地方。commonCountry
在一定时间内基本不会改变,但我们在这把他作为一个外部状态处理(内部实现可通过缓存或其他方式实现)。currentCountry
需要根据请求IP来动态生成,这个作为外部变量处理。至此,整体思路已清晰。
1.2 代码实现
国家信息 POJO
@Data
public class Country {
private String code;
private String nameZh;
private String nameEn;
private String tel;
private String pyName;
private String sortNo;
private Integer areaId;
}
享元类(内部状态、外部状态)
@Getter
public class CountryList {
// 内部状态
private final List allCountry;
// 外部状态
private List commonCountry;
private Country currentCountry;
// 内部状态,创建对象时设置
public CountryList(List allCountry) {
this.allCountry = allCountry;
}
public void setCommonCountry(List commonCountry) {
this.commonCountry = commonCountry;
}
public void setCurrentCountry(Country currentCountry) {
this.currentCountry = currentCountry;
}
}
享元工厂类
@Slf4j
@Component
@RequiredArgsConstructor
public class CountryListFactory {
private static final String DEFAULT = "DEFAULT";
private static final Map CL_MAP = Maps.newHashMap();
private final CountryClient countryClient;
@PostConstruct
public void init() {
final List countryList = countryClient.getCountryList();
CL_MAP.put(DEFAULT, new CountryList(countryList));
}
public static CountryList getDefaultCl() {
return CL_MAP.get(DEFAULT);
}
}
因为本例的享元类中,内部状态只有一种。所以增加了DEFAULT
key值,实际也就这一种。
在这里有没有发现什么???如果没有这个DEFAULT
,这不就是个单例模式吗???
这块之所以这样写,主要为了更好的理解享元模式本身。
CountryClient
为模拟外部Feign接口服务,提供全量国家数据获取服务。并通过@PostConstruct
将数据注入到静态字典中。通过该工厂类,就可以拿到CountryList
,在根据需要将外部状态
注入该类,就可以使用了。
1.4 单测
@SpringBootTest
class CountryListTest {
@Test
void testFactory() {
final CountryList cl = CountryListFactory.getDefaultCl();
assertNotNull(cl);
}
}
2 思考
跟单例模式有啥区别?
- 单例主要在于
单
,即全局仅有一个实例;而享元主要在于享
,即共享。 - 在一定程度上,可以认为单例是享元的一种特殊情况。
- 享元更主要的是通过共享,达到节约内存的目的。
跟缓存有些类似?
- 缓存,主要目的是为了加快访问速度。享元,在于数据共享节约空间为目的。
- 缓存是一种思想,可以通过享元模式来实现缓存。
其他的一些思考?
享元类似单例,这就可能会涉及到线程安全问题?
- 主要在于看问题的角度,并不冲突。涉及到并发,要考虑线程安全问题。
不通过享元模式,也能达到相同的效果?
- 设计模式是对特定(某些场景)通用的解决方案,变通也是很重要的。先有问题才有解决方案,而不是先有设计模式才有特定场景。
封面图来源:https://refactoring.guru/desi...
echo '5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp' | base64 -d