写点什么

工作中常用的设计模式 -- 享元模式

作者:lpe234
  • 2022-12-14
    北京
  • 本文字数:2194 字

    阅读完需:约 7 分钟

工作中常用的设计模式--享元模式

一般做业务开发,不太容易有大量使用设计模式的场景。这里总结一下在业务开发中使用较为频繁的设计模式。当然语言为 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


@Datapublic class Country {    private String code;    private String nameZh;    private String nameEn;    private String tel;    private String pyName;    private String sortNo;    private Integer areaId;}
复制代码


享元类(内部状态、外部状态)


@Getterpublic class CountryList {  // 内部状态    private final List<Country> allCountry;  // 外部状态    private List<Country> commonCountry;    private Country currentCountry;
// 内部状态,创建对象时设置 public CountryList(List<Country> allCountry) { this.allCountry = allCountry; } public void setCommonCountry(List<Country> commonCountry) { this.commonCountry = commonCountry; }
public void setCurrentCountry(Country currentCountry) { this.currentCountry = currentCountry; }}
复制代码


享元工厂类


@Slf4j@Component@RequiredArgsConstructorpublic class CountryListFactory {    private static final String DEFAULT = "DEFAULT";    private static final Map<String, CountryList> CL_MAP = Maps.newHashMap();
private final CountryClient countryClient;
@PostConstruct public void init() { final List<Country> countryList = countryClient.getCountryList(); CL_MAP.put(DEFAULT, new CountryList(countryList)); }
public static CountryList getDefaultCl() { return CL_MAP.get(DEFAULT); }}
复制代码


因为本例的享元类中,内部状态只有一种。所以增加了DEFAULTkey 值,实际也就这一种。在这里有没有发现什么???如果没有这个DEFAULT,这不就是个单例模式吗???这块之所以这样写,主要为了更好的理解享元模式本身。


CountryClient为模拟外部 Feign 接口服务,提供全量国家数据获取服务。并通过@PostConstruct将数据注入到静态字典中。通过该工厂类,就可以拿到CountryList,在根据需要将外部状态注入该类,就可以使用了。

1.4 单测

@SpringBootTestclass CountryListTest {
@Test void testFactory() { final CountryList cl = CountryListFactory.getDefaultCl(); assertNotNull(cl); }}
复制代码

2 思考

跟单例模式有啥区别?


  • 单例主要在于,即全局仅有一个实例;而享元主要在于,即共享。

  • 在一定程度上,可以认为单例是享元的一种特殊情况。

  • 享元更主要的是通过共享,达到节约内存的目的。


跟缓存有些类似?


  • 缓存,主要目的是为了加快访问速度。享元,在于数据共享节约空间为目的。

  • 缓存是一种思想,可以通过享元模式来实现缓存。


其他的一些思考?


  • 享元类似单例,这就可能会涉及到线程安全问题?

  • 主要在于看问题的角度,并不冲突。涉及到并发,要考虑线程安全问题。

  • 不通过享元模式,也能达到相同的效果?

  • 设计模式是对特定(某些场景)通用的解决方案,变通也是很重要的。先有问题才有解决方案,而不是先有设计模式才有特定场景。




封面图来源:https://refactoring.guru/design-patterns/flyweight




echo '5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp' | base64 -d
复制代码


发布于: 刚刚阅读数: 5
用户头像

lpe234

关注

路漫漫其修远兮 2018-07-04 加入

还未添加个人简介

评论

发布
暂无评论
工作中常用的设计模式--享元模式_Java_lpe234_InfoQ写作社区