Flyweight w C# - jak współdzielić dane i oszczędzać pamięć

Przemysław Kwiatkowski 15 czerwca 2026
Schemat ilustruje wzorzec projektowy Flyweight: wspólne obiekty (Shared Object) z centralnego procesora obsługują unikalne instancje użytkowników, optymalizując zasoby.

Spis treści

Gdy aplikacja tworzy setki tysięcy podobnych obiektów, problemem szybko przestaje być sam czas konstrukcji. Znacznie częściej ograniczeniem okazuje się pamięć, zwłaszcza gdy każdy obiekt przechowuje te same dane, takie jak tekstura, nazwa, konfiguracja albo metadane. W tym artykule pokazuję, jak działa wzorzec flyweight pattern, jak rozdzielić stan współdzielony od unikalnego oraz jak zaimplementować takie rozwiązanie w C# bez komplikowania kodu.

Współdziel dane, które naprawdę się powtarzają

  • Flyweight ogranicza zużycie pamięci przez współdzielenie niezmiennego stanu między wieloma obiektami.
  • Stan wewnętrzny jest wspólny, a stan zewnętrzny pozostaje unikalny dla konkretnego użycia.
  • Fabryka przechowuje obiekty w cache i zwraca istniejącą instancję zamiast tworzyć kolejną.
  • Wzorzec ma sens głównie przy dużej liczbie podobnych obiektów, a nie przy kilku instancjach.
  • Najważniejsze zabezpieczenie to niemutowalność współdzielonych danych.

Co rozwiązuje wzorzec Flyweight

Flyweight należy do wzorców strukturalnych. Jego zadaniem jest ograniczenie liczby przechowywanych kopii tych samych danych, a nie samo przyspieszenie wywołania konstruktora. Jeżeli milion obiektów zawiera identyczną nazwę, kolor i konfigurację, trzymanie tych wartości milion razy jest zwyczajnie niepotrzebne.

Najprościej wyobrazić sobie planszę z milionem drzew. Każde drzewo ma inne współrzędne, ale wiele z nich korzysta z tego samego gatunku, modelu 3D i obrazu tekstury. Flyweight przechowuje wspólny opis gatunku, natomiast pozycja, skala i stan konkretnego drzewa pozostają poza współdzielonym obiektem.

Przykładowo, jeśli powtarzające się dane zajmują umownie 200 bajtów, milion osobnych kopii oznacza około 200 MB samych danych. W realnym środowisku dochodzi jeszcze narzut obiektów, referencji i struktur zarządzanych przez runtime, więc rzeczywisty koszt może być wyższy.

Stan wewnętrzny i zewnętrzny

Cała idea opiera się na podziale właściwości obiektu na dwie grupy. Ten podział jest ważniejszy niż sama fabryka, bo źle wybrany stan może sprawić, że współdzielenie stanie się niebezpieczne albo po prostu nie przyniesie oszczędności.

Rodzaj stanu Znaczenie Przykłady
Wewnętrzny Nie zależy od konkretnego użycia i może być współdzielony. Nazwa gatunku, tekstura, kolor, rozmiar czcionki
Zewnętrzny Zależy od miejsca, czasu lub konkretnego obiektu. Pozycja, indeks, aktualny stan, właściciel, kierunek

Stan wewnętrzny powinien być niezmienny po utworzeniu. Gdy jeden użytkownik zmieni współdzielony kolor, zmiana nie może przypadkiem wpłynąć na tysiące innych elementów. To jedna z tych zasad, które na diagramie wyglądają niewinnie, a w działającej aplikacji decydują o tym, czy rozwiązanie będzie stabilne.

Jak wygląda współpraca obiektów

Typowa implementacja składa się z trzech elementów. Flyweight przechowuje wspólny stan, kontekst przechowuje dane konkretnego użycia, a fabryka zarządza pulą istniejących instancji. Klient nie powinien za każdym razem tworzyć współdzielonego obiektu bezpośrednio, bo wtedy traci się najważniejszą korzyść wzorca.

  • Flyweight udostępnia operacje wykorzystujące stan współdzielony.
  • Context przechowuje dane unikalne, na przykład współrzędne lub identyfikator.
  • Factory wyszukuje obiekt po kluczu i tworzy go tylko wtedy, gdy nie ma go jeszcze w cache.
  • Client dostarcza stan zewnętrzny podczas użycia obiektu.

W praktyce flyweight nie reprezentuje już kompletnego obiektu biznesowego. Jest raczej współdzielonym opisem albo szablonem, który razem z kontekstem daje pełny obiekt widoczny z punktu widzenia aplikacji.

To rozróżnienie dobrze widać w systemie renderowania. `TreeType` może przechowywać nazwę i teksturę, natomiast `Tree` przechowuje pozycję. Ten sam `TreeType` może obsługiwać tysiące drzew, ale każde z nich ma własny kontekst.

Implementacja w C# na praktycznym przykładzie

