Najkrócej mówiąc, chodzi o dzielenie powtarzalnego stanu między wieloma obiektami
- Wzorzec rozdziela dane wspólne od danych zależnych od kontekstu.
- Najlepiej działa przy bardzo dużej liczbie drobnych, podobnych obiektów.
- Realny zysk to mniejsze zużycie pamięci i często mniej kosztowna inicjalizacja.
- Nie jest dobrym wyborem, jeśli stan prawie się nie powtarza albo kod ma być maksymalnie prosty.
- W aplikacjach webowych sprawdza się m.in. przy znacznikach na mapie, ikonach, tokenach składni i kafelkach UI.
Co tak naprawdę rozwiązuje wzorzec flyweight
Najprościej: ten wzorzec ma sens wtedy, gdy chcesz reprezentować bardzo wiele drobnych obiektów, ale nie chcesz powielać w każdym z nich tych samych danych. To nie jest sztuczka do „oszczędzania pamięci za wszelką cenę”, tylko sposób na sensowne rozdzielenie informacji, które są wspólne, od tych, które zależą od konkretnego użycia. W klasycznych przykładach pojawiają się znaki w edytorze tekstu, glify, markery na mapie albo drobne elementy UI, które różnią się głównie pozycją, etykietą lub stanem chwilowym.
W praktyce patrzę na to tak: jeśli obiekt ma 5 pól wspólnych dla tysięcy instancji i 1 pole zmienne, to pełne kopiowanie całości jest zwykle niepotrzebne. Stan wewnętrzny (intrinsic) zostaje współdzielony, a stan zewnętrzny (extrinsic) jest przekazywany przy użyciu obiektu. Dzięki temu nie tworzysz osobnej kopii każdego powtarzalnego fragmentu danych tylko dlatego, że potrzebujesz kolejnej instancji logicznej.
To podejście działa szczególnie dobrze, gdy obiekt sam w sobie jest prosty, ale liczba wystąpień jest duża. Jeśli natomiast każdy egzemplarz różni się niemal wszystkim, korzyść szybko znika. Wtedy lepiej iść w prostszy model danych niż dokładać warstwę współdzielenia na siłę. Dalej rozbiję to na konkretny mechanizm, żeby było jasne, gdzie kończy się teoria, a zaczyna użyteczny kod.

