【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 缓存是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用

享元模式跟对象池的区别

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

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

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

总结一下

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

相关文章
|
16天前
|
设计模式 测试技术 Python
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
【7月更文挑战第10天】Page Object Model (POM)是Selenium自动化测试中的设计模式,用于提高代码的可读性和维护性。POM将每个页面表示为一个类,封装元素定位和交互操作,使得测试脚本与页面元素分离。当页面元素改变时,只需更新对应页面类,减少了脚本的重复工作和维护复杂度,有利于团队协作。POM通过创建页面对象,管理页面元素集合,将业务逻辑与元素定位解耦合,增强了代码的复用性。示例展示了不使用POM时,脚本直接混杂了元素定位和业务逻辑,而POM则能解决这一问题。
33 6
|
14天前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
【7月更文挑战第12天】在本文中,作者宏哥介绍了如何在不使用PageFactory的情况下,用Java和Selenium实现Page Object Model (POM)。文章通过一个百度首页登录的实战例子来说明。首先,创建了一个名为`BaiduHomePage1`的页面对象类,其中包含了页面元素的定位和相关操作方法。接着,创建了测试类`TestWithPOM1`,在测试类中初始化WebDriver,设置驱动路径,最大化窗口,并调用页面对象类的方法进行登录操作。这样,测试脚本保持简洁,遵循了POM模式的高可读性和可维护性原则。
18 2
|
15天前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
【7月更文挑战第11天】页面对象模型(POM)通过Page Factory在Java Selenium测试中被应用,简化了代码维护。在POM中,每个网页对应一个Page Class,其中包含页面元素和相关操作。对比之下,非POM实现直接在测试脚本中处理元素定位和交互,代码可读性和可维护性较低。
14 0
|
17天前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
22 0
|
17天前
|
设计模式 缓存 安全
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
14 0
|
17天前
|
设计模式 并行计算 安全
Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
20 0
|
17天前
|
设计模式 安全 Java
Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
29 0
|
设计模式 Java
【Java设计模式 设计模式与范式】结构型模式 一:适配器模式
【Java设计模式 设计模式与范式】结构型模式 一:适配器模式
51 0
|
设计模式 Java
【玩转23种Java设计模式】结构型模式篇:适配器模式
适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。适配器模式属于结构型模式,主要分为三类:类适配器模式、对象适配器模式、接口适配器模式。
【玩转23种Java设计模式】结构型模式篇:适配器模式