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

摘要

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

定义

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

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

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

作用

节约内存空间​

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

提高系统性能​

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

享元工厂类的核心地位​

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

类图

角色

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

  • 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之间的整数对象。当请求的整数在这个范围内时,直接从缓存中返回对应的对象,实现了对象的共享,提高了系统的性能和内存利用率。

总结

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

相关推荐
杨充1 天前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 天前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 天前
6.设计原则的全景图
设计模式·开源·全栈
杨充1 天前
2.面向对象的特性
设计模式
杨充1 天前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 天前
8.反模式与坏味道
设计模式·开源·代码规范
杨充1 天前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式