Flyweight Pattern – Optymalizacja Pamięci w Javie

TL;DR: Flyweight pattern pozwala drastycznie zmniejszyć zużycie pamięci poprzez współdzielenie wspólnych danych między wieloma obiektami. Idealny gdy masz tysiące podobnych obiektów różniących się tylko stanem zewnętrznym. Przykład: gra z 10000 cząsteczek gdzie każda ma tę samą teksturę ale różną pozycję.

## Dlaczego Flyweight jest ważny?

W aplikacjach biznesowych często mamy do czynienia z tysiącami podobnych obiektów – produkty w sklepie internetowym, użytkownicy w systemie CRM, czy elementy interfejsu. Bez optymalizacji każdy obiekt konsumuje pamięć na dane, które mogą być współdzielone. Flyweight rozwiązuje ten problem, co przekłada się bezpośrednio na niższe koszty serwerów i lepszą wydajność aplikacji.

Co się nauczysz:

  • Jak implementować wzorzec Flyweight w Javie 8/10
  • Różnica między stanem intrinsic i extrinsic
  • Praktyczne zastosowania w aplikacjach biznesowych
  • Optymalizację pamięci na przykładach z życia
  • Kiedy stosować, a kiedy unikać tego wzorca
Wymagania wstępne: Znajomość podstaw Javy 8, wzorców projektowych (Factory), podstawowe rozumienie zarządzania pamięcią w JVM.

## Czym jest wzorzec Flyweight?

Flyweight to strukturalny wzorzec projektowy, który minimalizuje zużycie pamięci poprzez współdzielenie efektywnie danych wspólnych dla wielu obiektów. Zamiast przechowywać wszystkie dane w każdym obiekcie, wydzielamy:

Stan intrinsic (wewnętrzny) – dane współdzielone między obiektami, niezmienne
Stan extrinsic (zewnętrzny) – dane unikalne dla każdego obiektu, przekazywane jako parametry

Analogia: Wyobraź sobie bibliotekę z tysiącami książek. Zamiast kupować osobną kopię każdej książki dla każdego czytelnika, biblioteka ma jedną kopię (stan intrinsic), a czytelnicy „wypożyczają” ją ze swoimi własnymi notatkami (stan extrinsic).

## Implementacja w Javie – Przykład Praktyczny

Załóżmy, że tworzymy system zarządzania dokumentami w firmie. Mamy tysiące dokumentów, ale tylko kilka typów (PDF, DOC, XLS) z różnymi nazwami i rozmiarami.

### Krok 1: Interfejs Flyweight

// Intrinsic state - współdzielony między wszystkimi dokumentami tego typu
public interface DocumentType {
    void process(String fileName, int fileSize); // extrinsic state jako parametry
}

### Krok 2: Konkretne implementacje Flyweight

public class PdfDocumentType implements DocumentType {
    private final String typeIcon; // intrinsic state
    private final String defaultViewer; // intrinsic state
    
    public PdfDocumentType() {
        this.typeIcon = "pdf-icon.png";
        this.defaultViewer = "Adobe Reader";
        System.out.println("Utworzono PdfDocumentType flyweight");
    }
    
    @Override
    public void process(String fileName, int fileSize) {
        System.out.println(String.format(
            "Przetwarzanie PDF: %s (%d KB) z ikoną %s w %s",
            fileName, fileSize, typeIcon, defaultViewer
        ));
    }
}

public class WordDocumentType implements DocumentType {
    private final String typeIcon;
    private final String defaultViewer;
    
    public WordDocumentType() {
        this.typeIcon = "word-icon.png";
        this.defaultViewer = "Microsoft Word";
        System.out.println("Utworzono WordDocumentType flyweight");
    }
    
    @Override
    public void process(String fileName, int fileSize) {
        System.out.println(String.format(
            "Przetwarzanie Word: %s (%d KB) z ikoną %s w %s",
            fileName, fileSize, typeIcon, defaultViewer
        ));
    }
}

### Krok 3: Factory dla zarządzania Flyweight’ami

import java.util.HashMap;
import java.util.Map;

public class DocumentTypeFactory {
    private static final Map flyweights = new HashMap<>();
    
    public static DocumentType getDocumentType(String type) {
        DocumentType flyweight = flyweights.get(type);
        
        if (flyweight == null) {
            switch (type.toLowerCase()) {
                case "pdf":
                    flyweight = new PdfDocumentType();
                    break;
                case "doc":
                case "docx":
                    flyweight = new WordDocumentType();
                    break;
                default:
                    throw new IllegalArgumentException("Nieobsługiwany typ: " + type);
            }
            flyweights.put(type, flyweight);
        }
        
        return flyweight;
    }
    
    public static int getCreatedFlyweightsCount() {
        return flyweights.size();
    }
}

### Krok 4: Kontekst wykorzystujący Flyweight

public class Document {
    private final String fileName; // extrinsic state
    private final int fileSize; // extrinsic state
    private final DocumentType type; // reference do flyweight
    
    public Document(String fileName, int fileSize, String fileType) {
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.type = DocumentTypeFactory.getDocumentType(fileType);
    }
    
