享元模式这个名词可能不像其他设计模式一样直接见名知义,可能不像代理模式、单例模式那样一眼看到就知道是干什么的。接下来解释一下享元这个词的含义,享为共享的意思,元就是对象的意思。通俗得来讲享元模式就是共享对象的意思,这种模式一般在系统底层优化使用得比较多,比如前面说过的String类底层、线程池以及缓存池底层。

 翻译由来:FlyWeight 这个英文词汇直译更接近 "轻量级" 的含义,翻译为 "享元模式" 的原因可能是为了强调这种模式的核心思想,即共享和复用。设计模式的中文翻译有时候会采用更容易理解和贴近中文思维的术语,而 "享元模式" 在这方面表达得相对贴切。

享元模式是一种用于减少对象创建数量,从而降低内存占用,提高系统性能的设计模式。请结合具体应用场景,阐述享元模式的工作原理,并设计一个使用享元模式的简单例子。

享元模式的工作原理

享元模式是一种结构型设计模式,它通过将对象的可共享部分与不可共享部分分离,从而减少重复对象的数量,达到节省内存的目的。享元模式的核心思想是共享对象状态,在需要大量相似对象时,仅存储少量可复用的对象,其他状态通过外部管理。

享元模式主要由以下几个部分组成:

  1. Flyweight(抽象享元类):定义对象共享的接口,通常是一个轻量化接口。
  2. ConcreteFlyweight(具体享元类):实现抽象享元类,存储对象的共享部分。
  3. UnsharedConcreteFlyweight(非共享享元类):不需要共享的对象。
  4. FlyweightFactory(享元工厂类):管理享元对象的创建和共享。
  5. Client(客户端):负责维护外部状态,调用享元对象完成操作。

例子1

假设我们要开发一个网络围棋程序,允许多个玩家联机下棋。由于只有一台服务器,为了节省内存空间,我们可以采用享元模式来实现。软件公司开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能是Sunny公司开发人员需要解决的一个问题。为了解决这个问题,Sunny公司开发人员决定使用享元模式来设计该围棋软件的棋子对象,那么享元模式是如何实现节约内存进而提高系统性能的呢?

在这个例子中,棋子的颜色(黑或白)是可以共享的,而棋子的位置则是外部状态。因此,我们可以将棋子的颜色作为享元对象的共享部分,而棋子的位置则由客户端负责管理。

具体的类设计如下:

package com.ruoyi.test;

import java.util.*;

enum PieceColor {
    BLACK, WHITE
}
class PiecePos {
    private int x;
    private int y;

    public PiecePos(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}
abstract class Piece {
    protected PieceColor color;

    public Piece(PieceColor color) {
        this.color = color;
    }

    public abstract void display(PiecePos pos);
}
class ConcretePiece extends Piece {
    public ConcretePiece(PieceColor color) {
        super(color);
    }

    @Override
    public void display(PiecePos pos) {
        System.out.println("Piece [Color: " + color + "] at (" + pos.getX() + ", " + pos.getY() + ")");
    }
}
class PieceFactory {
    private Map<PieceColor, Piece> pieces = new HashMap<>();

    public Piece getPiece(PieceColor color) {
        if (!pieces.containsKey(color)) {
            pieces.put(color, new ConcretePiece(color));
        }
        return pieces.get(color);
    }
}
class Client {
    private PieceFactory pieceFactory;

    public Client(PieceFactory pieceFactory) {
        this.pieceFactory = pieceFactory;
    }

    public void displayPiece(PieceColor color, int x, int y) {
        Piece piece = pieceFactory.getPiece(color);
        PiecePos pos = new PiecePos(x, y);
        piece.display(pos);
    }
}
public class qq{
    public static void main(String[] args) {
        PieceFactory pieceFactory = new PieceFactory();
        Client client = new Client(pieceFactory);

        client.displayPiece(PieceColor.BLACK, 1, 1);
        client.displayPiece(PieceColor.WHITE, 2, 2);
        client.displayPiece(PieceColor.BLACK, 3, 3);
    }
}

java在这个例子中,我们使用了享元模式来创建和共享棋子对象。通过PieceFactory,我们确保了每种颜色的棋子只被创建一次,从而节省了内存空间。客户端通过调用displayPiece方法来显示棋子,并传入棋子的颜色和位置信息。

例子2联网类棋牌游戏

一个游戏厅中有成千上万个“房间”,每个房间对应一个象棋棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。

  假设有 1w 场象棋游戏在同时进行,不使用享元模式的话,系统需要维护 32*1w 个象棋对象。但象棋的ID、文案、颜色、规则是不变的,变的只是持有人和位置。所以将32个象棋对象抽象出来,当做享元(共享的对象),可以极大的节省空间,而且不会带来成本提升。

   享元模式与其说是一种设计模式,不如说是一种设计理念,主要讲的是抽象的能力,将相同模块提取出来,供不同模块使用。从这个维度来说,代码重构中提取相同功能、单例模式等,何尝不是另一种享元。

// 抽象享元类:棋子单元
public abstract class ChessPieceUnit {
    //棋子类,有ID、文案、颜色,这三种不变属性
    private int id;
    private String text;
    private String color;

    public ChessPieceUnit(int id, String text, String color) {
        this.id = id;
        this.text = text;
        this.color = color;
    }

    public abstract void display(int positionX, int positionY);

    // 省略其他方法和属性的实现

    public int getId() {
        return id;
    }

    public String getText() {
        return text;
    }

    public ChessPieceColor getColor() {
        return color;
    }
}

// 具体享元类:具体棋子单元
public class ConcreteChessPieceUnit extends ChessPieceUnit {
    public ConcreteChessPieceUnit(int id, String text, String color) {
        super(id, text, color);
    }

    @Override
    public void display(int positionX, int positionY) {
        System.out.println("棋子:" + getId() + " " + getColor() + " " + getText() + ",位置:" + positionX + ", " + positionY);
    }

    // 省略特有的方法和属性的实现
}

// 享元工厂类:棋子单元工厂
public class ChessPieceUnitFactory {
    private static final Map<Integer, ChessPieceUnit> chessPieces = new HashMap<>();
    // 通过工厂模式,在工厂类中,通过Map缓存已创建过的享元对象,达到复用
    static {
        chessPieces.put(1, new ConcreteChessPieceUnit(1, "将", "红"));
        chessPieces.put(2, new ConcreteChessPieceUnit(2, "兵", "黑");
        // 添加其他棋子
    }

    public static ChessPieceUnit getChessPiece(int chessPieceId) {
        return chessPieces.get(chessPieceId);
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        ChessPieceUnit redGeneral = ChessPieceUnitFactory.getChessPiece(1);
        redGeneral.display(0, 0);

        // 其他棋子的使用示例
        ChessPieceUnit blackPawn = ChessPieceUnitFactory.getChessPiece(2);
        blackPawn.display(1, 2);
    }
}

这种设计方式不仅提高了系统的性能,还降低了内存占用,是享元模式在实际应用中的一个典型例。

内部状态和外部状态


1)、享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态,既将对象的信息分为两部分:内部状态和外部状态。
2)、内部状态:指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变。
3)、外部状态:指对象得以依赖的一个标记,是随环境改变而改变的,不可共享的状态。

享元模式常见的应用场景:

**一、游戏开发领域**

1. **游戏角色和道具**
   - 在大型多人在线角色扮演游戏(MMORPG)中,有许多相同类型的角色和道具。例如,游戏中有大量的哥布林怪物,它们的外观、行为模式(如攻击方式、移动速度等基础属性)基本相同。如果为每个哥布林怪物都创建一个独立的对象,会占用大量的内存。
   - 利用享元模式,可以将哥布林怪物的通用属性(如外观纹理、基本行为脚本等)提取出来作为享元对象。当需要在游戏场景中出现哥布林怪物时,只需要共享这个享元对象,并为每个怪物实例赋予不同的位置、生命值等个体属性即可。这样就大大减少了对象的数量,提高了游戏的性能和内存利用率。

2. **游戏地图元素**
   - 游戏地图中的地形元素,如树木、石头等。假设一个游戏场景中有大片森林,森林中有成千上万棵树。如果每棵树都是一个独立的对象,会消耗巨大的内存资源。
   - 采用享元模式,可以将树的模型(包括纹理、几何形状等)作为享元对象。不同位置的树可以共享这个模型,只需要记录每棵树在地图中的位置、大小比例(可能因远近而有视觉上的大小差异)等少量可变信息。这样,在渲染游戏地图时,可以高效地利用内存,减少加载时间。

**二、图形用户界面(GUI)开发**

1. **窗口组件**
   - 在图形界面系统中,有许多按钮、文本框等组件。以按钮为例,在一个复杂的软件界面中可能有多个相同样式(如相同的颜色、形状、字体等)的按钮。
   - 通过享元模式,可以将按钮的外观样式(如背景颜色、边框样式、字体样式等)设计成享元对象。当需要创建新的按钮时,只要共享这个外观样式的享元对象,并为每个按钮设置不同的功能(如点击事件对应的操作)和位置即可。这有助于减少内存中相同样式信息的重复存储,优化界面渲染性能。

2. **图标系统**
   - 软件中的图标也是享元模式的适用场景。例如,一个办公软件可能有多个地方会用到“保存”图标。如果每个用到“保存”图标的地方都重新创建一个包含图标图像数据的对象,会浪费内存。
   - 利用享元模式,将“保存”图标等常用图标作为享元对象存储。不同的菜单选项、工具栏按钮等只要引用这个享元图标对象,并根据具体的布局需求设置图标大小、位置等属性,就可以高效地使用图标资源,避免内存浪费。

**三、文本处理和文档编辑**

1. **字符格式**
   - 在文字处理软件中,字符的格式(如字体、字号、颜色等)可以看作是享元对象。例如,一篇文档中有很多段落都使用了相同的字体和字号来显示文字。
   - 采用享元模式,可以将字体和字号等格式信息作为享元对象。当文档中的字符需要应用这些格式时,只需要引用相应的享元对象,而不需要为每个字符都单独存储格式信息。这样可以大大减少内存占用,尤其是在处理大型文档时,效果更加明显。

2. **样式模板**
   - 文档编辑软件通常有样式模板,如标题样式、正文样式等。这些样式模板可以作为享元对象。当用户在文档中应用标题样式时,实际上是共享了这个标题样式模板的享元对象,然后可以根据具体的标题内容进行个性化的修改,如修改文字内容,但格式(如字体加粗、居中对齐等)是共享的,从而提高了文档编辑的效率和内存使用效率。

享元模式(Flyweight Pattern)是一种结构型设计模式,它主要用于减少创建对象的数量,以降低内存占用和提高性能。以下是享元模式常见的应用场景:

**一、游戏开发领域**

1. **游戏角色和道具**
   - 在大型多人在线角色扮演游戏(MMORPG)中,有许多相同类型的角色和道具。例如,游戏中有大量的哥布林怪物,它们的外观、行为模式(如攻击方式、移动速度等基础属性)基本相同。如果为每个哥布林怪物都创建一个独立的对象,会占用大量的内存。
   - 利用享元模式,可以将哥布林怪物的通用属性(如外观纹理、基本行为脚本等)提取出来作为享元对象。当需要在游戏场景中出现哥布林怪物时,只需要共享这个享元对象,并为每个怪物实例赋予不同的位置、生命值等个体属性即可。这样就大大减少了对象的数量,提高了游戏的性能和内存利用率。

2. **游戏地图元素**
   - 游戏地图中的地形元素,如树木、石头等。假设一个游戏场景中有大片森林,森林中有成千上万棵树。如果每棵树都是一个独立的对象,会消耗巨大的内存资源。
   - 采用享元模式,可以将树的模型(包括纹理、几何形状等)作为享元对象。不同位置的树可以共享这个模型,只需要记录每棵树在地图中的位置、大小比例(可能因远近而有视觉上的大小差异)等少量可变信息。这样,在渲染游戏地图时,可以高效地利用内存,减少加载时间。

**二、图形用户界面(GUI)开发**

1. **窗口组件**
   - 在图形界面系统中,有许多按钮、文本框等组件。以按钮为例,在一个复杂的软件界面中可能有多个相同样式(如相同的颜色、形状、字体等)的按钮。
   - 通过享元模式,可以将按钮的外观样式(如背景颜色、边框样式、字体样式等)设计成享元对象。当需要创建新的按钮时,只要共享这个外观样式的享元对象,并为每个按钮设置不同的功能(如点击事件对应的操作)和位置即可。这有助于减少内存中相同样式信息的重复存储,优化界面渲染性能。

2. **图标系统**
   - 软件中的图标也是享元模式的适用场景。例如,一个办公软件可能有多个地方会用到“保存”图标。如果每个用到“保存”图标的地方都重新创建一个包含图标图像数据的对象,会浪费内存。
   - 利用享元模式,将“保存”图标等常用图标作为享元对象存储。不同的菜单选项、工具栏按钮等只要引用这个享元图标对象,并根据具体的布局需求设置图标大小、位置等属性,就可以高效地使用图标资源,避免内存浪费。

**三、文本处理和文档编辑**

1. **字符格式**
   - 在文字处理软件中,字符的格式(如字体、字号、颜色等)可以看作是享元对象。例如,一篇文档中有很多段落都使用了相同的字体和字号来显示文字。
   - 采用享元模式,可以将字体和字号等格式信息作为享元对象。当文档中的字符需要应用这些格式时,只需要引用相应的享元对象,而不需要为每个字符都单独存储格式信息。这样可以大大减少内存占用,尤其是在处理大型文档时,效果更加明显。

2. **样式模板**
   - 文档编辑软件通常有样式模板,如标题样式、正文样式等。这些样式模板可以作为享元对象。当用户在文档中应用标题样式时,实际上是共享了这个标题样式模板的享元对象,然后可以根据具体的标题内容进行个性化的修改,如修改文字内容,但格式(如字体加粗、居中对齐等)是共享的,从而提高了文档编辑的效率和内存使用效率。

更多推荐