IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 1 - Factory (tovární metoda)

V tutoriálu Návrhové vzory GoF si představíme návrhový vzor Factory, který odděluje vytváření instance od samotného programu. Ukážeme si, jaké další možnosti inicializace poskytuje oproti klasickému konstruktoru.

Motivace

V aplikacích se nám občas stává, že potřebujeme vytvořit instanci nějaké třídy a tu dodatečně inicializovat. Dobrým praktickým příkladem jsou formulářové komponenty, u kterých nestačí pouze instanci vytvořit, ale musíme ji také nastavit spoustu dalších vlastností (rozměry, titulek, pozici, barvu...). Pokud někde v aplikaci vytváříme 20 podobných tlačítek a vytvoření takového tlačítka zabírá 10 řádků, nutně nás napadne oddělit tento kód do metody.

Factory může také uchovávat proměnné, které potřebujeme k vytváření instancí. Tyto proměnné potom nemusí prostupovat celou aplikací. Další výhodou je návratový typ, které nemusí být u Factory specifikován přesně na typ objektu, který vytváříme. Můžeme vracet některou z rodičovských tříd nebo i rozhraní. Na každý z těchto případů se podíváme blíže.

Vzor se nachází v různých podobách a existují i jeho další variace (factory, abstract factory, factory method, simple factory) a různé materiály je často vykládají různým způsobem. Základní myšlenka je ovšem vždy stejná.

Definice vzoru Factory

Návrhový vzor Factory (nebo též počeštěně faktorka) je jeden z nejdůležitějších návrhových vzorů, který umožňuje vyšší abstrakci při vytváření třídy než klasický konstruktor. Typicky se používá pro zapouzdření složitější inicializace instance a pro vytváření různých typů instancí podle řetězce. Můžeme ho rozdělit na vzor Factory Method a samotný Factory.

Factory Method

Návrhový vzor Factory method využívá metody volající konstruktor. Má poměrně mnoho různých podob. Někdy může být použito dědění, ovšem většinou se píše proti rozhraní. My si zde ukážeme úplně nejjednodušší implementaci. Klíčové je oddělení konstrukce konkrétní instance do jiné třídy, čímž se původní třída neznečistí konstrukčním kódem.

Ukažme si příklad:

  • class Auto
    {
        private string znacka;
        private string model;
    
        public Auto(string znacka, string model)
        {
            this.znacka = znacka;
            this.model = model;
        }
    
    }
    
    class TovarnaNaAuta
    {
        public Auto VytvorFelicii()
        {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto {
        private String znacka;
        private String model;
    
        public Auto(String znacka, String model) {
            this.znacka = znacka;
            this.model = model;
        }
    }
    
    class TovarnaNaAuta {
        public Auto vytvorFelicii() {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto {
        // od PHP verze 7.4 mohou mít vlastnosti třídy datové typy
        private string $znacka;
        private string $model;
    
        // od PHP verze 7 mohou mít parametry metod datové typy
        public function __construct(string $znacka, string $model) {
            $this->znacka = $znacka;
            $this->model = $model;
        }
    
    }
    
    class TovarnaNaAuta {
        // od PHP verze 7.1 může mít metoda datový typ
        public function VytvorFelicii() : Auto {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto {
        constructor(znacka, model) {
            this.znacka = znacka;
            this.model = model;
        }
    }
    class TovarnaNaAuta {
        VytvorFelicii() {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto:
        def __init__(self, znacka, model):
            self.znacka = znacka
            self.model = model
    
    class TovarnaNaAuta:
        def vytvorFelicii(self):
            return Auto("Škoda", "Felicia")

Máme zde jednoduchou třídu s veřejným konstruktorem. Samozřejmě můžeme tvořit konkrétní instance automobilů:

  • Auto fabia = new Auto("Škoda", "Fabia");
  • Auto fabia = new Auto("Škoda", "Fabia");
  • $fabia = new Auto("Škoda", "Fabia");
  • fabia = new Auto("Škoda", "Fabia");
  • fabia = Auto("Škoda", "Fabia")

Jelikož v naší aplikaci často tvoříme Felicie nebo pro nás mají zkrátka nějaký vyšší význam a zároveň nechceme zasahovat do třídy Auto, je jejich konstrukce zjednodušena na pouhé zavolání metody tovární třídy:

  • TovarnaNaAuta tovarna = new TovarnaNaAuta();
    Auto felicia = tovarna.VytvorFelicii();
  • TovarnaNaAuta tovarna = new TovarnaNaAuta();
    Auto felicia = tovarna.vytvorFelicii();
  • $tovarna = new TovarnaNaAuta();
    $felicia = $tovarna->VytvorFelicii();
  • tovarna = new TovarnaNaAuta();
    felicia = tovarna.VytvorFelicii();
  • tovarna = TovarnaNaAuta()
    felicia = tovarna.vytvorFelicii()

Když si představíme, že Felicie má automaticky nastavených např. 30 atributů, tak se vzor určitě vyplatí. A i kdybychom Felicii potřebovali jen na jednom místě v programu, oddělení složité inicializace do jiné třídy zpřehlední další logiku ve třídě, kde instanci potřebujeme.

Factory

Obecnějším návrhem je Factory. V principu se jedná opět o Factory Method, protože to musí být opět metoda, která naši instanci vytváří. Zde jsou požadavky mnohem volnější. Metoda může být definována jako statická (někdy i v té samé třídě, s tím se setkáváme typicky u Javy).

Ukažme si příklad:

  • class Auto
    {
        private string znacka;
        private string model;
    
        private Auto(string znacka, string model)
        {
            this.model = model;
            this.znacka = znacka;
        }
    
        public static Auto Felicia()
        {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto {
        private String znacka;
        private String model;
    
        private Auto(String znacka, String model) {
            this.znacka = znacka;
            this.model = model;
        }
    
        public static Auto Felicia() {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto {
        // od PHP verze 7.4 mohou mít vlastnosti třídy datové typy
        private string $znacka;
        private string $model;
    
        // od PHP verze 7 mohou mít parametry metod datové typy
        public function __construct(string $znacka, string $model) {
            $this->znacka = $znacka;
            $this->model = $model;
        }
    
        // od PHP verze 7.1 může mít metoda datový typ
        public static function Felicia() : Auto {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto {
        constructor(znacka, model) {
            this.znacka = znacka;
            this.model = model;
        }
    
        static Felicia() {
            return new Auto("Škoda", "Felicia");
        }
    }
  • class Auto:
        def __init__(self, znacka: str, model: str):
            self.model = model
            self.znacka = znacka
    
        @staticmethod
        def Felicia():
            return Auto("Škoda", "Felicia")

V této variantě vzoru se instance třídy nedá vytvořit žádným jiným způsobem, než tovární metodou (ale samozřejmě může být konstruktor i veřejný).

Instanci vytvoříme takto:

  • Auto felicia = Auto.Felicia();
  • Auto felicia = Auto.Felicia();
  • $felicia = Auto::Felicia();
  • felicia = Auto.Felicia();
  • felicia = Auto.Felicia()

Výhodou statické metody přímo ve třídě je jednodušší implementace. Samozřejmě bychom jich neměli mít ve třídě moc a měly by být nějak vázané na původní funkcionalitu třídy, jinak by měly být v samostatné třídě. Asi nejsprávnějším příkladem takové statické metody je získání aktuálního data a času v C# .NET:

  • DateTime listopad = new DateTime(2020, 11, 24); // Konkrétní datum
    DateTime dnes = DateTime.Now();
  • LocalDate listopad = LocalDate.of(2020, 11, 24); // Konkrétní datum
    LocalDate dnes = LocalDate.now();
  • $listopad = new DateTime("2020-11-24");
    // PHP nemá metodu Now()
    $dnes = date_create();
  • listopad = new Date("2020-11-24");
    dnes = new Date.now();
  • listopad = datetime(2020, 11, 24) # Konkrétní datum
    dnes = datetime.now()

Metoda Now() vrátí instanci typu DateTime, inicializovanou na aktuální datum a čas. Taková metoda přímo souvisí s funkcionalitou datového typu DateTime, a proto je návrhově správně, že je v této třídě a i statika zde dává smysl. Naopak u naší metody Felicia() je to spíše odstrašující příklad, protože s obecnou třídou Auto příliš nesouvisí.

V C# .NET je Now() vlastnost, která se od metody v nějakých detailech odlišuje. V příkladu byla uvedena jako metoda, aby zbytečně nezmátla programátory v jiných jazycích.

Vytváření instancí různých tříd

Návratová hodnota nemusí být u vzoru Faktory stejná, jako je typ vytvářené instance. Klidně můžeme vytvářet při každém volání instanci jiného typu třídy a vracet pouze rozhraní, které všechny třídy implementují.

Jako příklad si uveďme grafický program, který vykresluje různé obrazce na obrazovku. Řekněme tedy, že máme čtverec, kruh a trojúhelník, které všechny implementují rozhraní IVykreslitelny. Data získáváme z řetězce (např. parsujeme nějaký textový soubor). Podle dodaných dat se rozhodneme, který typ tvaru vytvořit a vrátíme jej pouze jako rozhraní. Samotný program neví, co je to za obrazec. Pouze ví, že jej lze vykreslit.

Příklad grafického programu je následující:

  • interface IVykreslitelny
    {
        void Vykresli();
    }
    
    class Ctverec : IVykreslitelny { /* ... Kód ... */ }
    class Trojuhelnik : IVykreslitelny { /* ... Kód ... */ }
    class Kruh : IVykreslitelny { /* ... Kód ... */ }
    
    class TvarFactory
    {
        public IVykreslitelny Vytvor(string typ)
        {
            if (typ == "Ctverec")
                return new Ctverec();
            else if (typ == "Trojuhelnik")
                return new Trojuhelnik();
            else if (typ == "Kruh")
                return new Kruh();
        }
    }
    
    // použití v programu
    TvarFactory faktorka = new TvarFactory();
    IVykreslitelny instance = faktorka.Vytvor("Ctverec");
  • interface IVykreslitelny {
        void vykresli();
    }
    
    class Ctverec implements IVykreslitelny { /* ... Kód ... */ }
    class Trojuhelnik implements IVykreslitelny { /* ... Kód ... */ }
    class Kruh implements IVykreslitelny { /* ... Kód ... */ }
    
    class TvarFactory {
        public IVykreslitelny vytvor(String typ) {
            if (typ.equals("Ctverec"))
                return new Ctverec();
            else if (typ.equals("Trojuhelnik"))
                return new Trojuhelnik();
            else if (typ.equals("Kruh"))
                return new Kruh();
            return null;
        }
    }
    
    // použití v programu
    TvarFactory faktorka = new TvarFactory();
    IVykreslitelny instance = faktorka.vytvor("Ctverec");
  • interface IVykreslitelny {
        public function Vykresli();
    }
    
    class Ctverec implements IVykreslitelny { /* ... Kód ... */ }
    class Trojuhelnik implements IVykreslitelny { /* ... Kód ... */ }
    class Kruh implements IVykreslitelny { /* ... Kód ... */ }
    
    class TvarFactory {
        // Návratový typ metody je možný od verze 7.1
        public function Vytvor(string $typ) : IVykreslitelny {
            if ($typ == "Ctverec")
                return new Ctverec();
            else if ($typ == "Trojuhelnik")
                return new Trojuhelnik();
            else if ($typ == "Kruh")
                return new Kruh();
        }
    }
    
    // použití v programu
    $faktorka = new TvarFactory();
    $instance = $faktorka->Vytvor("Ctverec");
  • // JS nemá interface, můžeme to však alternativně zajistit takto:
    function TvarFactory() {
        this.vytvor = function (typ) {
            if (typ === "Ctverec") {
                return new Ctverec();
            } else if (typ === "Trojuhelnik") {
                return new Trojuhelnik();
            } else if (typ === "Kruh") {
                return new Kruh();
            }
        }
    }
    
    let Ctverec = function () {
        /* ... Kód ... */
    };
    
    let Trojuhelnik = function () {
        /* ... Kód ... */
    };
    
    let Kruh = function () {
        /* ... Kód ... */
    };
    
    // použití v programu
    let faktorka = new TvarFactory();
    let instance = faktorka.vytvor("Ctverec");
  • # Python nemá interface, můžeme to však alternativně zajistit takto:
    
    class Vykreslitelny:
        def vykresli(self):
            pass
    
    class Ctverec(Vykreslitelny):
        # kód ...
        pass
    
    class Trojuhelnik(Vykreslitelny):
        # kód ...
        pass
    
    class Kruh(Vykreslitelny):
        # kód ...
        pass
    
    class TvarFactory:
        def vytvor(typ):
            if typ == "Ctverec":
                return Ctverec()
            elif typ == "Trojuhelnik":
                return Trojuhelnik()
            elif typ == "Kruh":
                return Kruh()
    
    # použití v programu:
    faktorka = TvarFactory()
    instance = faktorka.vytvor("Ctverec")

Vidíme, že typ se mění. Jediné, co program ví, je, že lze objekt vykreslit, ale již nic o tom, jak se vykreslí. To by jej ale nemělo ani zajímat, protože by se o vykreslení starat neměl. Buď se o vykreslení stará opět konkrétní třída nebo samotné objekty.

Pokud by tvarů bylo více typů, je výhodnější použít reflexi a dynamicky vytvářet instance tříd, jejichž název koresponduje s názvem tvaru. Nesmíme zde zapomenout nějak ošetřit, aby bylo vytváření tříd omezeno na určitý balíček. Jinak by si uživatel mohl vytvořit prakticky cokoli z naší aplikace.

Grafické zobrazení kódu uvedeného výše v UML doménovém modelu:

Návrhový vzor Factory - Návrhové vzory GoF

Závislost na parametrech

Někdy potřebujeme vytvořit instanci třídy, ale parametry pro konstruktor získáme na jiném místě v programu. Konstruktor využít nemůžeme, protože ještě neznáme všechny parametry a vytvořit neinicializovanou instanci např. bezparametrickým konstruktorem není dobrá praktika.

Pokud využijeme třídu Factory, můžeme jí data postupně předávat a ona si je bude uchovávat. Ve chvíli, kdy dat bude dostatek, můžeme instanci vytvořit. Nemusíme se tedy starat o žádné předávání dat mezi částmi programu, předáme pouze instanci třídy Factory.

V další lekci, Singleton (jedináček), si ukážeme populární návrhový vzor Singleton umožňující globální přístup k instanci nějaké třídy. Ukážeme si výhody i nevýhody a zkusíme si ho i naprogramovat.


 

Všechny články v sekci
Návrhové vzory GoF
Přeskočit článek
(nedoporučujeme)
Singleton (jedináček)
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
221 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity