C#: Návrhový vzor Builder s dědičností

Návrhový vzor Builder je velmi užitečný v případě, že potřebujeme zapouzdřit a zjednodušit konstrukci složitějšího objektu. Spolu s návrhovým vzorem fluent interface nám umožní vytvořit praktické API, které může být součástí knihovny a je okamžitě pochopitelné pro ostatní vývojáře. Co když však přidáme dědičnost?

Builder s dědičností

Představme si, že chceme vytvořit fluent builder pro dva zděděné typy abstraktní třídy   Game  – LocalGame  a OnlineGame . Oba typy her mají některé vlastnosti společné (jako velikost hrací plochy nebo level), ale každý má určité chování navíc  (lokální hra může přidat nastavení obtížnosti umělé inteligence, on-line hra zase URL serveru, na kterém chceme hrát).

Abstraktní  GameBuilder  by mohl vypadat takto:

A jeho zděděné verze LocalGameBuilder  a OnlineGameBuilder:

Okamžitě můžeme vidět jeden jeden nedostatek. Metoda Build , definovaná v GameBuilderu  jako abstraktní,  vrací základní typ  Game . To je náchylné k problémům.   LocalGameBuilder  například nyní může vyprodukovat jakýkoliv druh hry:

Musíme najít způsob, jak zajistit, aby byl návratový typ metody  Build  pouze ten konkrétní typ, který daný builder má vytvářet.

Generický builder

Generické typy jsou součástí .NETu od verze 2.0 a přichází nás zachránit!

Můžeme abstraktní třídu  GameBuilder  udělat generickou tak, aby typ, který metoda  Build  vrací, byl konkrétním zděděným typem hry.

Všimněme si, že pomocí klíčového slova where vytváříme omezení generického typu, abychom se ujistili, že parametr bude skutečně zděděným typem třídy Game .

Konkrétní builder může nyní specifikovat typ:

Aktuální verze vypadá na první pohled dobře. Dokonce funguje i naše fluent API:

Ale vše růžové není. Co když změníme pořadí volání metod v řetězovém volání?

Dotaneme chybu na třetí řádce – 'GameBuilder<OnlineGame>' does not contain a definition for 'ServerUrl' .

Důvod problému je fakt, že metoda Level  nevrací OnlineGameBuilder , ale jen základní GameBuilder<OnlineGame> , který vlastnost ServerUrl  nemá vůbec k dispozici.

Ještě více generický builder

Vraťme se tedy k abstraktnímu builderu a pokusme se zajistit, aby fluent metody vracely konkrétní zděděný typ builderu.

Použijeme návrhový vzor Curiously Recurring Template, který pochází z C++, ale lze jej analogicky použít i v C#.

Na předání konkrétního typu builderu musíme GameBuilderu  přidat nový typový parametr, který dědí od něho samotného:

Tento koncept není jednoduché myšlenkově pojmout. Zjednodušeně řečeno chceme, aby typový parametr TBuilder  byl implementací  GameBuilderu , která má daný typ hry jako první typový parametr a sebe sama jako typový parametr TBuilder .

Samotná definice vypadá poměrně zamotaně, ale při použití je již čitelnější:

To nám umožní přesně to, co jsme potřebovali – poskytnout abstraktní třídě typ konkrétního builderu na použití.

Nyní se ještě musíme vypořádat s jedním dalším problémem. Fluent metody vrací aktuální instaci třídy, aby bylo možné volání řetězit:

Ale to nyní nefunguje! Na čtvrtém řádku se pokoušíme vrátit this , což je pouze GameBuilder<TGame, TBuilder> , ne konkrétní builder ( TBuilder ).

Pomůžeme si malým trikem. Přidáme novou read-only vlastnost do abstraktního GameBuilderu :

protected abstract TBuilder BuilderInstance { get; }

Tato vlastnost bude implementována konkrétními buildery a umožní jim vrátit jejich vlastní instanci ( this).

Takže nyní má abstraktní builder přístup nejen k zděděnému typu ve forme typového parametru TBuilder, ale i k samotné instanci pomocí vlastnosti  BuilderInstance  bez potřeby přetypování. Fluent metody budou nyní mít následující podobu:

Když teď použijeme naše API k vytvoření hry, vidíme, že vždy dostaneme zpět instanci konkrétního builderu nezávisle na pořadí volání metod.

Finální podoba

Zde je finální podoba našich builderů:

Aktualizováno: Vylepšení od Ondřeje Kunce

Finální verzi lze ještě vylepšit, na což mě upozornil v komentářích tohoto článku Ondřej Kunc a moc mu tímto děkuji za skvělý tip!

Ukázalo se, že jsem zbytečně komplikoval zpracování this  pomocí abstraktní vlastnosti BuilderInstance . Problém lze vyřešit mnohem jednodušeji a to bez nutnosti, aby zděděné verze builderu vracely vlastní instanci (což může být i nebezepečné, protože by například mohly vrátit pokaždé úplně novou instanci, čímž by byla způsobena nefunkčnost builderu).

Místo toho můžeme do abstraktního builderu přidat readonly  položku, do které si v konstruktoru uložíme this přetypované na typ TBuilder . Máme garanci, že přetypování proběhne úspěšně díky omezením, které jsme na typový parametr stanovili.

Shrnutí

Začali jsme s jednocuchou negenerickou, k chybám náchylnou, podobou návrhového vzoru builder. Ten jsme rozšířili o generiku, abychom se ujistili, že buildery skutečně vytvářejí instance typu, který slibují. Nakonec jsme použili C# podobu návrhového vzoru curiously recurring template, který nám zajistil plnou funkčnost fluent API.

Ukázkový zdrojový kód si můžete prohlédnout a stáhnout na mém GitHubu.

2 odpovědi na “C#: Návrhový vzor Builder s dědičností”

  1. Super článek. Jen takový detail mě napadl. Nebylo by lepší nastavit BuilderInstance přímo v konstruktoru třídy GameBuilder? Třeba takhle:

    BuilderInstance = (TBuilder) this;

    1. To je pravda, tím by se to ještě zlepšilo 🙂 ! Protože je určitě typ TBuilder, tak je přetypování bezpečné a zděděné třídy nemusí zodpovídat za to, aby skutečně vracely this a ne třeba úplně novou instanci.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

*