首页 > 系统相关 >【设计模式】享元模式Flyweight:通过共享对象减少内存加载消耗

【设计模式】享元模式Flyweight:通过共享对象减少内存加载消耗

时间:2023-08-03 21:06:37浏览次数:35  
标签:享元 状态 缓存 对象 模式 Flyweight 共享 设计模式

(目录)


享元模式Flyweight:通过共享对象减少内存加载消耗

享元模式的用意

享元模式以共享的⽅法⾼效地⽀持⼤量的细粒度对象享元对象能做到共享的关键是区分内蕴状态和外蕴状态

⼀个内蕴状态是存储在享元对象内部的,并且是不会随环境改变⽽有所不同的,因此⼀个享元可以具有内蕴状态并可以共享。⼀个外蕴状态是随环境的改变 ⽽改变的,不可以共享的状态,享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使⽤的时候各⽅⾯传⼊到享元对象内部,外蕴状态不可以 影响享元对象的内蕴状态,换句话说,它们是相互独⽴的.


享元模式应当在什么情况下使用

● ⼀个系统有⼤量的对象

● 这些对象耗费⼤量的内存

● 这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中删除时,每⼀个组都可以仅⽤⼀个对象代替

● 使⽤享元模式需要维护⼀个记录了系统已有的所有享元的表,⽽这需要耗费资源,因此应当在有⾜够多的享元实现可供共享时才值的使⽤享元模式


原理

享元模式的原理和实现都很简单,但是应用场景却相对狭窄,它和缓存模式、池化模式有所联系,却又有不同

原始定义是:

摒弃在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,从而让我们能在有限的内存容量中载入更多对象。

从定义中可以发现,享元模式要解决的核心问题就是节约内存空间使用的办法是找出相似对象之间的共有特征,然后复用这些特征。

UML 图是如何表示享元模式的,如下图:

image-20230708205506882

从这个 UML 图中,享元模式包含的关键角色有四个:

  1. 享元类(Flyweight):定义了享元对象需要实现的公共操作方法。在该方法中会使用一个状态作为输入参数,也叫外部状态,由客户端保存,在运行时改变;
  2. 享元工厂类(Flyweight Factory):管理一个享元对象类的缓存池。它会存储享元对象之间需要传递的共有状态,比如,按照大写英文字母来作为状态标识,这种只在享元对象之间传递的方式就叫内部状态。同时,它还提供了一个通用方法 getFlyweight(),主要通过内部状态标识来获取享元对象
  3. 可共享的具体享元类(ConcreteFlyweight)能够复用享元工厂内部状态并实现享元类公共操作的具体实现类
  4. 非共享的具体享元类(UnsharedConcreteFlyweight)不复用享元工厂内部状态,但实现享元类的具体实现类

备注:

内部状态不会随环境改变而改变的状态,俗称不可变对象。

比如,在 Java 中 Integer 对象初始化就是缓存 -127 到 128 的值,无论怎么使用 Integer,这些值都不会变化。

外部状态随环境改变而改变的状态

​ 通常是某个对象所独有的,不能被共享,因此,也只能由客户端保存。之所以需要外部状态就是因为客户端需要不同的定制化操作


UML 对应的代码实现:

/**
 * 享元类
 */
public interface FlyWeight {
    void operation(int state);
}

/**
 * 享元工厂类
 */
public class FlyWeightFactory {

    // 定义一个池容器
    public Map<String, FlyWeight> pool = new HashMap<>();

    public FlyWeightFactory() {
        // 将对应的内部状态添加进去
        pool.put("A", new ConcreteFlyweight("A"));
        pool.put("B", new ConcreteFlyweight("B"));
        pool.put("C", new ConcreteFlyweight("C"));
    }

    // 根据内部状态来查找值
    public FlyWeight getFlyWeight(String key) {
        if (pool.containsKey(key)) {
            System.out.println("==== 享元池中有,直接复用,key:" + key);
            return pool.get(key);
        } else {
            System.out.println("==== 享元池中没有,重新创建并复用,key:" + key);
            FlyWeight flyWeightNew = new ConcreteFlyweight(key);
            pool.put(key, flyWeightNew);
            return flyWeightNew ;
        }
    }

    public FlyWeight getUnSharedFlyWeight(String key) {
        FlyWeight flyWeight = new UnSharedConcreteFlyweight(key);
        System.out.println("=== 创建不共享的对象,key: " + key);
        return flyWeight;
    }
}


