Nahoru
 

Návrhový vzor stavitel

Objektově orientované programování poskytuje mnoho různých flexibilních a rozšiřitelných řešení pro časté problémy, s nimiž se jako vývojáři setkáváme. Jedním z takových problémů je vytváření složitě strukturovaných objektů, a právě zde lze využít návrhový vzor stavitel.

Stavitel (anglicky builder) patří do návrhových vzorů rodiny GoF (Gang of Four design patterns). Jedná se o vytvářecí vzor, jehož využití nalezneme při konstrukci komplexních objektů. Zároveň odděluje samotnou výrobu objektů od jejich reprezentace, čímž dosáhneme snadné budoucí rozšiřitelnosti.

Obecně vypadá struktura stavitele tak, že máme abstraktní třídu stavitel (builder) udávající rozhraní pro implementace jednotlivých konkrétních stavitelů (concrete builder), jež z něj dědí. Zároveň se zde vyskytuje tzv. direktor (director), který pomocí rozhraní stavitele konstruuje komplexní objekty někdy nazývané produkty.

UML diagram stavitele
Obrázek č. 1: UML diagram obecné struktury stavitele

Příklad stavitele

Nejlépe půjde ukázat sílu stavitele na příkladu. Pro naše potřeby si vymyslíme fiktivní program, který bude mít za úkol převádět komplexní objekt auta (Car) do textových formátů XML a JSON. Jde o prostou serializaci objektu do cílového formátu. K autům si udržujeme velké množství informací, jež si pro naše účely rozdělíme do tří kategorií: hlavní informace (jméno auta, prodejce, …), parametry (najeté kilometry, vybavení, …) a obrázky.

Účel stavitele mimo jiné spočívá v možnosti budovat produkty po částech. V našem případě chceme mít oddělené vytvoření:

  • hlavičky XML a JSONu obsahující hlavní informace o autě
  • části XML a JSONu pro parametry auta
  • části XML a JSONu pro obrázky auta
UML diagram stavitele pro příklad
Obrázek č. 2: UML diagram struktury stavitele pro příklad

Následující kód příkladu je psán v programovacím jazyce C#. Projekt s kódem je veřejně dostupný v repositáři na GitHub.

Začněme s vytvořením abstraktní třídy CarSerializer, která pro nás představuje abstraktní třídu stavitele. V něm definujeme obecné rozhraní pro výrobu našich produktů.

/* náš stavitel, třída udávající rozhraní pro vytváření
   komplexních objektů */
abstract class CarSerializer
{
    // metody pro vytváření částí objektu
    public abstract void BuildHeader(Car car);
    public abstract void AddParams(Car car);
    public abstract void AddImages(Car car);

    // metoda pro navrácení vytvořeného produktu
    public abstract string GetSerializedCar();
}

Můžeme si všimnout, že rozdělení výroby produktů na námi předem definované části uvádíme již v této třídě. Zároveň se zde vyskytuje metoda GetSerializedCar(), která vrací hotový zkonstruovaný produkt.

S hotovým rozhraním stavitele se lze pustit do konkrétních stavitelů JsonCarSerializer a XmlCarSerializer, jež z CarSerializer dědí a obsahují konkrétní implementace pro výrobu produktu.

// konkrétní stavitel pro formát json
class JsonCarSerializer : CarSerializer
{
    private string jsonCar;

    public override void BuildHeader(Car car)
    {
        // zde se vytvoří header pro json
	 ...
    }

    public override void AddImages(Car car)
    {
        // zde se přidají do produktu obrázky auta pro json
  	 ...
    }

    public override void AddParams(Car car)
    {
        // zde se přidají do produktu parametry auta pro json
	 ...
    }

    public override string GetSerializedCar()
    {
        // zde se v případě potřeby doladí finalizace json
        // a poté se vrátí hotový produkt json
	 ...
        return jsonCar;
    }
}
// konkrétní stavitel pro formát xml
class XmlCarSerializer : CarSerializer
{
    private string xmlCar;

    public override void BuildHeader(Car car)
    {
        // zde se vytvoří header pro xml
	 ...
    }

    public override void AddImages(Car car)
    {
        // zde se přidají do produktu obrázky auta pro xml
	 ...
    }

    public override void AddParams(Car car)
    {
        // zde se přidají do produktu parametry auta pro xml
	 ...
    }

    public override string GetSerializedCar()
    {
        // zde se v případě potřeby doladí finalizace xml
        // a poté se vrátí hotový produkt xml
	 ...
        return xmlCar;
    }
}