    public void process() {
        type.process(fileName, fileSize);
    }
}

## Praktyczne zastosowanie

public class DocumentManagementSystem {
    public static void main(String[] args) {
        List documents = Arrays.asList(
            new Document("raport-q4.pdf", 2048, "pdf"),
            new Document("prezentacja.pdf", 5120, "pdf"),
            new Document("umowa.docx", 512, "docx"),
            new Document("notatki.doc", 256, "doc"),
            new Document("analiza.pdf", 3072, "pdf")
        );
        
        System.out.println("=== Przetwarzanie dokumentów ===" );
        documents.forEach(Document::process);
        
        System.out.println(String.format(
            "\\nUtworzono %d flyweight'ów dla %d dokumentów",
            DocumentTypeFactory.getCreatedFlyweightsCount(),
            documents.size()
        ));
    }
}
Pro tip: W tym przykładzie mamy 5 dokumentów, ale tylko 2 flyweight’y (PDF i Word). W aplikacji z 10000 dokumentów nadal mielibyśmy tylko kilka flyweight’ów, oszczędzając ogromne ilości pamięci.

## Korzyści i wady wzorca

### Korzyści:
– **Dramatyczna redukcja pamięci** – szczególnie przy tysiącach obiektów
– **Lepsza wydajność cache** – mniej obiektów = lepsze wykorzystanie CPU cache
– **Centralne zarządzanie** – wspólne właściwości w jednym miejscu

### Wady:
– **Zwiększona złożoność kodu** – podział na stan intrinsic/extrinsic
– **Możliwe problemy z wielowątkowością** – współdzielone obiekty wymagają synchronizacji
– **Overhead na wywołania metod** – przekazywanie extrinsic state jako parametry

Uwaga: Flyweight ma sens tylko gdy masz DUŻO obiektów z MAŁĄ ilością unikalnych stanów intrinsic. Dla 10 obiektów to over-engineering.

## Kiedy stosować Flyweight?

Stosuj gdyUnikaj gdy
Tysiące podobnych obiektówMało obiektów (<100)
Duże koszty przechowywaniaObiekty mają głównie stan unikalny
Stan można podzielić na intrinsic/extrinsicPotrzebujesz modyfikować stan intrinsic
Aplikacja ma problemy z pamięciąWydajność ważniejsza niż pamięć

## Zastosowania w prawdziwych projektach

### 1. Systemy GUI
Swing w Javie używa Flyweight dla renderowania znaków – każda litera „A” ma współdzieloną reprezentację graficzną, ale różne pozycje na ekranie.

### 2. Gry komputerowe
Cząsteczki, drzewa, budynki – wszystko co występuje w dużych ilościach z podobnymi właściwościami.

### 3. Systemy e-commerce
Produkty w tej samej kategorii dzielą metadane (ikony, opisy kategorii), różnią się tylko ceną i nazwą.

Czy Flyweight jest thread-safe?

Flyweight’y powinny być immutable (niezmienne), co czyni je naturalnie thread-safe. Problemy mogą wystąpić w Factory – rozważ synchronizację lub ConcurrentHashMap.

Jak zmierzyć oszczędności pamięci?

Użyj profilerów jak JProfiler lub VisualVM. Porównaj heap usage przed i po implementacji. Spodziewaj się 70-90% redukcji dla dobrze dopasowanych przypadków.

Czy można łączyć Flyweight z innymi wzorcami?

Tak! Często łączy się z Factory (do zarządzania), Composite (drzewa obiektów) i State (różne zachowania bez zmiany flyweight’a).

Co z Garbage Collection?

Flyweight’y powinny żyć przez cały cykl aplikacji. Uważaj na memory leaks – rozważ WeakHashMap w Factory jeśli flyweight’y mogą być „zapominane”.

Jak testować kod z Flyweight?

Testuj Factory (czy zwraca te same instancje), logikę biznesową (z mockowymi flyweight’ami) i zużycie pamięci (testy integracyjne z wieloma obiektami).

Pułapka: Nie próbuj modyfikować stanu intrinsic po utworzeniu flyweight’a – to łamie cały wzorzec. Jeśli potrzebujesz zmian, stwórz nowy flyweight.

## Przydatne zasoby

– [Oracle Java Documentation – Memory Management](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/)
– [Gang of Four – Design Patterns (1994)](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612)
– [Java Memory Profiling Tools](https://www.oracle.com/java/technologies/javase/jvm-monitoring-tools.html)
– [Effective Java by Joshua Bloch – Item 6: Avoid creating unnecessary objects](https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997)

🚀 Zadanie dla Ciebie

Zaimplementuj system zarządzania fontami w edytorze tekstu używając Flyweight. Każdy font (Arial, Times New Roman) to flyweight, a rozmiar i kolor to stan extrinsic. Stwórz 1000 fragmentów tekstu z różnymi fontami i zmierz zużycie pamięci przed i po optymalizacji.

Czy implementowałeś już wzorzec Flyweight w swoich projektach? Jakie były efekty optymalizacji pamięci?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry