The Beauty of Design Patterns 54-Flyweight Mode (Part 1): How to use Flyweight Mode to optimize the memory usage of text editors?

54 | Flyweight mode (Part 1): How to use Flyweight mode to optimize the memory usage of text editors?

In the last lesson, we talked about the composition mode. Combination mode is not commonly used, and it is mainly used in scenarios where data can be represented as a tree structure and can be solved by tree traversal algorithms. Today, let's learn a less commonly used pattern, Flyweight Design Pattern. This is the last structural pattern we will learn.

Similar to all other design patterns, the principle and implementation of Flyweight pattern are very simple. Today, I will explain through two practical examples of board games and text editors. In addition, I will also talk about its differences and connections with singletons, caches, and object pools. In the next lesson, I will take you to analyze the application of flyweight mode in Java Integer and String.

Without further ado, let's officially start today's study!

Flyweight mode principle and implementation

The so-called "flying yuan", as the name suggests, is a shared unit. The purpose of the flyweight pattern is to reuse objects and save memory, provided that the flyweight object is an immutable object.

Specifically, when there are a large number of repeated objects in a system, if these repeated objects are immutable objects, we can use the flyweight pattern to design the objects as flyweights, and only keep one instance in memory for multiple Code references. This can reduce the number of objects in memory and save memory. In fact, not only the same objects can be designed as flyweights, but for similar objects, we can also extract the same parts (fields) from these objects and design them as flyweights, so that a large number of similar objects can refer to these flyweights.

Here I will explain a little bit. The "immutable object" in the definition means that once the initialization is completed through the constructor, its state (member variables or properties of the object) will not be modified again. Therefore, immutable objects cannot expose any methods such as set() that modify the internal state. The reason why the flyweight is required to be an immutable object is because it will be shared and used by multiple codes, so as to prevent one piece of code from modifying the flyweight and affecting other codes that use it.

Next, we explain the flyweight mode through a simple example.

Suppose we are developing a board game (such as chess). There are thousands of "rooms" in a game hall, and each room corresponds to a chess game. The chess game should save the data of each chess piece, such as: chess piece type (general, phase, soldier, cannon, etc.), chess piece color (red square, black square), and the position of the chess piece in the game. Using this data, we can display a complete board to the player. The specific code is as follows. Among them, the ChessPiece class represents a chess piece, and the ChessBoard class represents a chess game, which stores the information of 30 chess pieces in chess.

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) {
    //...省略...
  }
}

In order to record the current chess situation in each room, we need to create a ChessBoard game object for each room. Because there are thousands of rooms in the game hall (in fact, there are many game halls with millions of people online at the same time), saving so many game objects will consume a lot of memory. Is there any way to save memory?

At this time, the flyweight mode can come in handy. Like the implementation just now, there will be a large number of similar objects in memory. The id, text, and color of these similar objects are all the same, except for positionX and positionY. In fact, we can separate the id, text, and color attributes of chess pieces, design them as independent classes, and use them as flyweights for multiple chessboards to reuse. In this way, the chessboard only needs to record the position information of each chess piece. The specific code implementation is as follows:

// 享元类
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, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }

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

In the above code implementation, we use the factory class to cache ChessPieceUnit information (that is, id, text, color). The ChessPieceUnit obtained through the factory class is the Flyweight. All ChessBoard objects share these 30 ChessPieceUnit objects (because there are only 30 pieces in chess). Before using the Flyweight mode, to record 10,000 chess games, we need to create a ChessPieceUnit object with 300,000 (30*10,000) pieces. Using the flyweight mode, we only need to create 30 flyweight objects for all chess games to share, which greatly saves memory.

Now that the principle of flyweight mode is finished, let's summarize its code structure. In fact, its code implementation is very simple, mainly through the factory mode. In the factory class, a Map is used to cache the created enjoyment objects to achieve the purpose of reuse.

Application of flyweight mode in text editor

After understanding the principle and implementation of the flyweight mode, let's look at another example, which is given in the title of the article: How to use the flyweight mode to optimize the memory usage of the text editor?

You can think of the text editor mentioned here as Word for Office. However, in order to simplify the requirement background, we assume that this text editor only implements text editing functions, and does not include complex editing functions such as pictures and tables. For the simplified text editor, if we want to represent a text file in memory, we only need to record two parts of information, the text and the format. The format includes information such as the font, size, and color of the text.

Although in actual document writing, we generally format text according to text type (title, body...), the title is one format, the body is another format, and so on. However, in theory, we can set a different format for each text in the text file. In order to achieve such a flexible format setting, and the code implementation is not too complicated, we treat each text as an independent object and include its formatting information in it. A specific code example is as follows:

public class Character {//文字
  private char c;

  private Font font;
  private int size;
  private int colorRGB;

  public Character(char c, Font font, int size, int colorRGB) {
    this.c = c;
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }
}

public class Editor {
  private List<Character> chars = new ArrayList<>();

  public void appendCharacter(char c, Font font, int size, int colorRGB) {
    Character character = new Character(c, font, size, colorRGB);
    chars.add(character);
  }
}

In the text editor, every time we type a text, we will call the appendCharacter() method in the Editor class to create a new Character object and save it in the chars array. If there are tens of thousands, hundreds of thousands, or hundreds of thousands of text in a text file, then we need to store so many Character objects in memory. Is there a way to save a little memory?

In fact, in a text file, not too many font formats are used. After all, it is unlikely that someone sets each text into a different format. Therefore, for the font format, we can design it as a flyweight, so that different texts can be shared and used. According to this design idea, we refactor the above code. The refactored code looks like this:

public class CharacterStyle {
  private Font font;
  private int size;
  private int colorRGB;

  public CharacterStyle(Font font, int size, int colorRGB) {
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }

  @Override
  public boolean equals(Object o) {
    CharacterStyle otherStyle = (CharacterStyle) o;
    return font.equals(otherStyle.font)
            && size == otherStyle.size
            && colorRGB == otherStyle.colorRGB;
  }
}

public class CharacterStyleFactory {
  private static final List<CharacterStyle> styles = new ArrayList<>();

  public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
    CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
    for (CharacterStyle style : styles) {
      if (style.equals(newStyle)) {
        return style;
      }
    }
    styles.add(newStyle);
    return newStyle;
  }
}

public class Character {
  private char c;
  private CharacterStyle style;

  public Character(char c, CharacterStyle style) {
    this.c = c;
    this.style = style;
  }
}

public class Editor {
  private List<Character> chars = new ArrayList<>();

  public void appendCharacter(char c, Font font, int size, int colorRGB) {
    Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
    chars.add(character);
  }
}

Flyweight mode vs singleton, cache, object pool

In the above explanation, we mentioned the words "sharing", "cache" and "reuse" many times, so what is the difference between them and the concepts of singleton, cache, and object pool? Let's make a simple comparison.

Let's first look at the difference between the flyweight pattern and the singleton.

In the singleton mode, a class can only create one object, while in the Flyweight mode, a class can create multiple objects, and each object is shared by multiple code references. In fact, the Flyweight pattern is somewhat similar to the variant of the singleton mentioned earlier: multiple instances.

We have also mentioned many times before that to distinguish between the two design patterns, we cannot just look at the code implementation, but look at the design intent, that is, the problem to be solved. Although there are many similarities between flyweight mode and multiple instances from the point of view of code implementation, they are completely different from the point of view of design intent. The Flyweight mode is used to reuse objects and save memory, while the multi-instance mode is used to limit the number of objects.

Let's look at the difference between flyweight mode and cache.

In the implementation of the Flyweight pattern, we use the factory class to "cache" the created objects. The "cache" here actually means "storage", which is different from what we usually call "database cache", "CPU cache" and "MemCache cache". The cache we usually talk about is mainly to improve access efficiency, not reuse.

Finally, let's look at the difference between flyweight mode and object pool.

Object pools, connection pools (such as database connection pools), thread pools, etc. are also for reuse, so what is the difference between them and the Flyweight mode?

You may be familiar with connection pools and thread pools, but unfamiliar with object pools, so here I briefly explain object pools. In a programming language like C++, memory management is the responsibility of the programmer. In order to avoid memory fragmentation caused by frequent object creation and release, we can pre-apply for a continuous memory space, which is the object pool mentioned here. Every time an object is created, we directly take out an idle object from the object pool for use. After the object is used, it is put back into the object pool for subsequent reuse instead of being released directly.

Although object pools, connection pools, thread pools, and Flyweight modes are all for reuse, if we take a closer look at the word "reuse", the pooling technologies such as object pools, connection pools, and thread pools The "reuse" of "reuse" and "reuse" in Flyweight mode are actually different concepts.

"Reuse" in pooling technology can be understood as "reuse", the main purpose is to save time (such as taking a connection from the database pool without recreating it). At any time, each object, connection, and thread will not be used in multiple places, but will be exclusively used by one user. After the use is completed, it will be returned to the pool and reused by other users. "Reuse" in Flyweight mode can be understood as "shared use", which is shared by all users throughout the life cycle, and the main purpose is to save space.

key review

Well, that's all for today's content. Let's summarize and review the content you need to master.

1. The principle of Flyweight mode

The so-called "flying yuan", as the name suggests, is a shared unit. The purpose of the flyweight pattern is to reuse objects and save memory, provided that the flyweight object is an immutable object. Specifically, when there are a large number of duplicate objects in a system, we can use the flyweight pattern to design objects as flyweights, and only keep one instance in memory for multiple code references, which can reduce memory The number of objects to save memory. In fact, not only the same objects can be designed as flyweights, but for similar objects, we can also extract the same parts (fields) from these objects and design them as flyweights, so that these large numbers of similar objects can refer to these flyweights.

2. Realization of flyweight mode

The code implementation of the enjoyment mode is very simple, mainly through the factory mode. In the factory class, a Map or List is used to cache the created enjoyment objects to achieve the purpose of reuse.

3. Flyweight mode VS singleton, cache, object pool

We have also mentioned many times before that to distinguish between the two design patterns, we cannot just look at the code implementation, but look at the design intent, that is, the problem to be solved. The difference here is no exception.

We can summarize the difference between them in a few words. The application of the singleton pattern is to ensure that the object is globally unique. The Flyweight mode is applied to realize object reuse and save memory. Caching is for improving access efficiency, not for multiplexing. "Reuse" in pooling technology is understood as "reuse", mainly to save time.

class disscussion

  1. In the example of chess and card games, is it necessary to design ChessPiecePosition as flyweight?
  2. In the example of a text editor, calling the getStyle() method of the CharacterStyleFactory class requires traversal and search in the styles array, and traversal search is time-consuming. Can it be optimized?

Welcome to leave a message and share your thoughts with me. If you gain something, you are welcome to share this article with your friends.

Guess you like

Origin blog.csdn.net/fegus/article/details/130498806