Poniższy przykład pokazuje katalog typów drzew. Klasa `TreeType` jest niemutowalna, a fabryka korzysta ze słownika, który pełni rolę cache. Klucz zawiera wyłącznie dane potrzebne do rozpoznania, czy dwa typy są identyczne.

using System;
using System.Collections.Generic;

public sealed class TreeType
{
    public string Name { get; }
    public string TexturePath { get; }
    public string Color { get; }

    public TreeType(string name, string texturePath, string color)
    {
        Name = name;
        TexturePath = texturePath;
        Color = color;
    }

    public void Render(int x, int y)
    {
        Console.WriteLine(
            $"Renderuję {Name} w ({x}, {y}) z teksturą {TexturePath}");
    }
}

public sealed class TreeTypeFactory
{
    private readonly Dictionary _types = new();

    public TreeType Get(string name, string texturePath, string color)
    {
        var key = $"{name}|{texturePath}|{color}";

        if (_types.TryGetValue(key, out var existing))
        {
            return existing;
        }

        var created = new TreeType(name, texturePath, color);
        _types[key] = created;
        return created;
    }
}

public sealed class Tree
{
    private readonly int _x;
    private readonly int _y;
    private readonly TreeType _type;

    public Tree(int x, int y, TreeType type)
    {
        _x = x;
        _y = y;
        _type = type;
    }

    public void Render()
    {
        _type.Render(_x, _y);
    }
}

Użycie wygląda wtedy tak, że fabryka zwraca tę samą instancję dla identycznego zestawu danych. Unikalny pozostaje tylko obiekt `Tree` oraz jego pozycja.

var factory = new TreeTypeFactory();

var oak = factory.Get(
    "Dąb",
    "textures/oak.png",
    "green");

var firstTree = new Tree(10, 20, oak);
var secondTree = new Tree(100, 80, oak);

firstTree.Render();
secondTree.Render();

W aplikacji wielowątkowej prosty `Dictionary` trzeba zastąpić synchronizacją albo strukturą współbieżną. W ASP.NET Core fabrykę zwykle rejestruję jako singleton, ale tylko wtedy, gdy przechowywane obiekty są bezpieczne do współdzielenia i nie zawierają danych żądania użytkownika.

Warto też uważać na budowanie klucza przez konkatenację. Dla prostego przykładu jest czytelne, lecz w większym systemie lepiej użyć rekordu wartościowego, na przykład `record struct TreeKey`, aby uniknąć kolizji wynikających z separatorów lub niejednoznacznych wartości.

Gdzie ten wzorzec ma sens w .NET

Najlepsze zastosowania pojawiają się tam, gdzie aplikacja tworzy bardzo wiele podobnych elementów, a ich część pozostaje taka sama. Nie chodzi o abstrakcyjne użycie wzorca dla samego wzorca. Najpierw szukam powtarzających się danych i sprawdzam, czy zajmują istotną część pamięci.

Renderowanie i grafika

Silnik gry może przechowywać jeden opis tekstury, modelu i materiału, a osobno pozycję każdego obiektu na scenie. Podobnie działa wizualizacja mapy, graf dokumentów albo interfejs z dużą liczbą ikon. Współdzielenie zasobów graficznych ogranicza pamięć i może zmniejszyć liczbę kosztownych operacji inicjalizacyjnych.

Edytory tekstu i dokumenty

W dokumencie wiele znaków korzysta z tych samych informacji o kroju, rozmiarze i stylu. Pozycja znaku oraz jego fragment dokumentu są zewnętrzne, ale opis stylu można współdzielić. Przy dużych dokumentach taka separacja jest znacznie bardziej opłacalna niż tworzenie pełnego obiektu formatowania dla każdego znaku.

Systemy konfiguracji i metadanych

W aplikacjach biznesowych wiele rekordów może wskazywać na ten sam typ produktu, kraj, kategorię lub definicję reguły. Zamiast kopiować kompletny opis przy każdym rekordzie, można przechowywać jeden współdzielony obiekt i referencję do niego. Trzeba tylko dopilnować, aby dane słownikowe były tylko do odczytu.

Cache wartości

Niektóre biblioteki używają rozwiązań podobnych do flyweight, zwracając wcześniej utworzone obiekty dla często powtarzających się wartości. Dobrym przykładem jest interning napisów w środowisku .NET, choć nie należy traktować go jako uniwersalnego cache dla dowolnych stringów. Zbyt agresywne utrzymywanie wartości może zwiększyć zużycie pamięci zamiast je zmniejszyć.

Kiedy lepiej wybrać inne rozwiązanie

Flyweight nie jest automatycznym lekarstwem na dużą liczbę obiektów. Jeżeli aplikacja ma kilkaset instancji, koszt fabryki, kluczy i synchronizacji może być większy niż oszczędność. Zanim wdrożę ten wzorzec, sprawdzam profil pamięci i liczbę duplikujących się wartości.

