Přejít k navigační liště

Zdroják » Různé » Náhrada návrhových vzorů ve funkcionálním programování

Náhrada návrhových vzorů ve funkcionálním programování

Články Různé

Programátoři ve funkcionálních jazycích nepoužívají terminologii návrhových vzorů. Používají stejné mechanismy, ale mají pro ně jiné názvy. Jednomu by se pak mohlo zdát, že design patterns jsou zbytečné, ono to tak není. Spíš funkcionální jazyky, tím, že jsou expresivnější, než jazyky objektové, můžou takový dojem pouze vytvářet.

Článek původně vyšel na autorově blogu. Jiří Knesl pomáhá IT firmám k vyššímu zisku pomocí zvyšování kvality a produktivity vývojářů a IT managementu. Více se o něm dozvíte na www.knesl.com.

Internetem proběhl takový drobný kopanec do návrhových vzorů:

fpvsoo

Samozřejmě, v tomto obrázku není žádná faktická chyba. Ve funkcionálním programování je vše řešeno funkcemi (nečekaně). Na levé straně mohly být všude nápisy „Classes and objects“. A pak co, prostě různá paradigmata používají různé výrazové prostředky.

Funkce jsou univerzálnější, ale použít objektový jazyk neznamená, že něco nejde naprogramovat.

Pojďme se podívat na to, jak funkcionální programování nahrazuje některé návrhové vzory.

Factory pattern

Návrhový vzor Factory funguje tak, že zodpovídá za vytváření, konfiguraci a mnohdy i Dependency Injection objektu, u kterého je tato činnost příliš složitá, než abyste ji měli v DI kontejneru, případně rozstrkanou po celé aplikaci, pokud nepoužíváte DI.

Factories bývají často statické třídy s metodou create, což je ale implementační detail, ne podmínka.

Ve funkcionálním programování můžeme chápat tuto situaci tak, že např. máme složitou funkci, která očekává řadu parametrů.

mojefunkce(fn1, fn2, config1, config2, config3, par1, par2)

Vše mimo par1 a par2 chceme předat jednou, z jednoho místa.

Jednoduché řešení je vytvořit novou funkci:

function mojefunkce2(par1, par2) {
        return mojefunkce(fn1, fn2, config1, config2, par1, par2);
}

Tato nová funkce nastaví fn1, fn2, config1, config2 z nějakých svých zdrojů stejným způsobem, jako to udělá factory.

Výsledný zdroják je o trošku jednodušší než OOP varianta, ale ne o moc.

Existuje ještě jedna věc, jak vše zjednodušit a to s pomocí automatic partial application, kterou jsem zde už mnohokrát zmiňoval.

Pak lze vytvoření takové factory zapsat jako:

mojefunkce2 = mojefunkce(fn1, fn2, config1, config2)

Předal jsem jen ty parametry, které znám, a nechám zbytek předat v okamžiku, kdy je funkce aplikována.

Strategy pattern

Strategy slouží pro situace, kdy máme více algoritmů, každý z nich může nahradit původní. Hodí se v situacích, kdy například komprimujete data, vypočítáváte cenu pro zákazníka, renderujete detail položky atd.

Principem, stejně jako u návrhového vzoru Adapter, je stejné rozhraní.

U funkcionálního programování není téměř co ukazovat, princip je stejný. Vytvoříte funkce, které čekají stejné parametry a máte hotovo. Tzn. místo toho, abyste měli X tříd, které implementují interface IPricingStrategy, máte x funkcí se stejným interfacem, jako by měla metoda počítající cenu v realizaci IPricingStrategy (za předpokladu, že jste už předali nějak závislosti, např. postupem, který jsem popsal v kapitole Factory pattern).

Decorator patten

Návrhový vzor Decorator se nějčastěji používá pro situace, kdy potřebujete rozšířit funkčnost existující třídy bez toho, abyste do ní zasáhli a bez toho, aby ona třída měla nějakou explicitní podporu pro vlastní rozšiřování (typicky tak, že čeká, že jí předáte nějaké dependencies, které tato třída bude volat).

Nejčastější realizace je buď přes dědičnost a překrytí metod, jejichž chování měníte, nebo přes delegaci, kdy původní objekt držíte v atributu a kdy implementujete stejný interface, všechna volání delegujete dál mimo těch, kde chcete nějaké své vlastní chování.

Ve funkcionálním programování byste problém mohli řešit podobným způsobem. Měli byste funkci, která implementuje stejné rozhraní, udělá si své a pokud chce, zavolá i původní funkci.

Dependency Injection

Dependency Injection je bestie, která slouží mnoha pánům. Umožňuje unit testování, vyjasňuje závislosti, které objekt má, zvyšuje flexibilitu při použití objektů. Spousta výhod, které jsou opravdu skvělé.

V FP byste použili takzvané High Order Funkce, tzn. psali byste funkce, které očekávají (obecně HOF čekají nebo vrací funkce, zde očekáváme, o výstupu nic nevíme) funkce na vstupu.

