学习设计模式——享元模式

625 阅读3分钟

今天我们来学习最后一个设计模式:享元模式

相对来说,享元模式的原理和实现也比较简单,并且在实际的项目开发中也不怎么常用。

概述

享元模式:(Flyweight Design Pattern)运用共享技术有效的支持大量细粒度的对象。

所谓享元,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。

何时使用:

  • 系统中有大量对象。
  • 这些对象消耗大量内存。
  • 这些对象的状态大部分可以外部化。
  • 这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
  • 系统不依赖于这些对象身份,这些对象是不可分辨的。

UML 类图:

image.png 角色组成:

  1. 抽象享元(Flyweight)角色: 是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。

  2. 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。

  3. 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。

  4. 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

实例解析

假设我们要开发一个象棋游戏。一个游戏厅中有成千上万个房间,每个房间对应一个棋局。棋局要保存每个棋子的数据:棋子类型棋子颜色棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋局给玩家。

此时,我们就可以使用享元模式来实现。具体代码如下:

享元类 ChessPieceUnit.java

public class ChessPieceUnit {
    private int id;
    private String text;
    private Color color;

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

    public static enum Color {
        RED, BLACK
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }
}

棋子 ChessPiece.java

public class ChessPiece {
    private ChessPieceUnit chessPieceUnit;
    private int positionX;
    private int positionY;

    public ChessPiece(ChessPieceUnit chessPieceUnit, int positionX, int positionY) {
        this.chessPieceUnit = chessPieceUnit;
        this.positionX = positionX;
        this.positionY = positionY;
    }

    public ChessPieceUnit getChessPieceUnit() {
        return chessPieceUnit;
    }

    public void setChessPieceUnit(ChessPieceUnit chessPieceUnit) {
        this.chessPieceUnit = chessPieceUnit;
    }

    public int getPositionX() {
        return positionX;
    }

    public void setPositionX(int positionX) {
        this.positionX = positionX;
    }

    public int getPositionY() {
        return positionY;
    }

    public void setPositionY(int positionY) {
        this.positionY = positionY;
    }
}

享元工厂 ChessPieceUnitFactory.java

public class ChessPieceUnitFactory {
    private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();

    static {
        pieces.put(1, new ChessPieceUnit(1,"車", ChessPieceUnit.Color.BLACK));
        pieces.put(1, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
        pieces.put(1, new ChessPieceUnit(3,"相", ChessPieceUnit.Color.BLACK));
        // 省略其他棋子代码。。。
    }

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

棋局 ChessBoard.java

public class ChessBoard {
    private Map<Integer, ChessPiece> chessPieces = new HashMap<>();

    public ChessBoard() {
        init();
    }

    private void init() {
        chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(1), 0, 0));
        chessPieces.put(2, new ChessPiece(ChessPieceUnitFactory.getChessPiece(2), 1, 0));
        chessPieces.put(3, new ChessPiece(ChessPieceUnitFactory.getChessPiece(3), 2, 0));
        // 省略其他棋子代码。。。
    }

    public void move(int chessPieceId, int toPositionX, int toPositionY) {
        // 省略。。。
    }
}

总结

优缺点

  • 优点: 大大减少对象的创建,降低系统的内存,使效率提高。

  • 缺点: 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

应用

  • String 常量池、数据库连接池、缓冲池等等。
  • Java Integer 类中、String 类的 字符串常量池


实际上,享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此,在某些情况下,如果对象的生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。所以,除非经过线上验证,利用享元模式真的可以大大节省内存,否则,就不要过度使用这个模式,为了一点点内存的节省而引入一个复杂的设计模式,得不偿失。