Rozwiązanie Co współdzieli Kiedy je wybrać
Flyweight Niezmienny fragment stanu wielu obiektów Gdy podobnych obiektów są tysiące lub miliony
Object pool Gotowe instancje wielokrotnego użycia Gdy koszt tworzenia i usuwania obiektów jest wysoki
Cache Wyniki albo dane pobrane wcześniej Gdy chcemy ograniczyć koszt obliczeń lub dostępu do źródła
Zwykła klasa Nic automatycznie Gdy liczba obiektów jest mała, a kod ma pozostać prosty

Najczęstszy błąd polega na pomyleniu flyweight z pulą obiektów. Pool oddaje tę samą instancję do ponownego użycia, zwykle po wcześniejszym wyczyszczeniu jej stanu. Flyweight pozwala wielu klientom korzystać jednocześnie z tego samego niemutowalnego fragmentu danych.

Przeczytaj również: var w JavaScripcie - zakres, hoisting i pułapki starego kodu

Typowe pułapki

  • Współdzielony stan jest mutowalny, więc zmiana jednego klienta wpływa na pozostałych.
  • Fabryka ma nieskończony cache i nigdy nie usuwa rzadko używanych wartości.
  • Klucz jest zbyt szczegółowy, dlatego prawie każde żądanie tworzy nowy obiekt.
  • Do cache trafiają dane zależne od użytkownika, żądania lub transakcji.
  • Wzorzec zostaje dodany bez pomiaru, mimo że problemem nie jest pamięć, lecz czas CPU.

Jeżeli liczba możliwych kluczy jest bardzo duża, potrzebna może być polityka wygaszania, na przykład limit rozmiaru albo usuwanie najdawniej używanych elementów. W przeciwnym razie optymalizacja pamięci lokalnej zamieni się w nowe źródło wycieku pamięci.

Jak ocenić, czy wdrożenie się opłaca

Zaczynam od pomiaru liczby obiektów, rozmiaru powtarzających się danych i czasu życia instancji. Pomagają w tym profilery pamięci, ślady GC oraz benchmarki uruchamiane na reprezentatywnych danych. Sama obserwacja, że obiekt wygląda na ciężki, nie wystarcza do podjęcia dobrej decyzji.

Praktyczny test można oprzeć na trzech pytaniach. Czy tworzę dużo podobnych obiektów? Czy ich wspólne dane są niezmienne? Czy oszczędność pamięci będzie większa niż koszt słownika, kluczy i dodatkowych referencji?

Jeżeli odpowiedź na wszystkie pytania brzmi „tak”, wzorzec ma dobry punkt wyjścia. Jeżeli tylko obiekty są podobne, ale każdy ma inny stan, lepiej uprościć projekt i pozostawić zwykłe klasy. Prostota też jest zasobem, którego nie warto bez potrzeby oddawać.

Najważniejsza decyzja dotyczy granicy współdzielenia

Flyweight działa najlepiej wtedy, gdy granica między stanem wspólnym a unikalnym jest oczywista. Współdzielę dane stabilne, często powtarzające się i bezpieczne do odczytu, a do kontekstu przenoszę wszystko, co zależy od konkretnego użycia.

W aplikacji .NET zacząłbym od prostego pomiaru, małej fabryki i niemutowalnego obiektu. Dopiero gdy profil pamięci potwierdzi korzyść, rozbudowywałbym rozwiązanie o współbieżność, wygaszanie cache lub bardziej zaawansowane klucze. Najlepszy flyweight to taki, który rozwiązuje konkretny problem pamięci, a nie taki, który tylko komplikuje model obiektowy.

FAQ - Najczęstsze pytania

Wzorzec przechowuje wspólny, niezmienny stan tylko raz, a wiele obiektów korzysta z tej samej instancji. Przy milionie obiektów i 200 bajtach powtarzających się danych można uniknąć około 200 MB duplikowanych danych, nie licząc narzutu obiektów i referencji.

Stan wewnętrzny nie zależy od konkretnego użycia, dlatego może obejmować nazwę gatunku, teksturę, kolor lub rozmiar czcionki. Stan zewnętrzny zależy od konkretnego obiektu i powinien pozostać w kontekście, na przykład jako pozycja, indeks, właściciel albo aktualny kierunek.

Fabryka przechowuje instancje w słowniku i wyszukuje je po kluczu złożonym z danych współdzielonych, takich jak nazwa, ścieżka tekstury i kolor. Jeśli obiekt o danym kluczu istnieje, fabryka go zwraca, a w przeciwnym razie tworzy i zapisuje nową instancję. W większych systemach warto użyć rekordu wartościowego jako klucza, aby uniknąć kolizji konkatenowanych wartości.

Wzorzec zwykle nie opłaca się przy kilkuset instancjach, gdy koszt fabryki, kluczy i referencji przewyższa oszczędność. Nie należy go też mylić z object pool, ponieważ pool ponownie udostępnia instancję po wyczyszczeniu jej stanu, a Flyweight pozwala wielu klientom jednocześnie korzystać z tego samego niemutowalnego fragmentu danych.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

c#
cache
flyweight
niemutowalność
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz