ลดข้อมูลซ้ำด้วยการให้หลายออบเจ็กต์ใช้ข้อมูลส่วนกลางร่วมกัน โดยแยกข้อมูลเฉพาะของแต่ละออบเจ็กต์ออกอย่างชัดเจน

หมวดหมู่: Structural Design Pattern
กลุ่มผู้อ่าน: นักศึกษาและผู้พัฒนา Java ที่เข้าใจคลาส อินเทอร์เฟซ และ Collections

#1. Flyweight Pattern คืออะไร

info-flyweight-pattern-with-java

Flyweight เป็นรูปแบบการออกแบบที่ช่วยลดหน่วยความจำเมื่อระบบต้องจัดการออบเจ็กต์จำนวนมาก และออบเจ็กต์เหล่านั้นมีข้อมูลบางส่วนเหมือนกัน

แทนที่จะเก็บข้อมูลที่ซ้ำกันไว้ในทุกออบเจ็กต์ เราแยกข้อมูลส่วนนั้นเป็นออบเจ็กต์ที่ใช้ร่วมกัน แล้วให้ออบเจ็กต์แต่ละตัวเก็บเพียงข้อมูลเฉพาะของตนเองและ reference ไปยังข้อมูลส่วนกลาง

ตัวอย่างเช่น เกมมีต้นไม้ 10,000 ต้น แต่มีชนิดต้นไม้เพียง 3 รูปแบบ ข้อมูลชนิด สี และ texture สามารถใช้ร่วมกันได้ ส่วนตำแหน่งของต้นไม้ต้องแยกกัน

ต้นไม้ยังคงมี 10,000 ต้น แต่ข้อมูลชนิดต้นไม้ที่ใช้ร่วมกันมีเพียง 3 ชุด การใช้ Flyweight จึงไม่ได้หมายความว่าระบบจะเหลือออบเจ็กต์ทั้งหมดเพียง 3 ตัว

Flyweight อยู่ในกลุ่ม Structural Patterns ของหนังสือ Design Patterns: Elements of Reusable Object-Oriented Software [1]

#2. ปัญหาก่อนใช้ Flyweight

พิจารณาการออกแบบที่ให้ต้นไม้ทุกต้นมีข้อมูลภาพของตนเอง

final class TreeWithoutSharing {
    private final int x;
    private final int y;
    private final String species;
    private final String color;
    private final byte[] textureData;

    TreeWithoutSharing(int x, int y, String species, String color) {
        this.x = x;
        this.y = y;
        this.species = species;
        this.color = color;
        this.textureData = new byte[4 * 1024];
    }
}

โค้ดนี้จำลอง texture ด้วยอาร์เรย์ขนาด 4 KiB และสร้างอาร์เรย์ใหม่ทุกครั้งที่สร้างต้นไม้ หากสร้าง 10,000 ต้น จะมีอาร์เรย์ texture 10,000 ชุด แม้ต้นไม้หลายต้นจะใช้ภาพเดียวกันก็ตาม

จุดที่ทำให้เกิดการทำสำเนาในตัวอย่างคือ new byte[...] ทุกครั้ง การมีฟิลด์ชนิด reference เหมือนกันไม่ได้แปลว่า Java คัดลอกออบเจ็กต์ปลายทางให้เอง ถ้าระบบใช้ texture instance เดียวกันอยู่แล้ว ก็อาจมีการแชร์ทรัพยากรส่วนนั้นอยู่ก่อน

#3. แยก Intrinsic State และ Extrinsic State

หัวใจของ Flyweight คือการแยกสถานะสองกลุ่ม

ประเภทสถานะ ความหมาย ตัวอย่างในระบบต้นไม้ เก็บที่ใด
Intrinsic State ข้อมูลที่ไม่ขึ้นกับบริบทของต้นไม้แต่ละต้นและสามารถใช้ร่วมกันได้ ชนิด สี texture TreeType
Extrinsic State ข้อมูลเฉพาะของการใช้งานแต่ละครั้งหรือแต่ละต้น พิกัด x, y Tree และส่งให้ draw()