Budování objektů jako takové máme nyní hotové, ovšem chybí nám zde ještě direktor. Ten se zaslouží o zapouzdření konstrukce jednotlivých částí, které bude pomocí dodaného konkrétního stavitele vytvářet.

// náš direktor, třída obsluhující konstrukci objektu
class Director
{
    private CarSerializer carSerializer;

    public Director(CarSerializer carSerializer)
    {
        this.carSerializer = carSerializer;
    }

    // metoda pro serializaci auta se všemi jeho částmi
    public string SerializeCar(Car car)
    {
        carSerializer.BuildHeader(car);
        carSerializer.AddImages(car);
        carSerializer.AddParams(car);

        return carSerializer.GetSerializedCar();
    }

    // metoda pro serializaci auta bez obrázků
    public string SerializeCarWithoutImages(Car car)
    {
        carSerializer.BuildHeader(car);
        carSerializer.AddParams(car);

        return carSerializer.GetSerializedCar();
    }

    // metoda pro serializaci auta bez parametrů
    public string SerializeCarWithoutParams(Car car)
    {
        carSerializer.BuildHeader(car);
        carSerializer.AddImages(car);

        return carSerializer.GetSerializedCar();
    }

    // mohli bychom mít spoustu dalších metod upřesňujících
    // konstrukci serializovaného auta
}

Konkrétního stavitele direktorovi předáme už v konstruktoru a on poté pomocí obecného rozhraní stavitele může libovolně vytvářet námi kýžené produkty. Zde jsou na ukázku tři možnosti, jak auto serializovat, jednou jako celek a poté bez obrázků či parametrů.

Tím je náš úkol splněn a my nyní můžeme tento modul naplno využívat. Jak by mohlo vypadat konkrétní využití demonstruje následující kód.

void Client()
{
    // náš klient nyní může podle požadavků vytvářet 
    // serializovaná auta jak ve formátu json, tak ve
    // formátu xml -> stačí k tomu pouze výběr konkrétního
    // stavitele (CarSerializer)

    Car car = GetCar(); // načtení auta z databáze
    CarSerializer carSerializer;
    Director director;

    // řekněme, že chceme serializovat auto pro json
    carSerializer = new JsonCarSerializer();
    director = new Director(carSerializer);

    // nyní můžeme provést samotnou serializaci (například bez obrazků)
    string serializedCar = director.SerializeCarWithoutImages(car);
}

Výhody použití stavitele

Nyní nastává otázka, proč jsme vytvořili námi zadaný program zrovna tímhle způsobem. Na první pohled se dané řešení může zdát dokonce až nesmyslně komplikované, avšak zahrnuje spoustu výhod.

Jako první uveďme rozšiřitelnost. Co kdybychom chtěli do aplikace přidat možnost serializovat do dalšího jiného formátu? Není problém, stačí vytvořit novou třídu dědící ze stavitele a implementovat jeho rozhraní. Díky oddělení kódu pro konkrétní implementaci a reprezentaci (rozhraní) stavitele máme zajištěnou velmi jednoduchou rozšiřitelnost.

Druhá výhoda se skrývá v možnosti různé vnitřní reprezentace produktu. V našem příkladu jsme měli dva konkrétní produkty: JSON a XML. Ačkoliv sdílí stejný datový typ (string), jejich vnitřní reprezentace se velmi liší. Pomocí stavitele lze vyrábět všemožné rozmanité produkty se zachováním stejného rozhraní.

Poslední velkou výhodu lze spatřit v „jemné“ kontrole nad výrobním procesem. „Jemnou“ kontrolou máme na mysli budování produktu po částech, což je vlastně jedním z účelů stavitele. U velice složitého budování objektů takovou vlastnost vyhledáváme.

Rozdělení výroby na části je hlavním rozdílem mezi návrhovým vzorem stavitel a abstraktní továrna. Tyto dva návrhové vzory si jsou velmi blízké. Oba patří do kategorie vytvářecích GoF návrhových vzorů a sdílí výhody rozšiřitelnosti a možnosti různé vnitřní reprezentace konstruovaných objektů. Abstraktní továrna se oproti staviteli soustředí spíše na výrobu stejné „rodiny“ objektů, kdežto stavitel na členěnou konstrukci složitého objektu. Více o abstraktní továrně se můžete dozvědět v našem předešlém článku Abstraktní továrna.

Návrhový vzor stavitel se v praxi běžně používá a jeho znalost je velkou výhodou. Jakmile se vyskytne problém konstrukce složitých objektů s velkou pravděpodobností potřeby budoucího rozšíření, použití stavitele je správná volba.

Zdroje: