neděle 15. ledna 2012

Fluent interface - nakouknutí za oponu

Proč fluent?

Fluent interface se dá volně do češtiny přeložit jako plynně nebo také ladně používané rozhraní. Jedná se o techniku budování API rozhraní s důrazem na popisnost a snadnost použití. Konzumenti fluent rozhraní ocení příjemný a intuitivní způsob zápisu bez zbytečného opakování stejných sekvencí. Fluent přístup k budování API je typický pro problematiku DSL (doménově specifické jazyky). Tento příspěvek by měl pomoci pochopit základní techniky budování fluent API.

Srovnejme dva způsoby zápisu použití rozhraní pro nastavení fiktivní hry. Nejprve klasické "no fluent" provedení (optimalizací zápisu by mohlo být použití objektových inicializátorů):

Hra hra = new Hra();
 
NastaveniPravidel nastaveniPravidel = new NastaveniPravidel();
nastaveniPravidel.PridatPravidlo("Pravidlo1");
nastaveniPravidel.PridatPravidlo("Pravidlo2");
 
hra.NastaveniPravidel = nastaveniPravidel;
 
NastaveniHracu nastaveniHracu = new NastaveniHracu();
 
NastaveniHrace hrac1 = new NastaveniHrace();
hrac1.Jmeno = "Franta";
hrac1.Vek = 50;
 
nastaveniHracu.PridatHrace(hrac1);
 
NastaveniHrace hrac2 = new NastaveniHrace();
hrac2.Jmeno = "Bloncka";
hrac2.Vek = 18;
hrac2.ZacinaHru = true;
 
nastaveniHracu.PridatHrace(hrac2);
 
NastaveniHrace hrac3 = new NastaveniHrace();
hrac3.Jmeno = "ChuckNorris";
hrac3.JePocitac = true;
 
nastaveniHracu.PridatHrace(hrac3);
 
hra.NastaveniHracu = nastaveniHracu;
 
hra.HaziSeKostkou = true;
hra.Obtiznost = 5;

Ekvivalent s využitím fluent zápisu:

IHra hra = new Hra().
    NastavitPravidla(
        x =>
        {
            x.PridatPravidlo("Pravidlo1");
            x.PridatPravidlo("Pravidlo2");
        }).
    NastavitHrace(
        x =>
        {
            x.PridatHrace("Franta").NastavitVek(50);
            x.PridatHrace("Bloncka").NastavitVek(18).ZacinaHru();
            x.PridatHrace("ChuckNorris").JePocitac();
        }).
    NastavitObtiznost(5).
    HaziSeKostkou();

Posuďte sami, který zápis se Vám víc líbí. Vytváření kódu je v dnešní době také o jeho čitelnosti a znovupoužitelnosti. Fluent technika přispívá právě k dosažení tohoto požadavku. Důkazem je popularita frameworků, které vsadily na fluent rozhraní - StructureMap, Rhino Mock, Fluent NHibernate a další.

Principy fluent rozhraní

Fluent rozhraní má následující charakteristiky:

  • Návratovou hodnotou metod jsou rozhraní, které umožňují řetězení metod.
  • Ukončovací kontext jedné metody je ekvivalentní počátečnímu kontextu metody následující.
  • Posloupnost volání metod může být ukončena metodou, která vrací typ void.
  • Rozhraní je tvořeno výhradně metodami, nastavování vlastností je potlačeno.
  • S výhodou se používají statické třídy a statické metody.
  • Používají se generické typy.
  • Pro nastavování hodnot typu boolean se používají bezparametrické metody, které zapnou nebo vypnou příznak.

Při konstrukci fluent rozhraní se (mimojiné) používají dvě techniky, které stojí za bližší vysvětlení - method chaining a nested closure.

Method chaining - řetězení metod

Poměrně známá technika, která umožní v jednom příkazu zřetězit volání více metod. Finta je v tom, že každá metoda vrací referenci na rozhraní. V takovém případě pak nezáleží na pořadí volání jednotlivých metod. Podívejme se na implementaci rozhraní INastaveniHrace.

public interface INastaveniHrace
{
    INastaveniHrace ZacinaHru();
    INastaveniHrace JePocitac();
    INastaveniHrace NastavitVek(short vek);
}

Ukázka použití z příkladu výše:

x.PridatHrace("Franta").NastavitVek(50);
x.PridatHrace("Bloncka").NastavitVek(18).ZacinaHru();
x.PridatHrace("ChuckNorris").JePocitac();

Nested closure - zapouzdření metody

Technika, která umožní vkládat složitější vnořené konstrukce jako argument volání metody. Ve výše uvedeném příkladě je část týkající se nastavení pravidel a volání metody NastavitPravidla() řešená přes nested closure:

NastavitPravidla(
    x =>
    {
        x.PridatPravidlo("Pravidlo1");
        x.PridatPravidlo("Pravidlo2");
    })

