设计模式(结构型)-享元模式

摘要

在软件开发的广阔领域中,随着系统规模的不断膨胀,资源的有效利用逐渐成为了一个至关重要的议题。当一个系统中存在大量相似的对象时,如何优化这些对象的管理,减少内存的占用,提升系统的整体性能,成为了开发者们亟待解决的问题。享元模式作为一种结构型设计模式,应运而生,为这一难题提供了行之有效的解决方案。

定义

享元模式,运用共享技术有效地支持大量细粒度的对象。在这一模式中,对象的状态被清晰地划分为内部状态和外部状态。内部状态,是对象中那些可以共享的相同内容,它存储于对象内部,且不会随着环境的改变而发生变化。例如,在一个图形绘制系统中,圆形的半径、颜色等属性,对于所有同类型的圆形来说,若这些属性值相同,就可视为内部状态。而外部状态,则是那些需要外部环境来设置的、无法共享的内容,其会随着环境的变化而改变。比如,在上述图形绘制系统中,圆形在画布上的具体位置,就属于外部状态。​

值得注意的是,外部状态和内部状态相互独立,外部状态的变化不会对内部状态产生影响。通过这种状态的划分,我们可以为相同的对象设置不同的外部状态,从而使它们呈现出不同的特征,同时共享相同的内部状态,极大地减少了对象的创建数量。​

从本质上讲,享元模式的核心在于 "分离与共享"。它将对象的状态进行分离,区分出变化的部分(外部状态)和不变的部分(内部状态),并对不变的部分进行共享。通过这种方式,达到减少对象数量、节约内存空间的目的,使系统在处理大量细粒度对象时更加高效。

作用

节约内存空间​

在许多实际应用场景中,系统需要创建和管理大量相似的对象。例如,在一个在线游戏中,可能存在成千上万的游戏角色,这些角色在很多方面具有相似性,如角色的基本属性(生命值、攻击力等)、技能特效等。若为每个角色都创建一个独立的对象实例,将会消耗大量的内存资源。而运用享元模式,我们可以将这些角色的共同属性(内部状态)进行共享,仅为每个角色保存其独特的属性(外部状态,如在游戏地图中的位置、当前装备等)。这样,系统中实际存在的对象数量将大幅减少,从而显著节约内存空间。​

提高系统性能​

减少对象的创建数量不仅能够降低内存的使用,还能提升系统的性能。创建对象是一个相对耗时的操作,需要分配内存、初始化对象的属性等。当系统中存在大量对象创建需求时,频繁的对象创建操作会严重影响系统的响应速度。享元模式通过共享对象,减少了不必要的对象创建,使得系统在处理大量对象时,能够更加高效地运行,提高了系统的整体性能。​

享元工厂类的核心地位​

享元模式的核心在于享元工厂类。享元工厂类就像是一个对象的管理者,其主要职责是提供一个用于存储享元对象的享元池。当用户需要获取某个对象时,享元工厂首先会在享元池中进行查找。如果享元池中已经存在符合要求的对象,那么工厂将直接返回该对象给用户;若享元池中不存在该对象,工厂则会创建一个新的享元对象,然后将其返回给用户,并同时将这个新增对象保存到享元池中,以便后续再次使用。通过这种方式,享元工厂类有效地实现了对象的共享和复用,极大地提高了系统资源的利用效率。

类图

角色

在享元模式的类图中,主要包含以下几个关键角色,它们相互协作,共同实现了对象的共享和高效管理:​

  • Flyweight(享元接口):作为享元对象的抽象接口,它定义了对象需要实现的方法,并且通过这些方法来接收外部状态并对其进行处理。享元接口为具体的享元实现对象提供了统一的规范,确保不同的具体享元对象在行为上具有一致性。​

  • ConcreteFlyweight(具体的享元实现对象):具体实现 Flyweight 接口的类,它是可共享的对象。在 ConcreteFlyweight 类中,需要对享元对象的内部状态进行封装,实现接口中定义的业务逻辑。例如,在一个文字排版系统中,某个特定字体、字号的文字对象就可以作为一个具体的享元实现对象,它内部封装了字体、字号等内部状态。​

  • UnsharedConcreteFlyweight(非共享的享元实现对象):并非所有的享元对象都能够被共享,UnsharedConcreteFlyweight 就是这样的非共享对象。它通常是享元对象的组合对象,包含了一些独特的、无法共享的状态。比如,在一个复杂的图形绘制系统中,一个由多个不同形状组合而成的复杂图形对象,可能就无法作为共享的享元对象,而属于非共享的享元实现对象。​

  • FlyweightFactory(享元工厂):享元工厂类在整个享元模式中起着至关重要的作用。它主要负责创建并管理共享的享元对象,维护一个享元池来存储已经创建的享元对象。当外部系统请求一个享元对象时,享元工厂首先在享元池中查找是否存在符合要求的对象,如果存在则直接返回;若不存在,则创建一个新的享元对象,将其加入享元池后再返回给请求者。享元工厂通过这种方式,有效地实现了享元对象的共享和复用

具体实现

  • Flyweight(享元接口):在 Java 代码实现中,享元接口可以通过接口或者抽象类来定义。例如,在一个图形绘制的享元模式实现中,我们可以定义如下享元接口:

    public interface Shape {​
    void draw(int x, int y);​
    }

这里的draw方法接收外部状态(坐标x和y),用于在特定位置绘制图形。通过定义这样的接口,具体的图形享元实现对象(如圆形、矩形等)都需要实现该接口,确保了它们在处理外部状态和绘制行为上的一致性。

  • ConcreteFlyweight(具体的享元实现对象):以圆形为例,它实现了上述Shape接口:

    public class Circle implements Shape {​
    private String color; // 内部状态,颜色​

    public Circle(String color) {​
    this.color = color;​
    }​

    @Override​
    public void draw(int x, int y) {​
    System.out.println("绘制颜色为 " + color + " 的圆形,坐标为 (" + x + ", " + y + ")");​
    }​
    }

在Circle类中,color属性作为内部状态被封装在对象内部,并且在draw方法中,结合传入的外部状态(坐标x和y)进行图形的绘制操作。

  • UnsharedConcreteFlyweight(非共享的享元实现对象):假设在图形绘制系统中,存在一种特殊的组合图形,它由多个不同形状的图形组合而成,并且每个组合图形都有其独特的属性和行为,无法进行共享。我们可以定义如下非共享的享元实现对象类:

    public class ComplexShape {​
    private List shapes = new ArrayList<>();​
    private String uniqueProperty; // 独特的属性​

    public ComplexShape(String uniqueProperty) {​
    this.uniqueProperty = uniqueProperty;​
    }​

    public void addShape(Shape shape) {​
    shapes.add(shape);​
    }​

    public void draw() {​
    System.out.println("绘制具有独特属性 " + uniqueProperty + " 的复杂图形:");​
    for (Shape shape : shapes) {​
    shape.draw(0, 0); // 简单示例,实际可能需要更复杂的坐标计算​
    }​
    }​
    }

在这个ComplexShape类中,uniqueProperty属性表示其独特的、无法共享的状态,并且它可以包含多个共享的享元对象(通过addShape方法添加),在draw方法中实现了其独特的绘制逻辑。

  • FlyweightFactory(享元工厂):以下是一个简单的享元工厂类实现:

    import java.util.HashMap;​
    import java.util.Map;​

    public class ShapeFactory {​
    private static final Map circleMap = new HashMap<>();​

    public static Shape getCircle(String color) {​
    Circle circle = circleMap.get(color);​
    if (circle == null) {​
    circle = new Circle(color);​
    circleMap.put(color, circle);​
    System.out.println("创建颜色为 " + color + " 的圆形");​
    }​
    return circle;​
    }​
    }

在这个ShapeFactory类中,维护了一个circleMap享元池来存储已经创建的圆形对象。当外部请求获取某个颜色的圆形时,首先在享元池中查找,如果不存在则创建一个新的圆形对象并放入享元池,最后返回该圆形对象,实现了圆形对象的共享和复用。

优缺点

优点

  • 减少内存中对象数量:享元模式最显著的优点之一就是能够极大地减少内存中对象的数量。通过共享相同的内部状态,系统中只需要保存一份相同状态的对象实例,而对于不同的外部状态,通过传入不同的参数来进行区分。例如,在一个包含大量文本字符的文档处理系统中,每个字符的字体、字号等内部状态可以共享,仅需为每个字符的位置等外部状态单独存储,从而使得相同或相似对象在内存中只保存一份,有效地降低了内存的占用。​

  • 外部状态独立性:享元模式中,外部状态相对独立于内部状态,并且不会影响其内部状态。这意味着享元对象可以在不同的环境中被共享使用,因为它们的核心内部状态不会因为外部环境的变化而受到影响。例如,在一个游戏场景中,游戏角色的基本属性(如生命值、攻击力等内部状态)可以被共享,而角色在不同地图中的位置(外部状态)可以根据游戏的进行随时改变,不会对角色的基本属性产生影响,使得相同的角色享元对象可以在不同的游戏场景中复用。

缺点

  • 系统复杂度增加:引入享元模式会使系统变得更加复杂。为了实现对象状态的分离和共享,需要仔细地分析和设计,将对象的状态准确地划分为内部状态和外部状态。这一过程需要开发者具备较强的抽象思维和设计能力,同时也增加了代码的理解和维护难度。例如,在一个复杂的企业级应用系统中,对于业务对象状态的划分可能需要深入了解业务逻辑和系统架构,否则可能导致状态划分不合理,影响系统的正常运行。​

  • 运行时间变长:为了实现对象的共享,享元模式需要将享元对象的状态外部化,这就意味着在使用享元对象时,需要从外部读取这些状态信息。相比于直接访问对象内部的所有状态信息,读取外部状态的操作会增加一定的运行时间开销。特别是在对性能要求极高的场景中,这种额外的运行时间开销可能会对系统的整体性能产生一定的影响。例如,在一个对实时性要求很高的金融交易系统中,频繁读取外部状态可能会导致交易响应时间变长,影响用户体验。

