Autor: Arkadiusz Kotarski


Arkadiusz odezwał się do mnie około miesiąca temu. Wymieniliśmy kilka wiadomości dotyczących hostingu aplikacji oraz samego programowania. Wyszedł z ciekawą propozycją wsparcia bloga artykułami dotyczącymi wzorców projektowych w ramach własnej nauki.

Jak dobrze wiecie jestem otwarty na takie propozycje dlatego rozpoczeliśmy współpracę.

Arek ma 28 lat. Swoją przygodę z programowaniem rozpoczął 5 lat temu a zawodowo pracuje od 2018 roku. Obecnie poszerza swoją wiedzę w dziedzienie gier komputerowych, wykorzystania silnika Unity oraz oprogramowania do modelowania 3D. Chce dzielić się z nami swoją wiedzą oraz wspomagać rozwój bloga.

Mam nadzieję, że pierwszy wpis Arka zaowocuje kolejnymi artykułami poszerzającymi naszą wspólną wiedzę programistyczną
Wprowadzenie

Wzorzec projektowy Budowniczny należy do wzorców konstrukcyjnych. Pozwala na odseparowanie definiowania złożonych obiektów od ich reprezentacji.

Dzięki budowniczemu proces konstruowania jest zawsze taki sam, lecz "Produkt" czyli końcowy rezultat, może być różny. Dany wzorzec wykorzystuje podejście krok po kroku - oznacza to, że za pomocą publicznych metod ustawiamy czego w danej chwili potrzebujemy.

W ramach przykładu możemy odnieść się do 'opcji' dostępnych w grach: zmiana rozdzielczości czy szczegółowości tekstur nie zmienia rozgrywki - dostosowuje aplikacje do wymagań gracza. Konstruowanie skomplikowanych obiektów bez użycia wzorca budowniczego może doprowadzić do powstania obszernego konstruktora z wieloma liniami kodu, który jest nieczytelny i trudny w zarządzaniu. Ponadto, tworzenie dużych konstruktorów może doprowadzić do tego, że nie wszystkie zmienne zostaną ustawione, lub są niepotrzebne dla wszystkich stworzonych obiektów.

Spójrzcie na poniższy diagram obrazujący wzorzec konstrukcyjny: Diagram UML reprezentujący wzorzec konstrukcyjny Builder

Problem

Analogią do działania wzorca Budowniczego jest każda firma gdzie występuje kierownik i przynajmniej dwóch pracowników. W ramach szczegółowego omówienia wzorca dokonam porównania do zespołu game developerów, którzy pracują nad grą z niezależnymi od siebie poziomami.

Wyobraź sobie teraz, że jeden programista pracuje nad wszystkimi etapami - jest to bardzo nieefektywne ze względu na ograniczony czas i ilość pracy. Ogrom pracy stojący przed nim przywodzi na myśl niezwykle złożony konstruktor. Po pewnym czasie ta osoba po prostu sama się zgubi w dużej ilości danych, które musi przetworzyć.

Wzorzec Budowniczy organizuje pracę, dzieli ją na wszystkich dostępnych członków zespołu. Klasa GameDirector jest kierownikiem zespołu: wydaje polecenia mówiąc co ma zostać zrobione.

Developerzy(jako grupa) opisani są interfejsem ILevelBuilder - każdy programista w tym zespole wie jak zrobić poziom do gry.

Każdy z nich zdefiniowany jest jako ConcreteBuilder. Klasa ta dostarcza nam gotowy etap gry, czyli produkt: w naszym przypadku obiekt klasy Level.

Jeżeli nie wszystko jest dla Was jasne spójrzcie na poniższy diagram: Budowniczy: diagram implementacji

Implementacja

W ramach implementacji przygotujemy aplikację konsolową bazującą na powyższym opisie oraz diagramie UML. W kodzie zawarte są niezbędne komentarze ułatwiające interpretację:

// Dyrektor
class GameDirector 
{	
	// Wydanie polecenia zbudowania poziomu krok po kroku
    public void BuildLevel(ILevelBuilder levelBuilder)
    {
        levelBuilder.AddFloor();
        levelBuilder.AddEnvironment();
        levelBuilder.AddNpc();
    }
}

// Zespół programistów 
// interface po którym dziedziczą konkretni budowniczy
interface ILevelBuilder
{
    void AddFloor();
    void AddNpc();
    void AddEnvironment();
}

// Konkretny Budowniczy 1
class IceLevelConcreteBuilder : ILevelBuilder
{
    private Level level = new Level();

    public void AddEnvironment()
    {
        level.AddEnvironment("Ice blocks");
    }

    public void AddFloor()
    {
        level.AddFloor("Slippery floor");
    }

    public void AddNpc()
    {
        level.AddNpc("Ice mag");
    }

    public Level GetLevel()
    {
        return this.level;
    }
}

// Konkretny Budowniczy 2
class FireLevel : ILevelBuilder
{
    private Level level = new Level();

    public void AddEnvironment()
    {
        level.AddEnvironment("Red blocks");
    }

    public void AddFloor()
    {
        level.AddFloor("Hot floor");
    }

    public void AddNpc()
    {
        level.AddNpc("Fenix");
    }

    public Level GetLevel()
    {
        return this.level;
    }
}

// Produkt
class Level
{
    private string environment;
    private string floor;
    private string npc;

    public void AddEnvironment(string environment)
    {
        this.environment = environment;
    }

    public void AddFloor(string floor)
    {
        this.floor = floor;
    }

    public void AddNpc(string npc)
    {
        this.npc = npc;
    }

    public void Show()
    {
        Console.WriteLine(this.environment);
        Console.WriteLine(this.floor);
        Console.WriteLine(this.npc);
    }
}

// Główna klasa w aplikacji consolowej
class Program
{
    // Główna metoda aplikacji
    static void Main(string[] args)
    {
        // Tworzenie dyrektora i konkretnych budowniczych
        GameDirector director = new GameDirector();

        FireLevelConcreteBuilder fireLevel = new FireLevelConcreteBuilder();
        IceLevelConcreteBuilder iceLevel = new IceLevelConcreteBuilder ();
        
        // Tworzenie dwóch poziomów i wypisanie wiadomości o nich
        Console.WriteLine("Fire Level:");            
        director.BuildLevel(fireLevel);
        Level level = fireLevel.GetLevel();
        level.Show();

        Console.WriteLine("Ice Level:");            
        director.BuildLevel(iceLevel);
        Level level2 = iceLevel.GetLevel();
        level2.Show();
        
        // Czekanie na reakcje użytkownika
        Console.ReadLine();
    }
}

Podsumowanie

Wzorzec konstrukcyjny Budowniczny jest podobny do wzorca fabryki omawianego na tym blogu. Jeżeli checie dowiedzieć się o nim więcej zajrzycie tutaj: Wzorce projektowe - wzorzec fabryki

Jakie są zatem różnice pomiędzy nimi? Wzorca budowniczego używamy do ciężkich obiektów, czyli takich których stworzenie zajmuje dużo czasu.

Zaletą wykorzystania wzorca jest możliwość kontrolowania sposobu tworzenia danego obiektu. Prostym przykładem wykorzystania budowniczego jest uniknęcie powtórzeń w przygotowanym kodzie.

Główna wada wzorca wynika z prostoty jego implementacji - może być przez nas stosowany tam, gdzie tak naprawdę nie jest potrzebny.