【Java设计模式 设计模式与范式】结构型模式 七:享元模式

简介: 【Java设计模式 设计模式与范式】结构型模式 七:享元模式

本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是享元模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:

  • 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
  • 模式结构:包含模式的角色定义及调用关系以及其模版代码
  • 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
  • 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
  • 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
  • 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构

接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构

模式档案

面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子、图像中的坐标点或颜色,这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式

模式定义:享元模式运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的不变对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

解决什么问题:享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象,具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

优点:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力

缺点:享元模式 使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;

使用场景:当满足如下场景时可以使用享元模式:

  1. 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  2. 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。

需要注意的是:享元对象应该为不可变对象,不可变对象指一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码

模式结构

享元模式的主要角色有如下。

  • 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  • 具体享元角色(Concrete Flyweight):实现抽象享元角色中所规定的接口。
  • 非享元角色(Unsharable Flyweight):是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  • 享元工厂角色(Flyweight Factory):负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

角色间的关系UML如下:

模式实现

按照如上UML图角色代码如下:

1 抽象享元角色

// 抽象享元角色
interface IFlyweight {
    void operation(UnsharedConcreteFlyweight unsharedConcreteFlyweight);
}

2 具体享元角色

// 具体享元角色
class ConcreteFlyweight implements IFlyweight {
    private String intrinsicState;
    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }
    @Override
    public void operation(UnsharedConcreteFlyweight unsharedConcreteFlyweight) {
        System.out.println("Object address: " + System.identityHashCode(this));
        System.out.println("IntrinsicState: " + this.intrinsicState);
        System.out.println("ExtrinsicState: " + unsharedConcreteFlyweight.getExtrinsicState());
    }
}

3 非享元角色

// 非享元角色
class UnsharedConcreteFlyweight {
    private String extrinsicState;
    UnsharedConcreteFlyweight(String extrinsicState) {
        this.extrinsicState = extrinsicState;
    }
    public String getExtrinsicState() {
        return extrinsicState;
    }
    public void setExtrinsicState(String extrinsicState) {
        this.extrinsicState = extrinsicState;
    }
}

4 享元工厂角色

// 享元工厂
class FlyweightFactory {
    private static Map<String, IFlyweight> pool = new HashMap<>();
    // 因为内部状态具备不变性,因此作为缓存的键
    public static IFlyweight getFlyweight(String intrinsicState) {
        if (!pool.containsKey(intrinsicState)) {
            IFlyweight flyweight = new ConcreteFlyweight(intrinsicState);
            pool.put(intrinsicState, flyweight);
        }
        return pool.get(intrinsicState);
    }
}

客户端调用代码如下:

public class FlyweightPattern {
    public static void main(String[] args) {
        IFlyweight flyweight1 = FlyweightFactory.getFlyweight("intrinsicState-a");
        IFlyweight flyweight2 = FlyweightFactory.getFlyweight("intrinsicState-b");
        IFlyweight flyweight3 = FlyweightFactory.getFlyweight("intrinsicState-a");
        UnsharedConcreteFlyweight unsharedConcreteFlyweight_1 = new UnsharedConcreteFlyweight("extrinsicState-1");
        UnsharedConcreteFlyweight unsharedConcreteFlyweight_2 = new UnsharedConcreteFlyweight("extrinsicState-2");
        UnsharedConcreteFlyweight unsharedConcreteFlyweight_3 = new UnsharedConcreteFlyweight("extrinsicState-3");
        flyweight1.operation(unsharedConcreteFlyweight_1);
        flyweight2.operation(unsharedConcreteFlyweight_2);
        flyweight3.operation(unsharedConcreteFlyweight_3);
    }
}

打印结果如下:

内部状态一致时,可以看到打印的是同一个享元对象。

模式实践

就以下棋为例进行实践举例:设计一个在线棋牌游戏大厅

设计一个在线棋牌游戏大厅

一个游戏厅中有成千上万个房间,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 32 个棋子的信息

