Wzorzec projektowy Pyłek (Flyweight) to jeden ze strukturalnych wzorców projektowych opisanych przez Gang of Four. W tym artykule poznasz jego specyfikę oraz dowiesz się, kiedy warto go wykorzystać. Przykłady kodu w artykule przygotowane zostały w TypeScripcie.
Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce projektowe, to koniecznie sprawdź mój wpis o wzorcach projektowych.
Omówienie wzorca
Banda Czworga definiuje cel wzorca następująco:
Use sharing to support large numbers of fine-grained objects efficiently.
Głównym założeniem Pyłka jest dzielenie się i reużywanie danych już istniejących w pamięci. Pyłek znajdzie zastosowanie w aplikacjach, gdzie w pamięci tworzone jest wiele podobnych obiektów lub obiektów dzielących wspólne cechy np. stan, kolor. Określenie fine-grained nie jest tu bez przyczyny. Charakterystyka Pyłka zakłada, by Pyłki były na tyle granularne i ogólne, by można je wykorzystać wielokrotnie. Powstałe Pyłki powinny być również niemutowalne. Ich modyfikacja może sprawić, że korzystające z nich obiekty przestaną zachowywać się zgodnie z oczekiwaniami.
Załóżmy, że tworzone obiekty klasy Object
mają stan „open”, „closed”, „pending” itp. opisany obiektami klasy State
. Mając w systemie obiekty z określonym stanem, nie ma konieczności tworzenia nowego obiektu klasy State
każdorazowo. Liczba dostępnych stanów jest skończona. Zamiast tworzenia nowych obiektów można użyć referencji do już istniejących.
Wykorzystanie Pyłka w takim przykładzie dodatkowo zabezpiecza przed nadmiarowym rozszerzaniem wykorzystanych obiektów i łamaniu reguły pojedynczej odpowiedzialności (SRP). W podanym przykładzie chcąc na przykład śledzić, od kiedy dany obiekt nie ma możliwości, by trzymać tę informację w obiekcie klasy State
. Uważam to za zaletę, ponieważ to po stronie obiektu docelowego powinna być odpowiedzialność pilnowania, od kiedy jest w danym stanie.
Wzorzec Pyłek definiuje dwa typy danych:
- Intrinsic (shared) – dane dzielone między innymi obiektami; W podanym przykładzie byłyby to obiekty klasy
State
. Wiele obiektów może mieć taki sam stan. - Extrinsic (unique) – dane specyficzne dla konkretnego obiektu. Może to być np. identyfikator, data utworzenia, zawartość tekstowa, koordynaty, itp. Te dane dostarczane są przez klienta Pyłka.
Kolejnym elementem wzorca Pyłek jest Fabryka Pyłków. To w niej tworzone są nowe Pyłki oraz reużywane już te istniejące. Zadaniem fabryki jest zdecydować czy można wykorzystać jeden z już istniejących obiektów, czy trzeba utworzyć nowy. Fabryka Pyłków (FlyweightFactory
) jest implementacją wzorca Fabryki omówionego w dedykowanym artykule.
Pyłek a Object Pool
Zasada działania Pyłka może wydawać się zbliżona do zasady działania wzorca Object Pool. Jednak jest między nimi kilka zasadniczych różnic:
- Celem Pyłka jest zaoszczędzenie pamięci przez współdzielenie danych/obiektów, natomiast celem Object Pool jest ponowne użycie wcześniej utworzonych obiektów, aby ograniczyć ich kosztowne tworzenie i niszczenie;
- Pyłek minimalizuje zużycie pamięci przez wielokrotne wykorzystanie tych samych obiektów. Object Pool działa na zasadzie „wypożyczania” obiektów z puli, gdzie używany obiekt jest niedostępny dla innych wypożyczających oraz zwracany po użyciu.
- Obiekty „wypożyczane” z puli mogą być mutowalne, natomiast Pyłki powinny być niemutowalne.
Praktyczny przykład
W celu przedstawienia wzorca Pyłek w praktyce przygotowałem aplikację, która w konsoli przeglądarki renderuje „piksele” w postaci pojedynczego znaku we wskazanym kolorze. Jest to przykład edukacyjny, który nie ma zastosowania biznesowego. Obrazuje on jednak w prosty sposób zasadę działania omawanego wzorca.
Kod wykorzystuje mechanizm stylowania komunikatów dostępny w Console API. Zalecam uruchamiać kod w środowisku przeglądarkowym. Przykładowo w TypeScript Playground kod nie zwróci poprawnych rezultatów. Wystarczy skopiować rezultat transpilacji z TypeScript Playground i wkleić w konsoli przeglądarki.
interface IPixelDefinition {
text: string;
color: string;
};
interface IPixel {
render(): IPixelDefinition;
}
class Pixel implements IPixel {
constructor( private readonly _color: string ) {}
public render(): IPixelDefinition {
return { text: '%cx', color: `color: ${ this._color }` };
}
}
class PixelFactory {
private _pixels: { [ key: string ]: IPixel } = {};
get(color: string): IPixel {
if ( !this._pixels[ color ] ) {
this._pixels[ color ] = new Pixel( color );
console.log( `Creating new pixel: ${ color }` );
} else {
console.log( `Reusing existing pixel: ${ color }` );
}
return this._pixels[ color ];
}
}
class ImagePrinter {
public render( image: IPixel[][] ): void {
for ( const row of image ) {
const rowText = []
const rowColors = []
for ( const pixel of row ) {
const { text, color } = pixel.render();
rowText.push( text );
rowColors.push( color );
}
console.log( rowText.join( '' ), ...rowColors );
}
}
}
const pf = new PixelFactory();
const imageV = [
[pf.get('red'),pf.get('white'),pf.get('white'),pf.get('white'),pf.get('red')],
[pf.get('red'),pf.get('white'),pf.get('white'),pf.get('white'),pf.get('red')],
[pf.get('white'),pf.get('red'),pf.get('white'),pf.get('red'),pf.get('white')],
[pf.get('white'),pf.get('red'),pf.get('white'),pf.get('red'),pf.get('white')],
[pf.get('white'),pf.get('white'),pf.get('red'),pf.get('white'),pf.get('white')]
];
const printer = new ImagePrinter();
printer.render( imageV );
Po uruchomieniu kodu w konsoli przegladarki pojawi się kształt V w kolorze czerwonym.
Obiekt klasy Pixel
nie zawiera informacji o swoim położeniu, dzięki czemu można go łatwo użyć ponownie. Pixel
jest tytułowym Pyłkiem. Prawdziwą siłę wzorca można by dostrzec przy obrazach o większych wymiarach. Przykładowo, chcąc wydrukować z wykorzystaniem przedstawionego kodu obraz o wymiarach 800 x 600 w kolorze białym i czerwonym, pozwala zredukować liczbę obiektów w pamięci z 480000 do dwóch. W przygotowanym kodzie liczba obiektów będzie równa liczbie wykorzystanych kolorów.
Obiekty klasy Pixel
tworzone są za pomocą.PixelFactory
. Jednocześnie fabryka zapamiętuje stworzone obiekty i reużywa istniejącego obiektu, jeśli piksel w danym kolorze już istnieje w pamięci. Stworzone piksele zapisane w fabryce to obiekty intrinsic.
Z obiektów klasy Pixel
tworzona jest struktura IPixel[][]
reprezentująca obrazek. W tablicy przechowywane są tablice reprezentujące poszczególne wiersze. Struktura pikseli to stan zewnętrzny (extrinsic state). Obiekt klasy ImagePrinter
jest klientem używającym Pyłków, który iteruje przez obiekty Pixel
i renderuje obraz.
Zgadzam się, że ten sam efekt dałoby się uzyskać bez instancjonowania obiektów i wykorzystaniu wzorca. Jednak celem tego artykułu jest pokazanie wzorca, a zależało mi na tym, by przykład był prosty.
Korzystając z tego mechanizmu, w analogiczny sposób można tworzyć inne kształty w innych kolorach. Poniżej przykład kodu dla litery X.
const imageX = [
[pf.get('red'),pf.get('white'),pf.get('white'),pf.get('white'),pf.get('red')],
[pf.get('white'),pf.get('red'),pf.get('white'),pf.get('red'),pf.get('white')],
[pf.get('white'),pf.get('white'),pf.get('red'),pf.get('white'),pf.get('white')],
[pf.get('white'),pf.get('red'),pf.get('white'),pf.get('red'),pf.get('white')],
[pf.get('red'),pf.get('white'),pf.get('white'),pf.get('white'),pf.get('red')]
];
printer.render( imageX );
Podsumowanie
Jeśli podejście #oszczędzajramgdziekolwiekjesteś nie jest Ci obce, to wzorzec Pyłek przypadnie Ci do gustu. Jak przy większości wzorców jego wykorzystanie wiąże się z dodatkowym skomplikowaniem w kodzie. Zyskujemy jednak potencjalnie duże korzyści pod kątem oszczędności pamięci. Coś za coś. Daj znać, czy miałeś/aś okazję wykorzystać Pyłka w praktyce!
Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić, klikając TEN link. Kupując z tego linku, wspierasz rozwój bloga.
Źródła i materiały dodatkowe
- Design Patterns: Elements of Reusable Object-Oriented Software – Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994) str. 218
- Design patterns for humans – Flyweight
- SourceMaking – Flyweight Design Pattern
- Refactoring Guru – Flyweight
- Cezary Walenciuk – Flyweight, Pyłek : Wzorce projektowe C#
- Stack Overflow – What are the differences between Flyweight and Object Pool patterns?
- Stack Overflow – Colors in JavaScript console
- Wzorzec projektowy Factory (Fabryka)
Zapisz się na mailing i odbierz e-booka
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.