Wzorzec projektowy Pyłek służy do zmniejszania zużycia pamięci przesz aplikację poprzez współdzielenie przez wiele obiektów wspólnych zasobów.
Pyłek należy do strukturalnych wzorców projektowych. Więcej o rodzajach wzorców można poczytać tutaj [under construction].
Rozważmy przykład prostej gry 2D. W grze tej pojawiają się postacie elfów i krasnoludów. Każdy obiekt postaci zawiera w sobie położenie na mapie, ilość punktów zdrowia oraz teksturę z obrazkiem. Zobaczmy kod:
abstract class Sprite { protected abstract string TextureLocation { get; } protected Texture _texture; public Sprite() { _texture = Texture.Load(TextureLocation); } public int X { get; set; } public int Y { get; set; } public int HealthPoints { get; set; } } class Elf : Sprite { protected override string TextureLocation => "textures/elf"; } class Dwarf : Sprite { protected override string TextureLocation => "textures/dwarf"; } //ciężki obiekt tekstury class Texture { internal static Texture Load(string textureLocation) { //wyobraźmy sobie, że tu ładujemy teksturę z pamięci return new Texture(); } }
Przyjrzyjmy się bliżej obiektowi tekstury. Klasy Elf i Krasnolud zawierają w sobie lokację pliku tekstury. W momencie tworzenia obiektu klas Elf lub Dwarf tekstura jest ładowana z dysku i przypisywana do nowo utworzonego obiektu. Możemy założyć, że każdy obiekt tekstury jest stosunkowo duży. Wyobraźmy sobie, że zaraz po włączeniu gry tworzymy 100 postaci elfów i 100 postaci krasnoludów:
var sprites = new List<Sprite>(); for(int i =0; i < 100; ++i) { var elf = new Elf(); sprites.Add(elf); var dwarf = new Dwarf(); sprites.Add(dwarf); }
W tej niewinnej pętli dzieje się bardzo zła rzecz. Mianowicie ładujemy każdą teksturę z dysku 100 razy. Po przejściu pętli mamy 200 obiektów tekstur w pamięci aplikacji. To bardzo dużo. Zastosujmy wzorzec Pyłek, by zoptymalizować zużycie pamięci. Refactoringiem, który nasuwa się sam, jest wykorzystanie faktu, że każdy elf i każdy krasnolud ma taką samą teksturę. Nie musimy więc ładować każdej z nich 100 razy - wystarczy raz. Każdy obiekt postaci będzie miał swoje własne współrzędne X i Y oraz poziom punktów zdrowia, ale będą się dzielić obiektem tekstury.
Zacznijmy od utworzenia klasy TextureLoader, która wczytywać będzie tekstury. Najpierw przenieśmy do niej istniejący kod:
class TextureLoader { public Texture Load(string textureLocation) { //wyobraźmy sobie, że tu ładujemy teksturę z pamięci return new Texture(); } } class Texture { }
Teraz zmieńmy tę klasę w ten sposób, by każdą teksturę ładowała co najwyżej raz:
class TextureLoader { private Dictionary<string, Texture> _loadedTextures = new Dictionary<string, Texture>(); public Texture Load(string textureLocation) { if(!_loadedTextures.ContainsKey(textureLocation)) { //wyobraźmy sobie, że tu ładujemy teksturę z pamięci _loadedTextures[textureLocation] = new Texture(); } return _loadedTextures[textureLocation]; } }
Zobaczmy, co się zmieniło. Metoda Load nie będzie wczytywać tekstury z pamięci, jeśli została już ona wczytana. Wczytane tekstury zapamiętywane są w prywatnym słowniku _loadedTextures. Z metody zwracany jest wpis z tego słownika. Oznacza to, że każdy obiekt klasy Elf będzie zawierał w sobie referencję do tego samego obiektu tekstury. Dzięki temu w pamięci będziemy mieć dwa obiekty tekstur zamiast dwustu.
Wzorzec Pyłek pozwolił nam na stukrotne zmniejszenie liczby przechowywanych w pamięci obiektów tekstur.
W poście tym korzystałam z:
- http://devman.pl/pl/techniki/wzorce-projektowe-pylekflyweight/
- https://www.tutorialspoint.com/design_pattern/flyweight_pattern.htm
Te nazwy TextureLoader i Load sugerują, że dalej dzieje się coś obciążającego zasoby. Może lepiej by je było zmienić na TextureSource i Get?
OdpowiedzUsuńPoza tym częściej spotykam się z implementacją, w ramach której poszczególne „pyłki” posiadają referencję do wspólnego dla nich ciężkiego obiektu, a nie uzyskują go z innej klasy na podstawie jakichś lżejszych danych (w tym przypadku na podstawie napisu). Oczywiście przykład tutaj zamieszczony robi to co trzeba i oszczędza zasoby, ale osobie wdrażającej ten wzorzec polecam zastanowić się nad bardziej klasyczną implementacją.
Paweł Ś.