享元模式

时间:2023-01-09 10:11:20
title: 享元模式
tags: 设计模式
category: 结构型模式

如何利用享元模式优化文本编辑器的内存占用?

2023/1/8

定义

​ 享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。不可变指:一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。

原理

​ 当系统中存在大量"重复"(整体或者部分属性重复)的对象时,就可以利用享元模式,将“重复"对象设计成享元对象,在内存中只保留一份实例,供多处代码引用,以此减少内存中的对象数量,节省内存.若只是对象的部分字段重复,也可提取出来设计成享元对象。

实现

​ 主要是通过工厂模式,在工厂类中,通过一个Map或者List来缓存已经创建好的享元对象,以达到复用的目的。

实例

棋局游戏

​ 每把棋局有一个棋盘对象ChessBoard,一个棋盘对象ChessBoard存着30个棋子对象ChessPiece。如果有一万个棋局,就有30万个棋子对象ChessPiece,如何节省内存呢?

public class ChessPiece {//棋子
  private int id;
  private String text;
  private Color color;
  private int positionX;
  private int positionY;
  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) {
        //...省略...
    }
}

享元设计

​ 每个棋局的ChessPiece对象的id、text、color都是相同的,唯独positionX、positionY不同。可以将棋子的id、text、color属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。

// 享元类
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
  }
  // ...省略其他属性和getter方法...
}
//工厂类
public class ChessPieceUnitFactory {
private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
  static {
    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
    pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
    //...省略摆放其他棋子的代码...
  }
  public static ChessPieceUnit getChessPiece(int chessPieceId) {
    return pieces.get(chessPieceId);
  }
}
//棋子
public class ChessPiece {
  private ChessPieceUnit chessPieceUnit;
  private int positionX;
  private int positionY;
  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    this.chessPieceUnit = unit;
    this.positionX = positionX;
    this.positionY = positionY;
  }
  // 省略getter、setter方法
}
//棋盘
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(1(为啥是1?应该为2), new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }
  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

文本编辑器实例

​ 在文本编辑器中,我们每敲一个文字,都会调用Editor类中的appendCharacter()方法,创建一个新的Character对象,保存到chars数组中。如果一个文本文件中,有上万、十几万、几十万的文字,那我们就要在内存中存储这么多Character对象。那有没有办法可以节省一点内存呢?

​ 实际上,在一个文本文件中,用到的字体格式不会太多,毕竟不大可能有人把每个文字都设置成不同的格式。所以,对于字体格式,我们可以将它设计成享元,让不同的文字共享使用。

享元模式vs单例、缓存、对象池

单例模式区别

​ 单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。

缓存的区别

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

对象池的区别

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

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