ในตัวอย่างนี้กำหนดให้ต้นไม้ที่มีชนิด สี และ texture เดียวกันสามารถใช้ TreeType instance เดียวกันได้ ข้อมูลที่แชร์ออกแบบให้ไม่เปลี่ยนหลังสร้าง

หากเกมต้องการให้ต้นไม้แต่ละต้นมีพลังชีวิต มุมหมุน หรือขนาดต่างกัน ข้อมูลเหล่านั้นควรอยู่ใน Tree เช่นเดียวกับพิกัด หากสีเป็นคุณสมบัติเฉพาะแต่ละต้น ก็ควรย้ายสีออกจาก TreeType ด้วย การจัดกลุ่มจึงขึ้นกับข้อกำหนดของระบบ

#4. องค์ประกอบของ Pattern

บทบาท คลาสในตัวอย่าง หน้าที่
Flyweight TreeAppearance กำหนดการทำงาน draw(x, y)
Concrete Flyweight TreeType เก็บข้อมูลที่ใช้ร่วมกันและทำงานร่วมกับพิกัดที่ได้รับ
Flyweight Factory TreeFactory คืน instance เดิมเมื่อ key เหมือนกัน และสร้างใหม่เมื่อยังไม่มี
Context Tree เก็บพิกัดและ reference ไปยัง TreeType
Client Forest และ main() สร้างต้นไม้ ขอชนิดต้นไม้จาก factory และเรียกวาด

TypeKey เป็นตัวช่วยค้นหา ประกอบด้วยข้อมูล intrinsic ทุกส่วนที่มีผลต่อรูปแบบต้นไม้ ตัว factory ใช้ key นี้เป็นกุญแจใน Map

classDiagram
    class TreeAppearance {
        <<interface>>
        +draw(int x, int y) void
    }
    class TreeType {
        -TypeKey key
        -byte[] textureData
        +draw(int x, int y) void
    }
    class TreeFactory {
        -Map cache
        +getType(TypeKey key) TreeType
    }
    class Tree {
        -int x
        -int y
        -TreeType type
        +draw() void
    }
    class Forest {
        -List trees
        +plant(int x, int y, TypeKey key) void
    }
    TreeAppearance <|.. TreeType
    TreeFactory --> TreeType : creates and reuses
    Tree --> TreeType : shares
    Forest *-- Tree : contains
    Forest --> TreeFactory : requests type

ไดอะแกรมแสดงโครงสร้างหลัก โดยละรายละเอียด TypeKey เพื่อให้อ่านง่าย ความสัมพันธ์ Tree ไปยัง TreeType เป็น reference ที่หลายต้นใช้ปลายทางร่วมกันได้

#5. Workshop: สร้างป่า 10,000 ต้นด้วย Java

ตัวอย่างนี้เป็นโปรแกรม console ไฟล์เดียว ใช้ข้อมูล texture จำลองขนาด 4 KiB ต่อชนิด และพิมพ์คำสั่งวาดออกทางหน้าจอ จึงไม่ต้องมีไฟล์รูปหรือ GUI

