Nhẹ tựa lông hồng!

Nếu chúng ta đang lập trình ở server side thì việc khởi tạo, sử dụng và giải phóng đối tượng là điều chúng ta vẫn thường xuyên làm, vì ở môi trường server các đối tượng được khởi tạo cũng khá nhẹ nhàng, thêm nữa là server phục vụ hàng triệu, hàng tỉ request nên chúng ta cần nhanh chóng giải phóng tài nguyên để đảm bảo sự sống còn của server. Nhưng ở phía client thì sao? client là nơi chúng ta là việc trực tiếp với các đối tượng graphics nặng nề, là nơi chúng ta cần hiển thị mượt mà nhất để tăng trải nghiệm người dùng đặc biệt trong lĩnh vực game, chúng ta cần phải xử lý khéo léo để game không bị giật lag. Thật may cho chúng ta là số lượng đối tượng ở client là tương đối ít, căng lắm cũng chỉ có vài trăm đối tượng, và may mắn hơn nữa chúng ta có Flyweight design pattern!

Bài toán thực tế

Hãy tưởng tượng bạn đang làm một game bắn quái vật, vậy game của bạn sẽ cần những đối tượng sau:

  1. Phi thuyền đại diện cho bạn
  2. Các con quái vật
  3. Đạn bắn ra
  4. Các hiệu ứng nổ

Câu hỏi đặt ra là:

  1. Làm thế nào để game không bị giật đùng đùng mỗi khi sinh ra quái vật hay đạn bắn ra rồi nổ tung? Chúng ta đều biết rằng điểm tắc nghẽn nằm ở việc khởi tạo một đối tượng trong game, có thể hiểu đơn giản là việc load 1 cái ảnh từ vài KB đến vài trăm KB vào bộ nhớ và việc này tương đối mất thời gian vậy cách giải quyết đơn giản nhất là khi khởi tạo game, hãy tạo sẵn ra hết các đối tượng 1 lần và quản lý chúng nó, đừng khởi tạo và khi cần phải ẩn đối tượng đi thì có thể đơn giản là set 1 vị trí ngoài màn hình hoặc set visible = false.
  2. Tất cả các con quái vật, đạn, hay hiệu ứng nổ đều giống nhau thì làm sao để sử dụng chung được tài nguyên mà không phải duplicate lên?
  3. Làm thế nào để quản lý được tất cả các đối tượng này? Ý là làm sao để có thể biết đâu là phi thuyền, đâu là quái vật, đâu là đạn và đâu là hiệu ứng nổ để lấy ra khi cần thiết? Cách giải quyết là hãy quản lý các đối tượng này theo type (kiểu đối tượng) và id hoặc index của đối tượng.

Mục tiêu ra đời

Như vậy chúng ta có thể thấy Flyweight pattern ra đời nhằm 1 đích:

  1. Khởi tạo các đối tượng 1 lần duy nhất và sử dụng lâu dài để tăng hiệu năng của chương trình trong quá trình chạy
  2. Tái sử dụng lại các đối tượng khi cần thiết
  3. Quản lý và chia sẻ thông tin của các đối tượng, tạo thuận lợi cho việc truy xuất thông tin đối tượng ở bất cứ đâu
  4. Tiết kiệm được bộ nhớ, ví dụ 2 đối tượng sử dụng chung 1 cái ảnh, thì chỉ cần load 1 lần cái ảnh và 2 đối tượng sẽ trỏ chung vào vùng nhớ chứa cái ảnh đó mà thôi

Đổi bộ nhớ lấy hiệu năng

Chúng ta có thể thấy cái giá Flyweight pattern phải trả đó là chi phí bộ nhớ ban đầu, nhưng rất may, với dung lượng RAM hàng GB cho các thiết bị như bây giờ, nó không còn là vấn đề quá lớn nữa. Tuy nhiên trong một số trường hợp nếu chúng ta tính toán số lượng đối tượng được dư thừa thì trong quá trình chạy, sẽ có nhiều trường hợp mà đối tượng được tạo ra không sử dụng đến, đó là một sự lãng phí, vậy chúng ta hãy chỉ tạo ra vừa đủ số lượng đối tượng để vừa tiết kiệm được RAM vừa tăng được trải nghiệm người dùng nhé.

Ví dụ

Các đối tượng của Flyweight pattern thường được tạo bởi Factory và được đưa vào Singleton để quản lý. Flyweight pattern cũng thường được sử dụng cùng với Strategy pattern (một design pattern siêu hay, mà mình cảm thấy thú vị nhất trong tất cả các pattern). Ví dụ:

// đối tượng Flyweight
class GameObject {
    public int id;
    public GameObjectType type;
    void update(float dt) {}
    void setPosition(int x, int y, int z) {}
}
class Ship extends GameObject {}
class Bullet extends GameObject {}
class Monster extends GameObject {}
class Explosion extends GameObject {}
enum GameObjectType {
    SHIP,
    BULLET,
    MONSTER,
    EXPLOSION
}
class GameObjectFactory {
    public GameObject newGameObject(GameObjectType type) {
        // logic khởi tạo
    }
}
class GameObjectManager {
    private final Map<GameObjectType, Map<Integer, GameObject>> gameObjects;
    private final static GameObjectManager INSTANCE = new GameObjectManager();
    private GameObjectManager() {
        this.gameObjects = new HashMap<>();
    }
    public void addGameObject(GameObject gameObject) {
        this.gameObjects.computeIfAbsent(gameObject.type, k -> new HashMap<>())
                        .put(gameObject.id, gameObject);
    }
    public <T> T getGameObject(GameObjectType type, int id) {
        return (T)this.gameObjects.get(type).get(id);
    }
    public static GameObjectManager getInstance() {
        return INSTANCE;
    }
}
class GameStartup {
    public static void main(String[] args) {
        GameObjectFactory gameObjectFactory = new GameObjectFactory();
        GameObjectManager gameObjectManager = GameObjectManager.getInstance();
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.SHIP));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.BULLET));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.BULLET));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.MONSTER));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.MONSTER));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.MONSTER));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.MONSTER));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.MONSTER));
        gameObjectManager.addGameObject(gameObjectFactory.newGameObject(GameObjectType.EXPLOSION));
    }
}
class GameController {
    public void start() {
        Ship ship = GameObjectManager.getInstance().getGameObject(GameObjectType.SHIP, 0);
        ship.setPosition(1, 2, 3);
    }
}

Flyweight cũng có phần nào đó giống với Object Pool pattern, mình sẽ viết ở bài sau nhé.

Tham khảo:

  1. Lý thuyết
  2. Example