1. Definicja
Pyłek (ang. Flyweight) to strukturalny wzorzec projektowy, który pozwala zmieścić w pamięci większą liczbę obiektów przez współdzielenie wspólnych części ich stanu zamiast trzymania wszystkiego w każdym obiekcie osobno.
Use sharing to support large numbers of fine-grained objects efficiently. — Gang of Four, Design Patterns
Najprościej rzecz ujmując: wyodrębnij to, co wspólne dla wielu obiektów, i przechowuj to w jednym miejscu — zamiast powielać w każdej instancji.
Pyłek należy do grupy wzorców strukturalnych, obok takich wzorców jak Adapter, Dekorator czy Fasada. O ile wzorce kreacyjne (Singleton, Builder, Factory) zajmowały się tym, jak tworzyć obiekty, wzorce strukturalne zajmują się tym jak je organizować i łączyć.
2. Przykład z życia: zobaczmy jak to zrozumieć?
Wyobraźmy sobie las w grze komputerowej.
Las składa się z setek tysięcy drzew. Każde drzewo ma swoją unikalną pozycję na mapie. Każde drzewo ma też gatunek, teksturę kory, kształt korony, kolor liści i tutaj robi się ciekawie: drzewo numer 47 832 wygląda dokładnie tak samo jak drzewo numer 12 i drzewo numer 8 941, bo wszystkie trzy to sosny. Różnią się tylko pozycją.
Naiwna implementacja: każde z 100 000 drzew to osobny obiekt, który trzyma w pamięci pełne dane, łącznie z teksturą o rozmiarze 2 MB. Wynik: 200 GB zużytej pamięci. Gra pada.
Sprytna implementacja: trzymamy jeden obiekt Sosna ze wszystkimi danymi wizualnymi, i 100 000 lekkich obiektów pozycji, z których każdy tylko wskazuje na wspólną Sosnę. Wynik: kilkadziesiąt MB. Gra działa.
To jest Pyłek — współdzielenie tego, co niezmienne i wspólne, by nie duplikować go tysiące razy.
Ćwiczenie myślowe: zanim przejdziemy do kodu, wyobraź sobie edytor tekstu. Mamy dokument z 50000 znaków. Każdy znak to obiekt z informacją o literze, czcionce, rozmiarze, kolorze. Ile znaków w całym dokumencie ma czcionkę Arial 12pt czarną? Prawdopodobnie 90%, te 45000 obiektów mogłoby współdzielić jeden obiekt stylu, zamiast każdy trzymać swój własny.
3. Kluczowe pojęcia — stan wewnętrzny i zewnętrzny
Zanim zobaczymy kod, musimy zrozumieć podział, który jest sercem wzorca.
Stan wewnętrzny (intrinsic state) — dane, które są wspólne dla wielu obiektów i się nie zmieniają w zależności od kontekstu. To właśnie ten stan współdzielimy. Dla drzewa w grze: gatunek, tekstura, model 3D. Dla znaku w edytorze: czcionka, rozmiar, kolor.
Stan zewnętrzny (extrinsic state) — dane, które są unikalne dla każdego wystąpienia i zmieniają się w zależności od kontekstu. Tego stanu nie współdzielimy — przekazujemy go z zewnątrz, w momencie użycia. Dla drzewa: pozycja X, Y na mapie. Dla znaku: pozycja w dokumencie, zaznaczenie.
┌──────────────────────────────────────────────────────────────┐ │ PYŁEK │ │ │ │ Stan wewnętrzny (współdzielony): │ │ ┌───────────────┐ │ │ │ Sosna │◄───────────┐───────────────┐──────┐ │ │ │ gatunek: "sosna" │ │ │ │ │ │ tekstura: [dane 2MB] │ │ │ │ │ │ kolorLisci: "ciemnozielony"│ │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ │ │ │ Stan zewnętrzny (unikalny): │ │ │ │ │ ┌───────────────┐ │ ┌──────────┐ │ ┌──────────┐ │ │ │ Drzewo #1 │────────────┘ │ Drzewo #2│─┘ │ Drzewo #3│ │ │ │ x: 10, y: 25 │ │ x: 847 │ │ x: 2341 │ │ │ └───────────────┘ │ y: 392 │ │ y: 78 │ │ │ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────────────────────┘
4. Przykład — naruszenie (bez Pyłka)
Tworzymy grę strategiczną z dużą mapą. Na mapie mogą stać żołnierze różnych typów — piechota, łucznicy, rycerze. Każdy żołnierz to osobny obiekt z pełnymi danymi.
class Zolnierz {
// Stan wewnętrzny — taki sam dla wszystkich żołnierzy tego samego typu
// Duplikowany w KAŻDEJ instancji
private String typ; // "Piechota", "Łucznik", "Rycerz"
private String teksturaSprite; // w rzeczywistości: dane binarne, dziesiątki KB
private String animacjaAtaku; // opis animacji — ten sam dla każdego łucznika
private int bazoweSilaAtaku; // statystyka bazowa — identyczna dla tego samego typu
// Stan zewnętrzny — unikalny dla każdego żołnierza
private int x;
private int y;
private int aktualnePunktyZycia;
public Zolnierz(String typ, String teksturaSprite, String animacjaAtaku,
int bazoweSilaAtaku, int x, int y) {
this.typ = typ;
this.teksturaSprite = teksturaSprite; // kopiujemy dla każdego obiektu osobno!
this.animacjaAtaku = animacjaAtaku; // kopiujemy dla każdego obiektu osobno!
this.bazoweSilaAtaku = bazoweSilaAtaku;
this.x = x;
this.y = y;
this.aktualnePunktyZycia = 100;
}
public void renderuj() {
System.out.println("Renderuję " + typ + " na pozycji (" + x + ", " + y + ")");
}
}
public class Main {
public static void main(String[] args) {
List<Zolnierz> armia = new ArrayList<>();
// Tworzymy 10 000 łuczników — każdy trzyma własną kopię tekstury i animacji
for (int i = 0; i < 10_000; i++) {
armia.add(new Zolnierz(
"Łucznik",
"[dane tekstury łucznika — 50KB]", // powielone 10 000 razy = 500MB!
"[animacja strzału]", // powielone 10 000 razy
15,
(int)(Math.random() * 1000),
(int)(Math.random() * 1000)
));
}
// Każdy z 10 000 obiektów trzyma identyczne dane tekstury i animacji
// Marnujemy setki megabajtów na duplikaty
System.out.println("Armia liczy: " + armia.size() + " żołnierzy");
}
}
Co jest tutaj złe?
Każdy z 10 000 łuczników trzyma własną kopię dokładnie tych samych danych — tekstury, animacji, statystyk bazowych. To tak, jakby każdy pracownik biura trzymał w szufladzie własny wydruk regulaminu firmy zamiast sięgnąć po jeden egzemplarz z tablicy ogłoszeń.
Konkretne konsekwencje:
- nadmierne zużycie pamięci — dane wspólne zwielokrotnione przez liczbę obiektów,
- spowolnienie aplikacji — więcej danych w pamięci to więcej operacji dla garbage collectora,
- brak spójności — gdy zmieniamy dane wspólne (np. poprawiamy teksturę), musimy zaktualizować każdy obiekt osobno,
- niemożliwość skalowania — gra z 100 000 jednostkami po prostu nie zmieści się w pamięci.
5. Przykład — zgodny z wzorcem Pyłek
Podejdźmy do tego lepiej. Rozdzielamy stan wewnętrzny (wspólny) od zewnętrznego (unikalnego) i współdzielimy ten pierwszy.
// Pyłek — zawiera TYLKO stan wewnętrzny (wspólny, niezmienny)
// Ten obiekt będzie współdzielony przez tysiące wystąpień
class TypZolnierza {
// Dane wspólne dla wszystkich żołnierzy tego samego typu — przechowywane raz
private final String typ;
private final String teksturaSprite; // jeden egzemplarz dla wszystkich łuczników
private final String animacjaAtaku; // jeden egzemplarz dla wszystkich łuczników
private final int bazowaSilaAtaku;
public TypZolnierza(String typ, String teksturaSprite,
String animacjaAtaku, int bazowaSilaAtaku) {
this.typ = typ;
this.teksturaSprite = teksturaSprite;
this.animacjaAtaku = animacjaAtaku;
this.bazowaSilaAtaku = bazowaSilaAtaku;
}
// Stan zewnętrzny (pozycja) przekazywany jako parametr — nie przechowywany w pyłku
public void renderuj(int x, int y) {
System.out.println("Renderuję " + typ + " [" + teksturaSprite + "]"
+ " na pozycji (" + x + ", " + y + ")");
}
public int getBazowaSilaAtaku() {
return bazowaSilaAtaku;
}
}
// Fabryka pyłków — zarządza pulą współdzielonych obiektów TypZolnierza
// Gwarantuje, że dla każdego typu istnieje dokładnie jedna instancja
class FabrykaPylkow {
// Cache — przechowuje już utworzone pyłki, by nie tworzyć duplikatów
private static final Map<String, TypZolnierza> cache = new HashMap<>();
public static TypZolnierza pobierz(String typ) {
// Jeśli pyłek dla tego typu już istnieje — zwracamy istniejący
if (!cache.containsKey(typ)) {
// Tworzymy nowy pyłek tylko gdy pojawia się po raz pierwszy
cache.put(typ, utworzTyp(typ));
System.out.println("Tworzę nowy pyłek dla typu: " + typ);
}
return cache.get(typ);
}
private static TypZolnierza utworzTyp(String typ) {
return switch (typ) {
case "Łucznik" -> new TypZolnierza("Łucznik", "[tekstura łucznika]", "[animacja strzału]", 15);
case "Piechota" -> new TypZolnierza("Piechota", "[tekstura piechoty]", "[animacja ciosu]", 20);
case "Rycerz" -> new TypZolnierza("Rycerz", "[tekstura rycerza]", "[animacja szarży]", 35);
default -> throw new IllegalArgumentException("Nieznany typ: " + typ);
};
}
public static int rozmiarCache() {
return cache.size();
}
}
// Lekki obiekt kontekstu — zawiera TYLKO stan zewnętrzny (unikalny)
// Zamiast duplikować dane, trzyma referencję do współdzielonego pyłka
class Zolnierz {
// Referencja do współdzielonego pyłka — nie kopia danych!
private final TypZolnierza typ;
// Stan zewnętrzny — unikalny dla każdego żołnierza
private int x;
private int y;
private int aktualnePunktyZycia;
public Zolnierz(String nazwaTypu, int x, int y) {
// Pobieramy współdzielony pyłek — nie tworzymy nowego obiektu z danymi
this.typ = FabrykaPylkow.pobierz(nazwaTypu);
this.x = x;
this.y = y;
this.aktualnePunktyZycia = 100;
}
public void renderuj() {
// Delegujemy renderowanie do pyłka, przekazując stan zewnętrzny
typ.renderuj(x, y);
}
public void przesun(int noweX, int noweY) {
this.x = noweX;
this.y = noweY;
}
}
public class Main {
public static void main(String[] args) {
List<Zolnierz> armia = new ArrayList<>();
// Tworzymy 10 000 łuczników
for (int i = 0; i < 10_000; i++) {
armia.add(new Zolnierz(
"Łucznik",
(int)(Math.random() * 1000),
(int)(Math.random() * 1000)
));
}
// Dodajemy 5 000 rycerzy
for (int i = 0; i < 5_000; i++) {
armia.add(new Zolnierz(
"Rycerz",
(int)(Math.random() * 1000),
(int)(Math.random() * 1000)
));
}
// Mimo 15 000 żołnierzy — mamy tylko 2 obiekty z ciężkimi danymi!
System.out.println("Żołnierzy w armii: " + armia.size()); // 15 000
System.out.println("Pyłków w pamięci: " + FabrykaPylkow.rozmiarCache()); // 2
// Renderujemy pierwszych kilku żołnierzy
armia.stream().limit(3).forEach(Zolnierz::renderuj);
}
}
Co zyskujemy?
15 000 żołnierzy, ale tylko 2 obiekty z ciężkimi danymi wizualnymi w pamięci. Każdy obiekt Zolnierz trzyma już tylko pozycję, punkty życia i referencję — kilkadziesiąt bajtów zamiast kilkudziesięciu kilobajtów. Zmiana tekstury łucznika wymaga aktualizacji jednego obiektu TypZolnierza — wszyscy łucznicy automatycznie ją "widzą".
6. Diagram struktury
FabrykaPylkow ┌──────────────────────────────┐ │ cache: Map<String, TypZoln.> │ │ + pobierz(typ): TypZolnierza │ └──────────────┬───────────────┘ │ zarządza ┌────────┼────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────┐ ┌────────┐ │ Łucznik │ │Piech.│ │ Rycerz │ ← pyłki (stan wewnętrzny) │ tekstura │ │ ... │ │ ... │ przechowywane RAZ │ animacja │ └──────┘ └────────┘ └────┬─────┘ │ ◄──────────────────────────────────────┐ │ ◄─────────────────────────┐ │ │ ◄──────────┐ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Zołnierz │ │ Zołnierz │ │ Zołnierz │ │ Zołnierz │ │ x: 10 │ │ x: 847 │ │ x: 23 │ │ x: 512 │ │ y: 25 │ │ y: 392 │ │ y: 671 │ │ y: 88 │ │ hp: 100 │ │ hp: 75 │ │ hp: 100 │ │ hp: 50 │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ stan zewnętrzny — unikalny dla każdego żołnierza
7. Dlaczego Pyłek jest ważny?
Drastyczna redukcja zużycia pamięci — to główna i jedyna motywacja wzorca. Gdy mamy tysiące lub miliony podobnych obiektów, współdzielenie wspólnych danych może zmniejszyć zużycie pamięci o kilka rzędów wielkości.
Wydajność garbage collectora — mniej obiektów w pamięci to mniej pracy dla GC. W Javie ma to realne przełożenie na responsywność aplikacji, szczególnie w grach i systemach czasu rzeczywistego.
Spójność danych wspólnych — zmiana stanu wewnętrznego w jednym miejscu natychmiast "propaguje się" do wszystkich obiektów, które go używają. Nie ma niespójnych kopii.
Kiedy Pyłek NIE jest potrzebny:
Wzorzec wprowadza złożoność — rozdzielenie stanu na wewnętrzny i zewnętrzny komplikuje kod. Warto go stosować tylko gdy:
- liczba obiektów jest naprawdę duża (tysiące, miliony),
- obiekty faktycznie współdzielą znaczącą ilość danych,
- zużycie pamięci jest realnym problemem w aplikacji.
Dla kilkudziesięciu obiektów — bez sensu. Dla miliona — rozważ koniecznie.
8. Kiedy stosować?
Pyłek ma sens w:
- grach komputerowych — drzewa, trawa, cząsteczki, żołnierze, NPC-e powtarzających się typów,
- edytorach tekstu i IDE — każdy znak w dokumencie jako lekki obiekt ze współdzielonym stylem,
- systemach renderowania UI — ikony, style, motywy współdzielone między komponentami,
- silnikach baz danych — buforowanie metadanych tabel i kolumn używanych przez wiele zapytań,
- systemach symulacji — miliony cząsteczek, komórek, agentów o powtarzających się właściwościach.
Pyłek w bibliotekach i JDK:
Integer.valueOf(int)— Java cachuje obiektyIntegerdla wartości od -128 do 127.Integer.valueOf(42) == Integer.valueOf(42)zwracatrue— to ten sam obiekt z puli,String.intern()— pula stringów (String Pool) w JVM to klasyczna implementacja Pyłka: identyczne literały stringów współdzielą jeden obiekt w pamięci,Boolean.TRUE/Boolean.FALSE— tylko dwie instancjeBooleanw całej JVM,- AWT/Swing
Font— obiekty czcionek są cachowane i współdzielone między komponentami UI.
Sygnały, że czas rozważyć Pyłka:
- profiler pokazuje setki tysięcy obiektów tego samego typu w pamięci,
OutOfMemoryErrorprzy tworzeniu dużej liczby podobnych obiektów,- aplikacja zwalnia przy zwiększeniu liczby encji na ekranie lub w symulacji,
- wiele obiektów trzyma identyczne, niezmienne dane — widać to w heap dumpie.
Trzy pytania do zidentyfikowania Pyłka:
1. Czy mam WIELE obiektów tego samego typu? → potencjalny kandydat 2. Czy te obiekty współdzielą ZNACZĄCE ilości danych? → warto rozważyć 3. Czy pamięć jest realnym PROBLEMEM? → czas na Pyłka