public class UnSharedConcreteFlyweight implements FlyWeight{

    private String uniqueKey;

    public UnSharedConcreteFlyweight(String uniqueKey) {
        this.uniqueKey = uniqueKey;
    }

    @Override
    public void operation(int state) {
        System.out.println("=== 使用不共享的对象,内部状态:"+uniqueKey+",外部状态:"+state);
    }
}

这段代码实现非常简单,不过这里你可能会联想到缓存模式,比如,LRU 缓存模式。但这两者是完全不同的设计意图

它们的本质区别在于:享元模式要解决的问题是节约内存的空间大小,而缓存模式本质上是为了节省时间

回到上面的代码分析中,能看出享元模式封装的变化有:

● 对象内部状态的定义规则,比如,是通过字母共享状态,还是通过固定的数字来共享状态; ● 具体享元对象所实现的公共操作的逻辑

所以说,享元模式本质上是通过创建更多的可复用对象的共有特征来尽可能地减少创建重复对象的内存消耗


使用场景

一般来讲,享元模式常用的使用场景有以下几种:

  1. 系统中存在大量重复创建的对象。比如,同一个商品的展示图片、详情介绍、文字介绍等,当自营商家系统调用或第三方商家调用时,商品系统可以使用同一个对象来节省内存空间;
  2. 可以使用外部特定的状态来控制使用的对象。比如,使用常用的中文汉字作为读取的标识,读取享元池中共享的多个中文汉字对象;
  3. 相关性很高并且可以复用的对象。比如,公司的组织结构人员基本信息、网站的分类信息列表等。

Java 的 Integer 享元设计

在 Java 中,享元模式一个常用的场景就是,使用数据类的包装类对象的 valueOf() 方法。比如,使用 Integer.valueOf() 方法时,实际的代码实现中有一个叫 IntegerCache 的静态类,它就是一直缓存了 -127 到 128 范围内的数值,如下代码所示,可以在 Java JDK 中的 Integer 类的源码中找到这段代码。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

享元模式本质上在使用时就是找到不可变的特征,并缓存起来,当类似对象使用时从缓存中读取,以达到节省内存空间的目的。比如,在需要承接大流量的系统中使用图片,都知道高清图片即便是压缩后占用的内存空间也很大,那么在使用图片时节省内存空间就是首要考虑的设计因素,而享元模式可以很好地帮助解决这类问题场景。


使用享元模式的原因

使用享元模式的原因,主要有以下两个:

  1. 减少内存消耗,节省服务器成本。 比如,当大量商家的商品图片、固定文字(如商品介绍、商品属性)在不同的网页进行展示时,通常不需要重复创建对象,而是可以使用同一个对象,以避免重复存储而浪费内存空间。由于通过享元模式构建的对象是共享的,所以当程序在运行时不仅不用重复创建,还能减少程序与操作系统的 IO 交互次数,大大提升了读写性能
  2. 聚合同一类的不可变对象,提高对象复用性。 比如,Java 中的 Number 对象族类(Integet、Long、Double 等)都是使用了享元模式例子,通过缓存不同范围数值来重复使用相同的数值

优缺点

通过上面的分析,我们可以得出使用享元模式主要有以下优点

可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。

通过封装内存特有的运行状态,达到共享对象之间高效复用的目的。

缺点:

以时间换空间,间接增加了系统的实现复杂度。比如,需要分离出内部状态和外部状态,其中,外部状态这个定义比较模糊,也很难统一,而内部状态除了一些常用的常量容易被找到以外,那些更通用的组件对象在不同的业务系统中其实是不容易被找到的,因为不同的人对相似对象的理解并不一致,这就需要对程序员的代码设计实现思维有一定的要求

运行时间更长,对于一些需要快速响应的系统并不合适享元模式的目的是节省空间,而没有说需要提供更短的时间,这适合数据类项目的使用,而不适合一些有高并发要求的系统


总结:

享元模式为共享对象定义了一个很好的结构范例。不过,用好享元模式的关键在于找到不可变对象,比如,常用数字、字符等

之所以做对象共享而不是对象复制的一个很重要的原因,就在于节省对象占用的内存空间大小。

缓存模式享元模式最大的区别就是:

  1. 享元模式强调的是空间效率,比如,一个很大的数据模型对象如何尽量少占用内存并提供可复用的能力;

  2. 缓存模式强调的是时间效率,比如:缓存秒杀的活动数据和库存数据等,数据可能会占用很多内存和磁盘空间,但是得保证在大促活动开始时要能及时响应用户的购买需求。