สร้างไฟล์ชื่อ FlyweightDemo.java แล้วใส่โค้ดต่อไปนี้

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class FlyweightDemo {

    // Includes every intrinsic attribute that identifies a tree type.
    record TypeKey(String species, String color, String textureAsset) {
        TypeKey {
            Objects.requireNonNull(species, "species");
            Objects.requireNonNull(color, "color");
            Objects.requireNonNull(textureAsset, "textureAsset");
        }
    }

    interface TreeAppearance {
        void draw(int x, int y);
    }

    static final class TreeType implements TreeAppearance {
        static final int TEXTURE_BYTES = 4 * 1024;

        private final TypeKey key;
        private final byte[] textureData;

        private TreeType(TypeKey key) {
            this.key = Objects.requireNonNull(key, "key");
            // Simulation only: a real application would load the asset here.
            this.textureData = new byte[TEXTURE_BYTES];
        }

        @Override
        public void draw(int x, int y) {
            System.out.printf(
                "Draw %s color=%s texture=%s bytes=%d at (%d, %d)%n",
                key.species(), key.color(), key.textureAsset(),
                textureData.length, x, y
            );
        }
    }

    // This factory is intended for single-threaded use.
    static final class TreeFactory {
        private final Map<TypeKey, TreeType> cache = new HashMap<>();

        TreeType getType(TypeKey key) {
            Objects.requireNonNull(key, "key");
            return cache.computeIfAbsent(key, TreeType::new);
        }

        int typeCount() {
            return cache.size();
        }

        long texturePayloadBytes() {
            return (long) cache.size() * TreeType.TEXTURE_BYTES;
        }
    }

    // Each tree keeps its own position and a shared type reference.
    record Tree(int x, int y, TreeType type) {
        Tree {
            Objects.requireNonNull(type, "type");
        }

        void draw() {
            type.draw(x, y);
        }
    }

    static final class Forest {
        private final TreeFactory factory;
        private final List<Tree> trees = new ArrayList<>();

        Forest(TreeFactory factory) {
            this.factory = Objects.requireNonNull(factory, "factory");
        }

        void plant(int x, int y, TypeKey key) {
            trees.add(new Tree(x, y, factory.getType(key)));
        }

        int treeCount() {
            return trees.size();
        }

        void drawFirst(int limit) {
            for (int i = 0; i < Math.min(limit, trees.size()); i++) {
                trees.get(i).draw();
            }
        }
    }

    public static void main(String[] args) {
        TreeFactory factory = new TreeFactory();
        Forest forest = new Forest(factory);

        TypeKey[] types = {
            new TypeKey("Pine", "#2E7D32", "pine.png"),
            new TypeKey("Oak", "#558B2F", "oak.png"),
            new TypeKey("Cherry", "#EC407A", "cherry.png")
        };

        for (int i = 0; i < 10_000; i++) {
            forest.plant(i % 100, i / 100, types[i % types.length]);
        }

        // Equal-valued keys must retrieve the exact same instance.
        TreeType first = factory.getType(types[0]);
        TreeType second = factory.getType(
            new TypeKey("Pine", "#2E7D32", "pine.png")
        );
        TreeType other = factory.getType(types[1]);

        System.out.println("Logical trees: " + forest.treeCount());
        System.out.println("Shared tree types: " + factory.typeCount());
        System.out.println("Same key shares instance: " + (first == second));
        System.out.println("Different key has different instance: " + (first != other));
        System.out.println("Texture payload bytes: " + factory.texturePayloadBytes());
        forest.drawFirst(3);
    }
}

#วิธีรัน

ตรวจสอบว่าใช้ JDK 17 ขึ้นไป

java -version

รันจาก source file โดยตรง

java FlyweightDemo.java

หรือ compile และรันแบบแยกขั้นตอน

javac --release 17 FlyweightDemo.java
java FlyweightDemo

#ผลลัพธ์

Logical trees: 10000
Shared tree types: 3
Same key shares instance: true
Different key has different instance: true
Texture payload bytes: 12288
Draw Pine color=#2E7D32 texture=pine.png bytes=4096 at (0, 0)
Draw Oak color=#558B2F texture=oak.png bytes=4096 at (1, 0)
Draw Cherry color=#EC407A texture=cherry.png bytes=4096 at (2, 0)

โปรแกรมสร้าง Tree 10,000 instance แต่สร้าง TreeType และอาร์เรย์ texture อย่างละ 3 instance ตัวเลข 12288 เป็นผลรวมความยาวอาร์เรย์ texture ไม่ใช่หน่วยความจำรวมของโปรแกรม

#6. อ่านโค้ดส่วนสำคัญ

#Factory เป็นจุดรวมการใช้ซ้ำ

return cache.computeIfAbsent(key, TreeType::new);

