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:
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.