Flyweight (Flugvikt)

Introduktion

Flyweight är ett strukturmönster som låter dig rymma fler objekt i tillgängligt RAM-minne genom att dela gemensamt tillstånd mellan flera objekt istället för att lagra allt data i varje objekt.


Problem

Du bygger ett skogssimuleringsspel med hundratusentals träd. Varje Tree-objekt lagrar sin position, ålder — men också mesh, texturer och färgdata som väger megabytes. Med 100 000 träd förbrukas enorma mängder minne trots att de flesta träd av samma art delar exakt samma grafik.

Lösning

Flyweight delar upp objektets tillstånd i intrinsic (delat, oföränderligt) och extrinsic (unikt per instans). Det delade tillståndet extraheras till ett Flyweight-objekt som delas av alla instanser av samma typ. Varje objekt lagrar bara sitt unika tillstånd.

När ska Flyweight användas?

  • När ett program behöver skapa ett enormt antal liknande objekt.
  • När objekten förbrukar för mycket RAM.
  • När en stor del av objektets tillstånd kan göras externt och delas.

Struktur

RollAnsvar
FlyweightLagrar det intrinsiska (delade) tillståndet.
Flyweight FactorySkapar och cachar Flyweight-objekt; returnerar befintliga om möjligt.
ContextLagrar det extrinsiska (unika) tillståndet + en referens till ett Flyweight.

Exempel — Skogssimulerare (Java)

Scenariot: Hundratusentals träd i ett spel, men bara ett fåtal unika TreeType-objekt med tung grafik.

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

// ── Flyweight — delar tungt intrinsiskt tillstånd ─────────

public final class TreeType {
    private final String name;
    private final String color;
    private final String texture; // Simulerar tung grafik

    public TreeType(String name, String color, String texture) {
        this.name    = name;
        this.color   = color;
        this.texture = texture;
        System.out.println("  [Flyweight skapad] " + name + " (" + texture + ")");
    }

    public void draw(int x, int y, int age) {
        System.out.printf("  🌲 %s på (%d,%d) ålder=%d färg=%s%n", name, x, y, age, color);
    }

    public String getName() { return name; }
}

// ── Flyweight Factory ─────────────────────────────────────

public class TreeTypeFactory {
    private static final Map<String, TreeType> cache = new HashMap<>();

    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + "_" + color;
        if (!cache.containsKey(key)) {
            cache.put(key, new TreeType(name, color, texture));
        }
        return cache.get(key);
    }

    public static int getCachedCount() { return cache.size(); }
}

// ── Context — lagrar unikt extrinsiskt tillstånd ──────────

public class Tree {
    private final int      x;
    private final int      y;
    private final int      age;
    private final TreeType type; // Referens till delat Flyweight

    public Tree(int x, int y, int age, TreeType type) {
        this.x    = x;
        this.y    = y;
        this.age  = age;
        this.type = type;
    }

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

// ── Forest — hanterar alla träd ───────────────────────────

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

    public void plantTree(int x, int y, int age, String name, String color, String texture) {
        TreeType type = TreeTypeFactory.getTreeType(name, color, texture);
        trees.add(new Tree(x, y, age, type));
    }

    public void draw() {
        System.out.println("Ritar " + trees.size() + " träd:");
        for (Tree tree : trees) {
            tree.draw();
        }
    }

    public int getTreeCount() { return trees.size(); }
}

// ── Klientkod ─────────────────────────────────────────────

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

        System.out.println("Skapar Flyweight-objekt (en gång per trädtyp):");

        // Plantera 10 träd av 3 typer — bara 3 TreeType-objekt skapas
        forest.plantTree(10, 20, 5,  "Gran",  "mörkgrön", "gran_texture_4mb.png");
        forest.plantTree(30, 40, 12, "Björk", "vit",      "bjork_texture_3mb.png");
        forest.plantTree(50, 10, 3,  "Gran",  "mörkgrön", "gran_texture_4mb.png"); // Återanvänder
        forest.plantTree(70, 80, 8,  "Ek",    "grön",     "ek_texture_5mb.png");
        forest.plantTree(15, 55, 20, "Björk", "vit",      "bjork_texture_3mb.png"); // Återanvänder
        forest.plantTree(90, 30, 1,  "Gran",  "mörkgrön", "gran_texture_4mb.png"); // Återanvänder
        forest.plantTree(25, 65, 7,  "Ek",    "grön",     "ek_texture_5mb.png");   // Återanvänder
        forest.plantTree(45, 15, 15, "Gran",  "mörkgrön", "gran_texture_4mb.png"); // Återanvänder
        forest.plantTree(60, 90, 4,  "Björk", "vit",      "bjork_texture_3mb.png"); // Återanvänder
        forest.plantTree(80, 50, 9,  "Ek",    "grön",     "ek_texture_5mb.png");   // Återanvänder

        System.out.println();
        forest.draw();

        System.out.println();
        System.out.println("Statistik:");
        System.out.println("  Träd i skogen:          " + forest.getTreeCount());
        System.out.println("  Unika TreeType-objekt:  " + TreeTypeFactory.getCachedCount());
        System.out.println("  Minnesbesparning: " + forest.getTreeCount() + " träd delar " +
            TreeTypeFactory.getCachedCount() + " tunga textureobjekt");
    }
}

Output:

Skapar Flyweight-objekt (en gång per trädtyp):
  [Flyweight skapad] Gran (gran_texture_4mb.png)
  [Flyweight skapad] Björk (bjork_texture_3mb.png)
  [Flyweight skapad] Ek (ek_texture_5mb.png)

Ritar 10 träd:
  🌲 Gran på (10,20) ålder=5 färg=mörkgrön
  🌲 Björk på (30,40) ålder=12 färg=vit
  🌲 Gran på (50,10) ålder=3 färg=mörkgrön
  🌲 Ek på (70,80) ålder=8 färg=grön
  🌲 Björk på (15,55) ålder=20 färg=vit
  🌲 Gran på (90,30) ålder=1 färg=mörkgrön
  🌲 Ek på (25,65) ålder=7 färg=grön
  🌲 Gran på (45,15) ålder=15 färg=mörkgrön
  🌲 Björk på (60,90) ålder=4 färg=vit
  🌲 Ek på (80,50) ålder=9 färg=grön

Statistik:
  Träd i skogen:          10
  Unika TreeType-objekt:  3
  Minnesbesparning: 10 träd delar 3 tunga textureobjekt

Fördelar

  • Drastisk minnesbesparing när många objekt delar gemensamt tillstånd.
  • Kan möjliggöra scenarier som annars vore omöjliga på grund av minnesbegränsningar.

Nackdelar

  • Ökar kodkomplexiteten — uppdelningen i intrinsiskt/extrinsiskt tillstånd kan vara svår att förstå.
  • CPU-prestanda kan påverkas om extrinsiskt tillstånd behöver beräknas om ofta.
  • Flyweight-objekt måste vara oföränderliga (immutable) — delat föränderligt tillstånd orsakar buggar.

Av Victor Hernandez från Bytebase.se