computeIfAbsent() จะคืนค่าที่มีอยู่เมื่อพบ key และคำนวณค่าใหม่เมื่อยังไม่มีค่าที่ใช้ได้ ในตัวอย่าง constructor คืนออบเจ็กต์ที่ไม่เป็น null จึงบันทึก TreeType ที่สร้างลงใน map [2]

เมื่อขอชนิด Pine ด้วย key เดิม factory จะคืน TreeType เดิม ต้นไม้ที่ตำแหน่งใหม่จึงไม่ต้องสร้าง texture ซ้ำ

#Key ต้องครอบคลุม Intrinsic State

TypeKey เป็น record ซึ่งสร้าง equals() และ hashCode() ตามค่าของ component ให้เหมาะกับการเป็น map key [4] กรณีนี้ component ทั้งหมดเป็น String ที่เปลี่ยนค่าไม่ได้ จึงไม่เกิดปัญหา key เปลี่ยนสถานะหลังใส่ map

หากใช้เฉพาะ species เป็น key ต้นไม้ Pine สีเขียวกับ Pine สีเหลืองอาจถูกจับให้ใช้ข้อมูลชุดเดียวกัน ทั้งที่มีรูปแบบต่างกัน การเพิ่มคุณสมบัติ intrinsic ใหม่ต้องทบทวน key ด้วย

#Context ส่งข้อมูลเฉพาะเข้าไปตอนใช้งาน

void draw() {
    type.draw(x, y);
}

TreeType ไม่เก็บตำแหน่งไว้ภายใน จึงรับใช้ต้นไม้หลายต้นที่อยู่คนละพิกัดได้ โดยไม่เกิดการเขียนทับพิกัดร่วมกัน

#ข้อมูลที่แชร์ต้องไม่ถูกเปลี่ยนโดยผู้ใช้

TreeType ไม่มี setter ไม่ส่งอาร์เรย์ภายในออกไป และไม่มีเมธอดแก้ไขอาร์เรย์หลังสร้าง แม้ byte[] จะเป็นชนิดที่เปลี่ยนค่าได้ แต่ตัวอย่างควบคุมไม่ให้ reference หลุดออกไป และไม่แก้ไขข้อมูลนั้น

เพียงประกาศฟิลด์เป็น final ยังไม่ทำให้ออบเจ็กต์ภายใน immutable โดยอัตโนมัติ หากระบบจริงรับอาร์เรย์จากภายนอกหรือคืนข้อมูลให้ผู้เรียก ควรใช้ defensive copy หรือ API ที่ป้องกันการแก้ไขตามความเหมาะสม

#7. วิเคราะห์หน่วยความจำอย่างมีขอบเขต

กำหนดให้

  • N คือจำนวนต้นไม้ทั้งหมด
  • K คือจำนวนรูปแบบต้นไม้ที่ไม่ซ้ำ
  • S คือขนาดข้อมูล intrinsic หนึ่งชุด
  • E คือขนาดข้อมูล extrinsic หนึ่งต้น
  • R คือขนาด reference ไปยัง flyweight
  • F คือ overhead รวมของ factory และโครงสร้างค้นหา

แบบจำลองเชิงแนวคิดที่ยังไม่รวมรายละเอียด object header และ alignment ทุกส่วนคือ

ไม่แชร์ข้อมูล:    N × (S + E)
ใช้ Flyweight:  K × S + N × (E + R) + F

โอกาสประหยัดจะสูงเมื่อ K น้อยกว่า N มาก และ S มีขนาดใหญ่พอที่จะชดเชย reference และ overhead ของ factory

สำหรับ เฉพาะ payload ของ texture จำลอง ใน workshop

รายการ สร้าง texture แยกทุกต้น ใช้ Flyweight
จำนวนต้นไม้ 10,000 10,000
จำนวนอาร์เรย์ texture 10,000 3
ขนาดต่ออาร์เรย์ 4,096 bytes 4,096 bytes
ผลรวม payload 40,960,000 bytes 12,288 bytes
ผลรวม payload หน่วย MiB 39.0625 MiB ประมาณ 0.0117 MiB

ตัวเลขนี้เป็นการคำนวณ payload ไม่ใช่ผล benchmark heap ของ JVM ยังมีหน่วยความจำของ Tree, TreeType, key, map, list, reference และส่วนประกอบอื่น การประเมินระบบจริงควรตรวจ heap และ allocation ด้วย profiler หรือ Java Flight Recorder [5] ภายใต้ workload เดียวกัน

จำนวน context ยังคงเพิ่มตาม N ดังนั้นหน่วยความจำรวมของระบบนี้ยังเป็น O(N + K) เมื่อขนาดข้อมูลต่อรายการคงที่ ประโยชน์หลักคือการลดข้อมูลขนาดใหญ่ที่ซ้ำกัน

#8. เมื่อใช้งานพร้อมกันหลาย Thread

factory หลักใช้ HashMap และตั้งใจให้ตัวอย่างทำงานใน thread เดียว HashMap ไม่ได้ทำ synchronization ให้โดยอัตโนมัติ [2]

ถ้าหลาย thread ต้องเรียก factory เดียวกัน สามารถใช้ factory แบบนี้แทน

static final class ConcurrentTreeFactory {
    private final java.util.concurrent.ConcurrentHashMap<TypeKey, TreeType> cache =
        new java.util.concurrent.ConcurrentHashMap<>();

    TreeType getType(TypeKey key) {
        Objects.requireNonNull(key, "key");
        return cache.computeIfAbsent(key, TreeType::new);
    }
}

วางคลาสนี้ภายใน FlyweightDemo เช่นเดียวกับคลาสอื่น ConcurrentHashMap.computeIfAbsent() ทำขั้นตอนดังกล่าวแบบ atomic แต่ mapping function ควรสั้น และไม่แก้ไข mapping อื่นใน map เดียวกันระหว่างคำนวณ [3]

ตัวอย่างนี้สร้างอาร์เรย์ขนาดเล็กใน constructor หากระบบจริงต้องโหลด texture จากดิสก์หรือเครือข่าย ควรออกแบบขั้นตอนโหลดล่วงหน้าหรือประสานงานแบบอื่น เพื่อไม่ให้การโหลดที่ช้ากลายเป็นจุดคอขวด

การเปลี่ยน map ทำให้ factory นี้รองรับการเรียกพร้อมกัน แต่ ไม่ได้ทำให้ Forest และ ArrayList ในตัวอย่างรองรับหลาย thread ไปด้วย และยังต้องรักษาข้อมูลที่แชร์ให้ปลอดภัยต่อการอ่านร่วมกัน

#9. อายุของ Factory และ Cache

ตัวอย่างใช้ factory เป็น instance แล้วส่งเข้า Forest เพื่อให้ควบคุมขอบเขตการแชร์ได้ เช่น สร้างหนึ่ง factory ต่อฉากเกม เมื่อไม่มีใครอ้างอิง factory, forest และต้นไม้เหล่านั้นอีก ข้อมูลก็มีโอกาสถูกเก็บคืนโดย GC

ไม่จำเป็นต้องทำ factory เป็น Singleton หรือใช้ static Map หากไม่มีความต้องการแชร์ข้ามทั้งโปรแกรม

กรณีชนิดข้อมูลเพิ่มได้ไม่จำกัด map อาจเก็บ flyweight มากขึ้นเรื่อย ๆ ควรพิจารณาขอบเขตการใช้งาน ขนาดสูงสุด หรือการล้างเมื่อจบงานตามลักษณะระบบ

การนำ entry ออกจาก cache ไม่ทำให้ออบเจ็กต์ที่ context ยังอ้างอิงอยู่ถูกทำลายทันที และถ้ามีการขอ key นั้นใหม่ factory อาจสร้างอีก instance ได้ จึงไม่ควรผูกตรรกะธุรกิจกับ == เมื่อระบบมี eviction หรือมีหลาย factory

#10. ใช้เมื่อใด และเมื่อใดควรออกแบบให้เรียบง่ายกว่า

สถานการณ์ แนวทางพิจารณา
มีออบเจ็กต์จำนวนมากและข้อมูลขนาดใหญ่ซ้ำกัน เป็นกรณีที่เหมาะกับ Flyweight
มีรูปแบบที่ไม่ซ้ำเพียงเล็กน้อยเมื่อเทียบกับจำนวน context มีโอกาสแชร์ได้คุ้มค่า
ข้อมูลที่แชร์คงที่และแยกจากข้อมูลเฉพาะได้ชัดเจน ออกแบบและตรวจสอบได้ง่าย
ทุกออบเจ็กต์มีข้อมูลแตกต่างกันเกือบทั้งหมด factory และ reference อาจเพิ่ม overhead มากกว่าประโยชน์
ต้องเปลี่ยนข้อมูลร่วมตามแต่ละ context ตลอดเวลา ทบทวนการแบ่ง intrinsic และ extrinsic ใหม่
ระบบมีขนาดเล็กและไม่พบปัญหาหน่วยความจำ ใช้โครงสร้างปกติและวัดก่อนเพิ่มความซับซ้อน

ตัวอย่างการประยุกต์ ได้แก่ รูปแบบตัวอักษรใน text editor, ข้อมูล tile ในแผนที่, mesh หรือ material ของวัตถุในเกม และชุดสไตล์การแสดงผลที่ใช้ซ้ำ โดยรายละเอียดที่เปลี่ยนตามแต่ละตำแหน่งยังคงอยู่ที่ context

#11. เปรียบเทียบกับแนวคิดที่ใกล้เคียง

แนวคิด จุดมุ่งหมาย ความสัมพันธ์กับ Flyweight
Flyweight ลดข้อมูลซ้ำด้วยการแชร์ intrinsic state มีหลาย flyweight ตามรูปแบบที่แตกต่างกัน
Singleton กำหนด instance เดียวในขอบเขตที่ออกแบบ ไม่ได้กำหนดให้ต้องแยก intrinsic/extrinsic
Object Pool นำทรัพยากรกลับมาใช้หลังผู้ใช้คืน โดยมักมีขั้นตอนยืมและคืน Flyweight ที่แชร์ได้สามารถถูกอ่านโดยหลาย context พร้อมกัน โดยไม่ต้องยืมคืนแบบ exclusive
Cache เก็บผลลัพธ์หรือทรัพยากรเพื่อใช้ซ้ำ factory อาจใช้ cache เป็นกลไก แต่ Flyweight เน้นโครงสร้างข้อมูลที่แชร์กับ context
Prototype สร้างออบเจ็กต์จากการคัดลอกต้นแบบ การคัดลอกอาจสร้าง state เพิ่ม ส่วน Flyweight ตั้งใจให้ใช้ state ชุดเดิมร่วมกัน

#12. ข้อผิดพลาดที่พบบ่อย

  1. เก็บตำแหน่งไว้ใน flyweight: การย้ายต้นไม้หนึ่งต้นอาจกระทบต้นอื่นที่แชร์ instance เดียวกัน
  2. สร้าง new TreeType(...) โดยข้าม factory: ข้อมูลที่ควรแชร์อาจถูกสร้างซ้ำ constructor ของตัวอย่างจึงเป็น private
  3. ใช้ key ไม่ครบ: ชนิดเดียวกันแต่ texture หรือสีต่างกันอาจถูกแชร์ผิดชุด
  4. ส่ง mutable data ภายในให้ผู้เรียก: ผู้ใช้รายหนึ่งอาจเปลี่ยนข้อมูลที่ทุก context มองเห็น
  5. เชื่อว่ามี map แล้วจะใช้หลาย thread ได้: ต้องพิจารณาการค้นหาและสร้าง instance ให้ครบทั้งขั้นตอน
  6. รายงานขนาดอาร์เรย์เป็นขนาด heap รวม: ต้องแยก payload ที่คำนวณออกจากหน่วยความจำจริงที่วัดได้
  7. ใช้ identity เป็นเงื่อนไขทางธุรกิจ: == ใน workshop ใช้ตรวจการแชร์ภายใน factory นี้ ส่วนการเปรียบเทียบค่าควรใช้ equals() ตามความหมายของข้อมูล