public class ChessPiece {//棋子
  private String id;
  private String text;
  private String color;
  private int positionX;
  private int positionY;
  public ChessPiece(String id, String text, Color color, int positionX, int positionY) {
    this.id = id;
    this.text = text;
    this.color = color;
    this.positionX = positionX;
    this.positionY = positionX;
  }
  // ...省略其他属性和getter/setter方法...
}
public class ChessBoard {//棋局
  private Map<String , ChessPiece> chessPieces = new HashMap<>();
  public ChessBoard() {
    init();
  }
  private void init() {
    chessPieces.put("black-che-0-0", new ChessPiece("black-che-0-0", "車", ChessPiece.Color.BLACK, 0, 0));
    chessPieces.put("red-che-0-1", new ChessPiece("red-che-0-0","馬", ChessPiece.Color.BLACK, 0, 1));
    //...省略摆放其他棋子的代码...
  }
  public void move(String chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?这个时候,享元模式就可以派上用场了。像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:

package com.example.designpattern.flyweight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
public class ChessBoard {
    public static void main(String[] args) {
        ChessPieceUnit chessPieceUnit1 = ChessPieceUnitFactory.getChessPiece("black-che");
        PiecePos piecePos1 = new PiecePos(0, 1);
        ChessPieceUnit chessPieceUnit2 = ChessPieceUnitFactory.getChessPiece("black-che");
        PiecePos piecePos2 = new PiecePos(5, 1);
        ChessPieceUnit chessPieceUnit3 = ChessPieceUnitFactory.getChessPiece("red-che");
        PiecePos piecePos3 = new PiecePos(0, 4);
        chessPieceUnit1.move(piecePos1);
        chessPieceUnit2.move(piecePos2);
        chessPieceUnit3.move(piecePos3);
    }
}
// 抽象享元角色
interface ChessPieceUnit {
    void move(PiecePos piecePos);
}
// 非享元角色
@Data
@AllArgsConstructor
class PiecePos {
    int toPositionX;
    int toPositionY;
}
// 具体享元角色
@AllArgsConstructor
@Getter
class ChessPieceUnitImpl implements ChessPieceUnit {
    private String name;
    private String text;
    private String color;
    @Override
    public void move(PiecePos piecePos) {
        System.out.println("Object address: " + System.identityHashCode(this));
        System.out.println(this.name + "棋子所在棋盘位置为: " + piecePos.toPositionX + "," + piecePos.toPositionY);
    }
}
// 享元工厂
class ChessPieceUnitFactory {
    private static final Map<String, ChessPieceUnitImpl> pieces = new HashMap<>();
    static {
        ChessPieceUnitImpl chessPieceUnit_black_che = new ChessPieceUnitImpl("black-che", "車", "black");
        ChessPieceUnitImpl chessPieceUnit_red_che = new ChessPieceUnitImpl("red-che", "車", "red");
        pieces.put(chessPieceUnit_black_che.getName(), chessPieceUnit_black_che);
        pieces.put(chessPieceUnit_red_che.getName(), chessPieceUnit_red_che);
        //...省略其他棋子的代码...
    }
    public static ChessPieceUnitImpl getChessPiece(String chessPieceId) {
        return pieces.get(chessPieceId);
    }
}

上面的代码中,我们利用工厂类来缓存 ChessPieceUnitImpl 信息(也就是 id、text、color)。通过工厂类获取到的 ChessPieceUnitImpl 就是享元。所有的 ChessBoard 对象共享这 32 个 ChessPieceUnitImpl 对象(因为象棋中只有 32 个棋子)。在使用享元模式之前,记录 1 万个棋局,我们要创建 32 万(32*1 万)个棋子的 ChessPieceUnitImpl 对象。利用享元模式,我们只需要创建 32 个享元对象供所有棋局共享使用即可,大大节省了内存。实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的

享元模式在JDK中应用

享元模式应用于Java中的基本类型Cache和String类型的缓存。

IntegerCache实现

Java中的IntegerCache就是享元模式的一个应用:当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候

Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);

如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建

/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
 * During VM initialization, java.lang.Integer.IntegerCache.high property
 * may be set and saved in the private system properties in the
 * sun.misc.VM class.
 */
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];
    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }
    private IntegerCache() {}
}

以下的例子中:

Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);

第一种创建方式并不会使用到 IntegerCache,而后面两种创建方法可以利用 IntegerCache 缓存,返回共享的对象,以达到节省内存的目的。举一个极端一点的例子,假设程序需要创建 1 万个 -128 到 127 之间的 Integer 对象。使用第一种创建方式,我们需要分配 1 万个 Integer 对象的内存空间;使用后两种创建方式,我们最多只需要分配 256 个 Integer 对象的内存空间

String字符串常量池

Java的Sting也用到了享元模式,JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作字符串常量池,细节参照我这篇Blog:【深入理解JVM 三】类Class文件结构

String s1 = "tml";
String s2 = "tml";
String s3 = new String("tml");
System.out.println(s1 == s2);
System.out.println(s1 == s3);

上面代码的运行结果是:一个 true,一个 false。跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的tml)。

模式对比

分别看看享元模式和几个相似概念的对比:

享元模式跟单例的区别

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。但是区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数

享元模式跟缓存的区别

在享元模式的实现中,我们通过工厂类来缓存已经创建好的对象。这里的缓存实际上是存储的意思,跟我们平时所说的数据库缓存、CPU 缓存、MemCache 缓存是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用

享元模式跟对象池的区别

对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?虽然对象池、连接池、线程池、享元模式都是为了复用,但是对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。

  • 池化技术中的复用可以理解为重复使用,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。
  • 享元模式中的复用可以理解为共享使用,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间

所以说设计结构类似但是设计意图不同

总结一下

享元模式从概念上看非常简单,意图就是复用相似对象,结构上和多例模式、缓存、池化技术等类似都用到了工厂,但是设计意图却大相径庭,例如多例模式主要用于限制对象个数,缓存时提高对象访问效率,池化技术主要目的是节省时间。从这里也能看出,有些模型设计虽然结构非常类似,但是出发点却不同,这也印证了学习设计模式的出发点应为设计意图而非死记结构,最终结构只是设计的落地方式而已。

相关文章
|
25天前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
38 0
[Java]23种设计模式
|
8天前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
1月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
1月前
|
设计模式 Java
Java设计模式
Java设计模式
28 0
|
1月前
|
设计模式 Java
Java设计模式之外观模式
这篇文章详细解释了Java设计模式之外观模式的原理及其应用场景,并通过具体代码示例展示了如何通过外观模式简化子系统的使用。
29 0
|
1月前
|
设计模式 Java
Java设计模式之桥接模式
这篇文章介绍了Java设计模式中的桥接模式,包括桥接模式的目的、实现方式,并通过具体代码示例展示了如何分离抽象与实现,使得两者可以独立变化。
43 0
|
1月前
|
设计模式 Java
Java设计模式之适配器模式
这篇文章详细讲解了Java设计模式中的适配器模式,包括其应用场景、实现方式及代码示例。
42 0
|
17天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
20天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###