也就是说,两者本质上解决的问题类型是不同的

虽然享元模式应用不如缓存模式多,但是对于超大型数据模式来说,它却是非常有效的优化方法之一。特别是对于现在越来越多的数据系统来说,共享变得更加重要,因为复制虽然时间效率更高,但是空间上可能完全不够。


标签:享元,状态,缓存,对象,模式,Flyweight,共享,设计模式
From: https://blog.51cto.com/panyujie/6952904

相关文章

  • 【设计模式】装饰器模式Decorator:在基础组件上扩展新功能
    (目录)装饰器模式看上去和适配器模式、桥接模式很相似,都是使用组合方式来扩展原有类的,但其实本质上却相差甚远呢。简单来说,适配器模式侧重于转换,而装饰模式侧重于动态扩展;桥接模式侧重于横向宽度的扩展,而装饰模式侧重于纵向深度的扩展。原理装饰模式的原始定义是:允许动态地向......
  • PHP设计模式汇总
    PHP设计模式汇总没想到啊,没想到。自己竟然坚持了下来,完成了设计模式深入的学习,并且输出了23篇用php演示的设计模式的文章。但这不是最主要的,更深层次的收获是顺便背下了这些模式的定义及类图。在深入学习了设计模式之后,对Laravel等框架的架构理解也更清楚明了了。就像我在很多模式......
  • Java设计模式--装饰器模式
    Java设计模式--装饰器模式一、问题背景在项目场景中,有这样一个需求,需要对录入的加班进行规则校验,包括但不限于,对加班的录入时间进行检查,对录入的加班类型进行检查,对加班日期的班次进行对比检查,对潜入系统的时长进行对比检查等等。具体来说,就是对一条加班记录,进行多种规则的检查......
  • 【设计模式】适配器模式Adapter:处理不同 API 接口的兼容性
    (目录)适配器模式适配器模式(AdapterPattern)是作为两个不兼容的接⼝之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独⽴接⼝的功能。在某些时候,客户期望获得某种功能接⼝但现有的接⼝⽆法满⾜客户的需求,例如美国的正常供电电压为110V,⼀个中国⼈带了⼀款中国制造......
  • Java设计模式
    #Java设计模式GoF(最先开始着手进行设计模式分类整理工作)对设计模式的定义是:设计模式是在特定的环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用。一、面向对象设计的七大原则1.1单一职责原则定义:一个对象应该只包含单一的职......
  • JDK中有关23个经典设计模式
    Structural(结构模式)Adapter:把一个接口或是类变成另外一种。java.util.Arrays#asList()javax.swing.JTable(TableModel)java.io.InputStreamReader(InputStream)java.io.OutputStreamWriter(OutputStream)javax.xml.bind.annotation.adapters.XmlAdapter#marshal()javax.......
  • 设计模式原则之:单一职责模式
     对类来说的,即一个类应该只负责一项职责。如类A负责两个不同的职责,职责1,职责2。当职责1需求变更而改变A时,可能造成职责2智行错误,所以要将类A的粒度分解为A1,A2错误的应用实例packageorg.example.demo0;/***@description:单一职责原则*@author:abel.he*@date:20......
  • Android 设计模式:(二)观察者模…
    Android设计模式:(二)观察者模式——让你的对象知悉现况设计模式2012-05-2813:28 1074人阅读 评论(1) 收藏 举报*观察者模式:定义了对象之间的一对多依赖关系,当一个对象(主题对象)的状态改变时,它的所有依赖者(观察者对象)都会收到通知并自动更新。*观察......
  • Spring中的设计模式详解
    Spring中的设计模式详解​JDK中用到了哪些设计模式?Spring中用到了哪些设计模式?这两个问题,在面试中比较常见。我在网上搜索了一下关于Spring中设计模式的讲解几乎都是千篇一律,而且大部分都年代久远。所以,花了几天时间自己总结了一下,由于我的个人能力有限,文中如有任何错误各......
  • 创建型设计模式:工厂方法、简单工厂、抽象工厂
    1.前言设计模式,对于像java这种面向对象的语言来说,个人感觉是比较重要的。尤其在构建大型项目,设计模式的优点不言而喻。那么设计模式是什么?解决了软件工程中的什么问题?要想学一门东西,学一个知识点,我觉得首先得了解这个东西(知识、技术)它解决了什么问题,如果没有这门技术,那之前的做......