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

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

因为本例的享元类中,内部状态只有一种。所以增加了DEFAULTkey值,实际也就这一种。
在这里有没有发现什么???如果没有这个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

你可能感兴趣的:(工作中常用的设计模式--享元模式)