使用场景

大量相同或相似对象的场景​

当一个系统中有大量相同或相似的对象存在,并且这些对象的大量使用导致内存大量耗费时,享元模式是一个非常合适的解决方案。例如,在一个在线地图应用中,地图上可能存在成千上万的标注点,这些标注点在很多属性上(如标注点的图标样式、大小等)可能是相同的,只有其在地图上的位置不同。通过使用享元模式,将标注点的共同属性作为内部状态进行共享,仅为每个标注点保存其独特的位置信息(外部状态),可以显著减少内存的占用,提高系统的运行效率。​

对象状态可外部化的场景​

如果对象的大部分状态可以被外部化,即这些状态可以从对象中分离出来,并在需要时通过外部环境传入对象中,那么享元模式就可以发挥作用。例如,在一个图形渲染系统中,图形的颜色、形状等属性可以作为内部状态进行共享,而图形在屏幕上的显示位置、旋转角度等属性可以作为外部状态,根据用户的操作动态地传入图形对象中。这样,通过共享内部状态,减少了对象的创建数量,同时通过灵活设置外部状态,满足了不同的显示需求。​

多次重复使用享元对象的场景​

由于使用享元模式需要维护一个存储享元对象的享元池,这本身需要耗费一定的资源,包括内存和时间。因此,只有在多次重复使用享元对象的情况下,使用享元模式才是值得的。例如,在一个数据库连接池的实现中,数据库连接对象的创建和销毁是比较耗时的操作。通过使用享元模式,将数据库连接对象作为享元对象,维护一个连接池来存储和管理这些连接对象。当应用程序需要数据库连接时,从连接池中获取已有的连接对象,使用完毕后再放回连接池。这样,在大量的数据库操作中,通过重复使用连接对象,有效地提高了系统的性能,并且弥补了维护连接池所带来的资源开销。

使用案例

JDK 类库中的 String 类

在 JDK 类库中,String类是使用享元模式的典型代表。当我们创建字符串对象时,如果字符串的值相同,Java 会尝试从字符串常量池中获取已经存在的字符串对象,而不是创建一个新的对象。例如:

复制代码
String str1 = "hello";​
String str2 = "hello";​
System.out.println(str1 == str2); // 输出 true

在这个例子中,str1和str2指向的是字符串常量池中的同一个对象,因为它们的值都是"hello"。通过这种方式,Java 有效地减少了字符串对象的创建数量,节约了内存空间。

Integer 类中的享元模式

在Integer类中,也应用了享元模式。当我们通过Integer.valueOf(int i)方法获取Integer对象时,如果目标值在-128到127之间,Integer类会从缓存中获取已经存在的对象,而不是创建新的对象。例如:

复制代码
Integer num1 = Integer.valueOf(10);​
Integer num2 = Integer.valueOf(10);​
System.out.println(num1 == num2); // 输出 true

这是因为在Integer类的内部维护了一个缓存数组,用于存储-128到127之间的整数对象。当请求的整数在这个范围内时,直接从缓存中返回对应的对象,实现了对象的共享,提高了系统的性能和内存利用率。

总结

元模式作为一种强大的结构型设计模式,在优化系统性能、减少内存占用方面具有显著的优势。尽管它增加了系统的复杂度,并且在某些场景下可能会带来一定的运行时间开销,但在合适的应用场景中,其带来的好处远远超过了这些弊端。通过深入理解享元模式的原理、结构和应用场景,开发者能够更加高效地设计和构建软件系统,使其在面对大量细粒度对象时,依然能够保持良好的性能和资源利用率。

相关推荐
骊山道童7 小时前
设计模式-外观模式
设计模式·外观模式
小马爱打代码9 小时前
设计模式:迪米特法则 - 最少依赖,实现高内聚低耦合
设计模式·迪米特法则
骊山道童9 小时前
设计模式-观察者模式
观察者模式·设计模式
自在如风。14 小时前
Java 设计模式:组合模式详解
java·设计模式·组合模式
cccccchd14 小时前
23种设计模式生活化场景,帮助理解
设计模式
未定义.22114 小时前
Java设计模式实战:装饰模式在星巴克咖啡系统中的应用
java·开发语言·设计模式·软件工程
blackA_14 小时前
Java学习——day29(并发控制高级工具与设计模式)
java·学习·设计模式
Antonio91515 小时前
【设计模式】适配器模式
设计模式·oracle·适配器模式
小猪乔治爱打球16 小时前
[Golang修仙之路]单例模式
设计模式