XiaoLin's Blog

Xiao Lin

享元设计模式学习笔记

9
2024-02-26

享元设计模式介绍

享元模式(Flyweight Pattern)是一种软件设计模式,主要用于减少内存使用量和分享资讯给尽可能多的相似物件。当大量物件因重复而导致内存使用无法接受时,享元模式可以提供有效的解决方案。享元模式通过共享技术,将细粒度的对象的状态存储在共享的享元中,从而减少内存消耗。

享元模式通常将对象的部分状态放在外部数据结构中,当需要使用时再将它们传递给享元。享元模式包含两个状态:内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境改变而改变,是可以共享的。外蕴状态则是不可以共享的,它随环境改变而改变,因此由客户端来保持。

享元模式在 Java 中的实现通常包括以下四个角色:

  1. 抽象享元角色:为具体享元角色规定必须实现的方法,外蕴状态以参数的形式通过此方法传入。在 Java 中可以由抽象类、接口来担当。
  2. 具体享元角色:实现抽象角色规定的方法,并为内蕴状态提供存储空间。
  3. 享元工厂角色:负责创建和管理享元角色,是实现共享的关键。
  4. 客户端角色:维护对所有享元对象的引用,并存储对应的外蕴状态。

享元模式适用于大量细粒度的对象导致内存消耗过大的场景。例如,当一个应用程序需要处理大量对象,而这些对象造成较大的存储开销时,可以考虑使用享元模式。典型的应用场景包括文书处理器中的图形结构表示字符,通过共享字形物件来减少内存消耗。

享元模式实现

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

public class ChessPiece {//棋子
    private int id;
    private String text;
    private Color color;
 
    public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
        this.id = id;
        this.text = text;
        this.color = color;
        this.positionX = positionX;
        this.positionY = positionX;
    }
    public static enum Color {
        RED, BLACK
    }
    // ...省略其他属性和getter/setter方法...
}

public class ChessBoard {//棋局
    private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
    public ChessBoard() {
        init();
    }
    private void init() {
        chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
        chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
        //...省略摆放其他棋子的代码...
    }
    public void move(int chessPieceId, int toPositionX, int toPositionY) {
        //...省略...
    }
}

为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?

这个时候,享元模式就可以派上用场了。像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:

package xyz.xiaolinz.demo.enjoy;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 享元模式 - 抽象出来的棋子享元,通过享元工厂获取
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/11/30
 */
@AllArgsConstructor
@Data

public class ChessPieceUnit {

    private Integer id;

    private String text;

    private Color color;

    /**
     * 颜色
     *
     * @author huangmuhong
     * @version 1.0.0
     * @date 2023/11/30
     * @see Enum
     */
    public enum Color {
        RED, BLACK
    }

}


package xyz.xiaolinz.demo.enjoy;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 坐标
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/11/30
 */
@Data
@AllArgsConstructor
@EqualsAndHashCode
public class Coordinates {

    private Integer x;

    private Integer y;
}


package xyz.xiaolinz.demo.enjoy;

import java.util.ArrayList;
import java.util.List;
import xyz.xiaolinz.demo.enjoy.ChessPieceUnit.Color;

/**
 * 默认国际象棋享元工厂
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/11/30
 * @see ChessFactory
 */
public class DefaultChessFactory implements ChessFactory {

    private final List<ChessPieceUnit> chessPieceUnits = new ArrayList<>();

    public DefaultChessFactory() {
        chessPieceUnits.add(new ChessPieceUnit(1, "車", Color.RED));
        chessPieceUnits.add(new ChessPieceUnit(2, "馬", Color.RED));
        chessPieceUnits.add(new ChessPieceUnit(3, "相", Color.RED));
        chessPieceUnits.add(new ChessPieceUnit(4, "仕", Color.RED));
        chessPieceUnits.add(new ChessPieceUnit(5, "帥", Color.RED));

        chessPieceUnits.add(new ChessPieceUnit(1, "車", Color.BLACK));
        chessPieceUnits.add(new ChessPieceUnit(2, "馬", Color.BLACK));
        chessPieceUnits.add(new ChessPieceUnit(3, "相", Color.BLACK));
        chessPieceUnits.add(new ChessPieceUnit(4, "仕", Color.BLACK));
        chessPieceUnits.add(new ChessPieceUnit(5, "帥", Color.BLACK));
    }

    @Override
    public ChessPieceUnit getChessPiece(int id, Color color) {
        return chessPieceUnits.stream()
            .filter(chessPieceUnit -> chessPieceUnit.getId() == id && chessPieceUnit.getColor() == color).findFirst()
            .orElseThrow(() -> new RuntimeException("未找到棋子"));
    }
}


package xyz.xiaolinz.demo.enjoy;

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

/**
 * 享元模式 - 客户端角色
 *
 * @author huangmuhong
 * @version 1.0.0
 * @date 2023/11/30
 */
public class Checkerboard {

    private final Map<Coordinates, ChessPieceUnit> chessPieces = new HashMap<>();

    public void display() {
        for (Map.Entry<Coordinates, ChessPieceUnit> entry : chessPieces.entrySet()) {
            System.out.println(entry.getKey() + "-->" + entry.getValue());

        }
    }

    /**
     * 放棋子
     *
     * @param coordinates    坐标
     * @param chessPieceUnit 棋子单位
     * @author huangmuhong
     * @date 2023/11/30
     * @since 1.0.0
     */
    public void putChessPiece(Coordinates coordinates, ChessPieceUnit chessPieceUnit) {
        chessPieces.put(coordinates, chessPieceUnit);
    }

    /**
     * 得到棋子
     *
     * @param coordinates 坐标
     * @return {@link ChessPieceUnit }
     * @author huangmuhong
     * @date 2023/11/30
     * @since 1.0.0
     */
    public ChessPieceUnit getChessPiece(Coordinates coordinates) {
        return chessPieces.get(coordinates);
    }

}

package xyz.xiaolinz.demo.enjoy;

/**
 * @author huangmuhong
 * @date 2023/11/30
 */
public class Main {
    public static void main(String[] args) {
        final Checkerboard checkerboard = new Checkerboard();
        final ChessFactory defaultChessFactory = new DefaultChessFactory();
        checkerboard.putChessPiece(new Coordinates(1, 1),
            defaultChessFactory.getChessPiece(1, ChessPieceUnit.Color.RED));
        checkerboard.putChessPiece(new Coordinates(1, 2),
            defaultChessFactory.getChessPiece(2, ChessPieceUnit.Color.RED));
        checkerboard.putChessPiece(new Coordinates(1, 3),
            defaultChessFactory.getChessPiece(3, ChessPieceUnit.Color.RED));
        checkerboard.putChessPiece(new Coordinates(1, 4),
            defaultChessFactory.getChessPiece(4, ChessPieceUnit.Color.RED));
        checkerboard.putChessPiece(new Coordinates(1, 5),
            defaultChessFactory.getChessPiece(5, ChessPieceUnit.Color.RED));
        checkerboard.putChessPiece(new Coordinates(2, 1),
            defaultChessFactory.getChessPiece(1, ChessPieceUnit.Color.BLACK));
        checkerboard.putChessPiece(new Coordinates(2, 2),
            defaultChessFactory.getChessPiece(2, ChessPieceUnit.Color.BLACK));
        checkerboard.putChessPiece(new Coordinates(2, 3),
            defaultChessFactory.getChessPiece(3, ChessPieceUnit.Color.BLACK));
        checkerboard.putChessPiece(new Coordinates(2, 4),
            defaultChessFactory.getChessPiece(4, ChessPieceUnit.Color.BLACK));
        checkerboard.putChessPiece(new Coordinates(2, 5),
            defaultChessFactory.getChessPiece(5, ChessPieceUnit.Color.BLACK));
        checkerboard.display();
    }
}

  • 结果
    image.png

享元模式和单例、缓存、池化的区别

在上面的讲解中,我们多次提到“共享”“缓存”“复用”这些字眼,那它跟单例、缓存、对象池这些概念有什么区别呢?我们来简单对比一下。

我们先来看享元模式跟单例的区别。

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。

我们前面也多次提到,区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

我们再来看享元模式跟缓存的区别。

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

最后我们来看享元模式跟对象池的区别。

对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?

虽然对象池、连接池、线程池、享元模式都是为了复用,但是,如果我们再细致地抠一抠“复用”这个字眼的话,对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。

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