#13. แบบฝึกหัด

#แบบฝึกหัดที่ 1: เพิ่มรูปแบบที่ไม่ซ้ำ

เพิ่ม new TypeKey("Pine", "#F9A825", "pine.png") ในอาร์เรย์ types แล้วรันอีกครั้ง จำนวนต้นไม้ต้องยังเป็น 10,000 แต่จำนวนชนิดที่แชร์ต้องเป็น 4

#แบบฝึกหัดที่ 2: เพิ่มข้อมูลเฉพาะต้น

เพิ่ม rotation ใน Tree และส่งค่านี้เข้าเมธอด draw() โดยไม่ใส่ใน TypeKey ตรวจสอบว่าการเพิ่มมุมหมุนไม่ได้เพิ่มจำนวน TreeType

#แบบฝึกหัดที่ 3: ตรวจความถูกต้องของ Key

เรียก factory ด้วย key ที่มีค่าตรงกันแต่สร้างเป็นคนละ record instance แล้วตรวจด้วย == ว่าได้ TreeType instance เดียวกัน จากนั้นเปลี่ยนเฉพาะ textureAsset และตรวจว่าได้คนละ instance

#แบบฝึกหัดที่ 4: เปรียบเทียบด้วยเครื่องมือวัด

สร้างโปรแกรมสองแบบที่เก็บต้นไม้ 10,000 ต้นเหมือนกัน แบบแรกสร้างอาร์เรย์ texture ทุกต้น แบบที่สองแชร์ผ่าน factory เก็บ reference ของป่าไว้จนจบช่วงตรวจวัด แล้วใช้ profiler ตรวจชนิดและจำนวนออบเจ็กต์ รวมทั้ง heap ที่ยังถูกอ้างอิง หลีกเลี่ยงการสรุปจาก Runtime.freeMemory() เพียงค่าเดียว เพราะ GC และสภาพ JVM ส่งผลต่อค่าที่เห็น

#14. ประเด็นสำคัญสำหรับนำไปใช้งาน

Flyweight ช่วยประหยัดหน่วยความจำด้วยการลดข้อมูลร่วมที่ถูกสร้างซ้ำ ความถูกต้องของการใช้งานขึ้นกับการแบ่ง intrinsic/extrinsic state, การออกแบบ key, การควบคุมการแก้ไขข้อมูลที่แชร์ และอายุของ factory

ใน workshop นี้ต้นไม้ 10,000 ต้นยังคงมีตำแหน่งของตนเอง แต่ใช้ข้อมูลชนิดต้นไม้ 3 ชุดร่วมกัน เมื่อจะใช้ในระบบจริง ให้ตรวจว่าข้อมูลซ้ำมีขนาดและจำนวนมากพอที่จะคุ้มกับโครงสร้างที่เพิ่มขึ้น และยืนยันผลด้วยการวัด

#แหล่งอ้างอิง

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. ข้อมูลหนังสือและสารบัญจากสำนักพิมพ์
  2. Oracle. Java SE 17: HashMap — พฤติกรรม computeIfAbsent() และข้อกำหนดเรื่อง synchronization
  3. Oracle. Java SE 17: ConcurrentHashMap — atomic computation และข้อกำหนดของ mapping function
  4. Oracle. Java SE 17: Record Classes — component และเมธอดที่สร้างให้ record
  5. Oracle. Java SE 17: Flight Recorder API Programmer’s Guide — เครื่องมือเก็บข้อมูลการทำงานของ JVM

บทความและตัวอย่างเขียนขึ้นเพื่อการเรียนรู้ ตัวเลขหน่วยความจำในตารางคำนวณจาก payload จำลอง และไม่อ้างเป็นผลการทดลองสมรรถนะของระบบจริง