Jak działa współdzielenie stanu w praktyce
Jeśli projektuję taki mechanizm, myślę o nim w trzech warstwach: co jest wspólne, co jest kontekstowe i kto pilnuje ponownego użycia. Wzorzec nie musi mieć rozbudowanej architektury, ale musi być konsekwentny. Gdy ta konsekwencja znika, szybko kończy się na chaosie zamiast optymalizacji.
Stan wewnętrzny
To dane, które są identyczne dla wielu obiektów i mogą być trzymane raz. W przykładzie z markerami mapy będą to np. ikona, kolor, rozmiar lub typ znacznika. Taki stan powinien być możliwie niemutowalny, bo wtedy bezpiecznie go współdzielisz między wieloma użyciami.
Stan zewnętrzny
To wszystko, co zależy od konkretnego miejsca użycia: współrzędne, czas, indeks w liście, aktualna wartość licznikowa, tekst etykiety, kontekst użytkownika. Tych danych nie opłaca się wkładać do współdzielonego obiektu, bo zmieniają się zbyt często. Zamiast tego przekazujesz je do metody renderującej lub operującej na flyweight.
Przeczytaj również: Wzorzec szablonowa metoda – kiedy upraszcza kod, a kiedy szkodzi?
Fabryka lub rejestr obiektów
Żeby współdzielenie nie było ręczne i kruche, zwykle używam prostego rejestru lub fabryki. Jej zadanie jest banalne: jeśli obiekt o takim samym stanie wewnętrznym już istnieje, zwraca istniejącą instancję. Jeśli nie, tworzy nową i zapisuje ją w cache. W JavaScript lub TypeScript często wystarcza zwykły Map, o ile klucz jest dobrze zbudowany i jednoznaczny.
type MarkerStyle = {
icon: string;
color: string;
size: number;
};
class MarkerFlyweight {
constructor(private readonly style: MarkerStyle) {}
render(x: number, y: number, label: string) {
return { ...this.style, x, y, label };
}
}
class MarkerFactory {
private pool = new Map();
get(icon: string, color: string, size: number) {
const key = `${icon}:${color}:${size}`;
let flyweight = this.pool.get(key);
if (!flyweight) {
flyweight = new MarkerFlyweight({ icon, color, size });
this.pool.set(key, flyweight);
}
return flyweight;
}
}
W takim układzie masz kilka wspólnych wariantów stylu, a nie tysiące niemal identycznych obiektów z powielonym opisem wyglądu. Właśnie tu widać sedno wzorca: obiekt logicznie istnieje dla konkretnego elementu interfejsu, ale fizycznie część jego danych jest wspólna. To prowadzi naturalnie do pytania, kiedy taki wysiłek naprawdę się zwraca.
Kiedy ten wzorzec daje realny zysk
Najczęściej zaczynam od prostego pytania: czy powtarzalny stan jest na tyle duży, że naprawdę boli? Jeśli nie, zysk będzie kosmetyczny, a złożoność wzrośnie natychmiast. Ten wzorzec nie jest uniwersalną odpowiedzią na wszystkie problemy z pamięcią.
| Sygnał w projekcie | Co to zwykle oznacza | Czy flyweight ma sens |
|---|---|---|
| 10 000+ podobnych rekordów | Dużo pamięci idzie na powtarzalne pola | Tak, szczególnie gdy 70-90% pól się powtarza |
| Elementy różnią się głównie pozycją, etykietą lub czasem | Stan kontekstowy można wyciągnąć na zewnątrz | Tak |
| Każdy obiekt ma unikalny zestaw danych | Mało współdzielenia, dużo wariantów | Zwykle nie |
| Masz tylko kilkaset obiektów | Koszt pamięci jest raczej mały | Rzadko |
Jeśli każdy z 20 000 obiektów dubluje 150 bajtów stałych danych, powstaje około 3 MB samego powtórzonego stanu. To nie zawsze jest katastrofa, ale w frontendzie, gdzie dochodzą DOM, grafika i inne struktury, taki koszt potrafi być odczuwalny. Zysk trzeba jednak potwierdzić profilem pamięci, nie intuicją.
W praktyce dobry moment na ten wzorzec widzę tam, gdzie dane są masowe, ale schemat powtarzalny: znaczniki na mapie, komórki planszy, tokeny składni, zestawy ikon albo elementy canvas renderowane w dużej liczbie. Jeśli Twoja aplikacja operuje na setkach, a nie dziesiątkach tysięcy elementów, częściej wygrywa prostota. Z tego miejsca naturalnie przechodzę do pytania, jak taki wzorzec wdrożyć bez zrobienia z kodu niepotrzebnej układanki.
Jak wdrożyć go w aplikacji webowej
Jeśli miałbym rozpisać wdrożenie na prosty plan, zrobiłbym to w pięciu krokach. Dzięki temu nie zaczynasz od abstrakcji, tylko od danych i ich charakteru. To ważne, bo źle wydzielony stan psuje cały sens podejścia.
- Wyłapuję pola, które powtarzają się w wielu instancjach.
- Oddzielam je od danych zależnych od konkretnego użycia.
- Tworzę fabrykę albo rejestr z kluczem opartym o stan wspólny.
- Przekazuję stan zewnętrzny przy renderowaniu lub wykonywaniu operacji.
- Mierzę pamięć i czas renderu przed oraz po zmianie.
W kodzie frontendowym najczęściej pilnuję dwóch rzeczy: żeby stan współdzielony był niemutowalny oraz żeby klucz cache był naprawdę jednoznaczny. Jeśli klucz jest zbyt szeroki, łączysz rzeczy, które nie powinny być wspólne. Jeśli jest zbyt wąski, przestajesz odzyskiwać pamięć, bo wariantów robi się za dużo. To drobny detal, ale właśnie na takich detalach ten wzorzec się wygrywa albo przegrywa.
W praktyce lubię sprawdzać efekt na jednym konkretnym ekranie, nie na całej aplikacji. Na przykład na widoku mapy z tysiącami znaczników albo na liście z bardzo podobnymi kartami. Jeśli zmiana poprawia pamięć i nie komplikuje renderowania, wtedy dopiero rozciągam ją na większy fragment systemu. To prowadzi do kolejnej, bardzo ważnej sprawy: nie mylić flyweightu z innymi technikami, które wyglądają podobnie, ale rozwiązują inny problem.
Czym różni się od cache, memoizacji i puli obiektów
To są pojęcia, które łatwo ze sobą pomylić, bo wszystkie kręcą się wokół ponownego użycia. Różnica jest jednak istotna. Gdy mylisz te mechanizmy, kończysz z kodem, który niby działa, ale trudno go utrzymać i jeszcze trudniej zoptymalizować dalej.
| Podejście | Co robi | Główna korzyść | Kiedy uważać |
|---|---|---|---|
| Flyweight | Współdzieli stan wewnętrzny między wieloma obiektami | Mniej powtórzonej pamięci i mniej duplikacji | Gdy stan nie jest naprawdę wspólny albo obiektów jest mało |
| Cache / memoizacja | Zapamiętuje wynik obliczenia lub utworzony obiekt | Szybsze kolejne wywołania | Gdy przechowujesz zbyt wiele wpisów albo wynik zależy od ukrytego kontekstu |
| Pula obiektów | Recyklinguje instancje, zwykle tymczasowo i często mutowalnie | Mniej kosztownych alokacji | Gdy obiekty są trudne do „resetowania” albo żyją zbyt krótko |
| Singleton | Zapewnia jedną globalną instancję | Jedno miejsce dostępu | Gdy zaczynasz zbyt mocno sprzęgać cały system z globalnym stanem |
Najważniejsza różnica jest taka, że flyweight nie służy do „przyspieszania wszystkiego”. On służy do tego, by wiele podobnych bytów nie niosło ze sobą tej samej porcji danych. Cache może być częścią implementacji, ale nie każdy cache jest flyweightem. Jeśli z tego rozróżnienia coś zapamiętasz, to właśnie to.
Najczęstsze błędy przy takim podejściu
W tym wzorcu najłatwiej popełnić błąd nie techniczny, tylko projektowy. Ktoś widzi dużą liczbę obiektów i od razu chce je „zmniejszać”, zamiast najpierw zrozumieć, co faktycznie się powtarza. To prowadzi do niepotrzebnej komplikacji.
- Współdzielone dane nie są naprawdę stałe. Jeśli coś zmienia się co chwilę, nie powinno siedzieć w wspólnym obiekcie. Inaczej dostajesz ukryte zależności i trudne do śledzenia skutki uboczne.
- Klucz rejestru jest źle zaprojektowany. Za szeroki klucz scala różne przypadki, za wąski tworzy za dużo wariantów. W obu sytuacjach tracisz kontrolę nad współdzieleniem.
- Patrzysz na liczbę obiektów zamiast na koszt. Sama duża liczba instancji nie jest problemem, jeśli ich stan jest mały. Najpierw profil, dopiero potem optymalizacja.
- Wzorzec trafia do miejsca, gdzie zysk jest mały. Przy małych listach lub jednorazowych ekranach zysk zwykle nie rekompensuje złożoności architektury.
- Dodajesz zbyt wiele warstw pośrednich. Jeśli każda operacja przechodzi przez fabrykę, cache, adapter i jeszcze serwis, debugowanie staje się uciążliwe.
Jest też druga strona medalu: flyweight bywa bardzo skuteczny, ale tylko wtedy, gdy dane są powtarzalne i dobrze ustrukturyzowane. W aplikacjach webowych często lepiej działa przy renderowaniu i reprezentacji danych niż przy logice biznesowej. To ważne rozróżnienie, bo w przeciwnym razie wzorzec zaczyna przynosić więcej abstrakcji niż korzyści.
Co zabrać z tego wzorca do projektowania architektury
Najbardziej praktyczna lekcja jest prosta: jeśli obiekt ma wiele pól stałych, a tylko niewielki fragment zależy od kontekstu, rozdziel te dwa światy. W architekturze aplikacji webowej taki ruch często upraszcza renderowanie, obniża zużycie pamięci i pozwala lepiej kontrolować koszt dużych list, map czy edytorów.
- Najpierw profiluję, potem optymalizuję.
- Wydzielam stan wspólny tylko wtedy, gdy powtarza się naprawdę często.
- Trzymam wspólne dane możliwie niezmienne.
- Przekazuję kontekst przy użyciu, a nie przy tworzeniu.
- Nie komplikuję modelu, jeśli problem dotyczy kilkuset obiektów.
W dobrze dobranym miejscu ten wzorzec daje wyraźny efekt: mniej pamięci, mniej duplikacji i czytelniejszy podział odpowiedzialności. W źle dobranym miejscu dokłada tylko dodatkową warstwę abstrakcji, więc tu akurat prostota i profilowanie wygrywają z samą elegancją idei.