Wzorzec projektowy Pyłek - okładka

Wzorzec projektowy Pyłek

Opublikowano Kategorie Czysty kodCzas czytania 7min

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.

Diagram klas z klasą Object oraz właściwością state o klasie State.

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.

Diagram klas z klasą Object oraz właściwością state o klasie State rozszerzony o property stateChangedAt

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.
Output w konsoli przeglądarki z przedstawionego kodu - czerwona litera V ułożona z wielu znaków.

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 );

Output w konsoli przeglądarki z przedstawionego kodu - czerwona litera X ułożona z wielu znaków.

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

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

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.

Subscribe
Powiadom o
guest

0 komentarzy
oceniany
najnowszy najstarszy
Inline Feedbacks
View all comments