Tyto funkce by byly naší závislostí. Pak byste si připravili funkce, které umí vytvářet jiné funkce, jako jsem ukázal zde Dependency Injection ve funkcionálním LiveScriptu.

Jen taková poznámka na okraj, zjistil jsem, že u FP mám daleko menší potřebu používat DI. Tam, kde je nahraditelnost použitelná a užitečná, tam člověk přirozeně použije HOF, ve všech ostatních případech volá funkci napevno.

Singleton

Singleton se používá tehdy, kdy smíte mít právě 1 instanci. Příkladem může být přístup ke zdroji, který z principu není thread-safe, nebo k omezeným zdrojům (připojení k databázi).

Někteří lidé nesprávně používají Singleton tam, kde jim 1 instance stačí – což je rozhodně příliš málo k tomu, aby z něčeho singleton dělali. Ale o tom jindy, ostatně Singleton vymírá, DI kontejnery veškeré výhody Singletonu vynulovaly.

Nejblíž k singletonu ve funkcionálních jazycích mají Actory, kde je právě 1 actor, který může řešit danou odpovědnost. To sice není mechanismus, který je unikátní pouze pro FP, ale přijde mi nejbližší.

Pokud nepotřebujete svázat funkčnost se stavem a stačí vám předávat pouze 1 hodnotu, řešilo by se to tak, že by si funkce navzájem předávaly právě tu 1 hodnotu a je to.

Observer

Observer slouží pro situace, kdy události dějící se jednomu objektu jsou zajímavé i pro ostatní objekty. Tyto objekty (Observery) se zaregistrují u objektu, který je zajímá (Observable) a pak jsou notifikovány.

Funkcionální programování, co jsem si tak všiml, neřeší problémy tímto způsobem. Registrovat se někde znamená změnit stav. Pocitově (sám jsem Observer ve FP nikdy nepotřeboval) bych se snažil věc vyřešit normálním voláním a určením observerů v compile-time, ne až v run-time, jako to umožňuje Observer.

Jak jste mohli vidět, v FP jsou věci velmi podobné OOP. Některé věci by se řešily principielně jinak, ale naprostá většina je jen rozdíl v přemýšlení a terminologii.

Komentáře

Subscribe
Upozornit na
guest
11 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Pavel Tisnovsky

Díky za článek, jsou v něm zajímavé postřehy, které mě při přechodu k FP ani nenapadly – prostě v FP jsem jaksi ztratil důvod hledat a používat návrhové vzory. Ty tam samozřejmě jsou, ale asi je nikdo neměl potřebu pojmenovat :-)

Tak mě napadlo – v mnoha FP jazycích se dost používají DSL, to je asi taky „mapovatelné“ na pár návrhových vzorů ne?

podlesh

Potřebu pojmenovat samozřejmě někdo měl – celý koncept design patternů byly navrženy právě k tomu, aby se sjednotila terminologie napříč jazyky a kulturami. Autoři si od toho slibovali překlenutí komunikačních problémů

Je zřejmé, že celý záměr fatálně ztroskotal. Stejně jako v reálné (mezinárodní) politice – znesvářené strany prostě domluvit nechtějí.

Radek Miček

Trochu více návrhových vzorů, ale v Haskellu: http://blog.ezyang.com/2010/05/design-patterns-in-haskel/

Lemming

Přiznám se, že mě překvapilo, když jsem zjistil (*), kolik lidí pod návrhovým vzorem vidí jen konkrétní implementaci v konkrétním jazyce. Pro mě vždycky byly, alespoň většina z nich, čistě architektonickou záležitostí.

*) Už dřív, ne přímo ve spojitosti s tímto článkem

Jiří Knesl (autor)

V návrhovém vzoru nevidím jen čistě implementaci. Spíš vidím v návrhovém vzoru pojmenování vzorového řešení.

A ta řešení pak můžou vypadat ve zdrojácích různě. Já jsem se snažil srovnat, jak se potom ty projekce do reality v jednotlivých paradigmatech liší.

Lemming

Však to nebylo myšleno na vás, ale na autory různých výkřiků „Ve funkcionálním programování / Ruby / Pythonu / … návrhové vzory nepotřebujeme!“ – jako je třeba ten „kopanec“ uvedený na začátku článku.

balki

Dovolil by som si tvrdit, ze dependency injection kontajnery nezrusili dolezitost singletonu prave naopak.
Clovek si moze byt isty, ze dostane vzdy tu istu instanciu ked zavola bean s rovnakym id hocikde v aplikacii.

satai

To je dost specificky pozadavek na to, co se ma injektovat. Vetsinou nic takoveho nechci. Proc taky?

balki

A proc ne?
To je dost caste pouzitie, ze je vytvorena z triedy len jedna bean-a a autowiringom sa injektne.

satai

Tak ono to tak, technicky vzato, vetsinou dopadne. Ale neni to nic, co bych extra chtel. Proc bych mel? Ve vetsine pripadu je mi to totalne jedno, i kdyby se pokazde udelala nejaka nova instance, tak by to nemelo na funkci vliv.

tacoberu

No, ale už to není singleton. Je to jen ta samá instance.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.