Implementace nested closure využívá delegát Action<T>, za který je v místě volání dosažen příslušný lambda výraz. Deklarace metody NastavitPravidla() v rozhraní IHry s použitím delegátu Action:

IHra NastavitPravidla(Action<INastaveniPravidel> akce);

Implementace metody pak může vypadat takto:

public IHra NastavitPravidla(Action<INastaveniPravidel> akce)
{
    akce(nastaveniPravidel);
    return this;
}

Ukázková implementace fluent rozhraní IHra

using System;
using System.Collections.Generic;
 
namespace Priklad.FluentInterface
{
    public interface IHra
    {
        IHra NastavitPravidla(Action<INastaveniPravidel> akce);
        IHra NastavitHrace(Action<INastaveniHracu> akce);
        IHra NastavitObtiznost(int obtiznost);
        IHra HaziSeKostkou();
    }
 
    public interface INastaveniPravidel
    {
        void PridatPravidlo(string nazev);
    }
 
    public interface INastaveniHracu
    {
        INastaveniHrace PridatHrace(string jmeno);
    }
 
    public interface INastaveniHrace
    {
        INastaveniHrace ZacinaHru();
        INastaveniHrace JePocitac();
        INastaveniHrace NastavitVek(short vek);
    }
 
    public class NastaveniPravidel : INastaveniPravidel
    {
        IList<string> pravidla = new List<string>();
 
        public void PridatPravidlo(string nazev)
        {
            pravidla.Add(nazev);
        }
    }
 
    public class NastaveniHrace : INastaveniHrace
    {
        internal bool jePocitac = false;
        internal string jmeno;
        internal short vek;
        internal bool zacinaHru = false;
        
        public INastaveniHrace ZacinaHru()
        {
            zacinaHru = true;
            return this;
        }
 
        public INastaveniHrace JePocitac()
        {
            jePocitac = true;
            return this;
        }
 
        public INastaveniHrace NastavitVek(short vek)
        {
            this.vek = vek;
            return this;
        }
    }
 
    public class NastaveniHracu : INastaveniHracu
    {
        IList<NastaveniHrace> hraci = new List<NastaveniHrace>();
 
        public INastaveniHrace PridatHrace(string jmeno)
        {
            NastaveniHrace hrac = new NastaveniHrace();
            hrac.jmeno = jmeno;
 
            hraci.Add(hrac);
 
            return hrac;
        }
    }
 
    public class Hra : IHra
    {
        private bool haziSeKostkou = false;
        private NastaveniHracu nastaveniHracu = new NastaveniHracu();
        private NastaveniPravidel nastaveniPravidel = new NastaveniPravidel();
        private int obtiznost;
 
        public IHra NastavitPravidla(Action<INastaveniPravidel> akce)
        {
            akce(nastaveniPravidel);
            return this;
        }
 
        public IHra NastavitHrace(Action<INastaveniHracu> akce)
        {
            akce(nastaveniHracu);
            return this;
        }
 
        public IHra NastavitObtiznost(int obtiznost)
        {
            this.obtiznost = obtiznost;
            return this;
        }
 
        public IHra HaziSeKostkou()
        {
            this.haziSeKostkou = true;
            return this;
        }
    }
}

Statické typy a generika

Výhody fluent rozhraní vyniknou v kombinaci se statickými typy a statickými metodami. Např. při použití mockovacího frameworku Rhino Mocks můžete jednoduchým zápisem nastavit očekávané chování getteru KodMeny mockovaného rozhraní IDoklad:

IDoklad doklad = mockRepository.StrictMock<IDoklad>();
Expect.Call(doklad.KodMeny).Return("EUR").Repeat.Any();

Třída Expect i metoda Call<T>() jsou statické. Vlastnost KodMeny vrátí hodnotu "EUR" a může být dotazována libovolněkrát (Repeat.Any()).

Všimněte si, že metoda Call() je generická. Generický typ použitý při jejím volání je určujícím generickým typem pro metodu Return(). V našem případě je doklad.KodMeny typu string.

Slabé stránky fluent interface

Konstrukce zapsané pomocí zřetězení metod a lambda výrazů se problematicky debugují. Problémem je nemožnost umístění bodu přerušení do podvýrazu. Lambda výrazy nelze používat při ladění ve funkci Watch.

Rozhraní napsané jako fluent často porušují pravidlo jedné zodpovědnosti a nutí k vytváření velkého "božského" objektového typu s mnoha zodpovědnostmi. Je to dané podstatou zřetězení metod, kdy každá metoda musí vracet odkaz na stejné rozhraní.

Dalo by se diskutovat, zda-li definice fluent rozhraní neporušuje Deméterův zákon o přístupu do vnitřních objektů. Pokud Vás tato diskuze zajímá, můžete si ji přečíst na stackoverflow.com.

1 komentář:

  1. cus, super clanek - vecnej a pritom citelnej! jen tak dal!

    OdpovědětVymazat