úterý 19. ledna 2016

Maybe - návrhový vzor

Výsledek obrázku pro pluralsightPokud máte štěstí na dobrého zaměstnavatele, máte možná předplacen přístup ke službě Pluralsight. A třeba si to někdo platí i sám - roční cena je kolem 500 dolarů ročně za plný přístup, ten částečný pak stojí ročně 300 dolarů. Platit lze i měsíčně, pak jsou poplatky 42, respektive 25 dolarů. Zřízením účtu získáte přístup k několika tisicům kurzů na různá témata. Výhodou je, že kurzy mají určitou vyšš úroveň a jdou často více do hloubky, než volné materiály, které jsou k nalezení na Internetu, popřípadě výuková videa na YouTube či záznamy z vývojařských akcí. 


Zaujala mne přednáška o zlepšování kódu a uplatňování principů SOLID od Mark Seemanna - blog tohoto chytrého pána z Dánska je veřejně dostupný na adrese http://blog.ploeh.dk/ a kurz v Pluralsight má název  Encapsulation and SOLID. Přednášející věnuje hodně času popisu, jak má vypadat dobře napsaná třída - tak aby se objekty chovali předvídavě. V principu rozděluje metody třídy na příkazové a dotazové (command a query). To není nic překvapivého a myslím, že je to součástí každého kurzu či knížky o objektovém programování. Metody typu Command by neměli nic vracet a mění stav objektu, naopak metody typu Query vždy něco vrací, ale stav objektu nemění.

Dalším principem je, že objekt by měl být liberální v tom, co akceptuje na vstupu, ale konzervativní ve svém výstupu. Tyhle pravidla autor demonstruje na jednoduché třídě, která vrací obsah souboru z uložiště a umí vrátit i cestu k tomuto souboru v rámci úložiště - tedy vlastně vrací jakýsi identifikátor úložiště - například v případě disku by to byla cesta (tu jsem ostatně použil v následujících ukázkách). Základní návrh třídy vypadá takto:

public class Storage
{
    public string GetStorageName(int id)
    {
        return Path.Combine(@"C:\", id + ".txt");
    }
    public string GetContent(int id)
    {
        return string.Concat(Enumerable.Repeat("hello ", id));
    }
}

Použití této třídy je jednoduché:

int[] ids = new[] { 1, 2, 3, 4, 5 };

var storage = new Storage();

foreach (var id in ids)
{
    Console.WriteLine("{0}: {1}", storage.GetStorageName(id), storage.GetContent(id));
}

Jenže svět není úplně ideální, v realném světě mohou nastat další situace, například  že pro dané id není  v úložišti žádný záznam, tedy například soubor neexistuje.

Stav, kdy pro sudá id není v úložišti žádný záznam, lze navodit takto:

public string GetContent(int id)
{
    if (id % 2 == 0)
        throw new ArgumentException("Cannot find it");

    return string.Concat(Enumerable.Repeat("hello ", id));
}

Práce s touto metodou už ale nebude tak snadná, volající musí své volání uzavřít do bloku Try-Catch.
Takže nyní volání vypadá takto:

var someStorage = new NotSoReliableStorage();

foreach (var id in ids)
{
    try
    {
        Console.WriteLine("{0}: {1}", someStorage.GetStorageName(id), someStorage.GetContent(id));
    }
    catch
    {
        Console.WriteLine("{0}: NO CONTENT", someStorage.GetStorageName(id));
    }
}

To nevypadá úplně přehledně  - ale jaké jsou jiné možnosti? Mohli bychom se pokusit zavést pravidlo, že metoda GetContent vrátí null pokud nenalezne záznam. Jenže zrovna toto porušuje pravidlo o konzervativním výstupu - druhá metoda GetStorageName null nikdy vrátit nemůže a tak bychom měli jednou metodu, která místo stringu vrací někdy  null a druhou, u které toto nikdy nenastane.  To není dobré pro konzistenci přístupu  a autor přednášky to docela podrobně rozebírá - a já tady nechci opisovat obsah placeného materiálu, na který autor jistě vynaložil dost práce - ostatně celá přednáška má pět hodin - ale řeší se tam toho daleko více, tenhle článek je vlastně inspirován jen "předmluvou",

Ale jaké je řešení? Dříve se přidala metoda bool IsContentAvailable(int id) před volání GetContent - je to řešení, ale volající nesmí na toto zapomenout, mohou nastat problémy pokud v budoucnu využijeme více vláken apod. Takže tento přístup se nyní moc nepoužívá.

Druhým, a přiznám se, že doposud to byl můj favorit a v kódu tento přístup/vzor  používám, je metoda bool TryGetContent(int id, out string) - ostatně v C#/NET frameworku je tento vzor hojně používán.  Metoda by v našem případě vypadala  nějak takto:

public bool TryGetContent(int id, out string content)
{
    if (id % 2 == 0)
    {
        content = null;
        return false;
    }

    content = string.Concat(Enumerable.Repeat("hello ", id));
    return true;
}

a použití pak takto:

foreach(var id in ids)
{
    string message = "";

    if(betterStorage.TryGetContent(id, out message))
        Console.WriteLine("{0}: {1}", betterStorage.GetStorageName(id), message);    
    else
        Console.WriteLine("{0}: NO CONTENT", betterStorage.GetStorageName(id));
}

 I když je třída, respektive metoda, nyní mnohem srozumitelnější - volající jasně vidí, že se objekt pouze pokusí získat obsah, nicméně to není garantováno. Není to prostě jednoznačný dotaz. IMHO také to nevypadá moc pěkně.

 A co autor nakonec navrhuje?

Použití Maybe

V principu je to třída založená na IEnumerable, která vrací buď prázdnou kolekci (to v případě, že se akce nepovedla) a nebo kolekci s právě jedním elementem, který odpovídá původní očekáváné návratové hodnotě.

public class Maybe<T> : IEnumerable<T>
{
    private readonly IEnumerable<T> values;

    public Maybe()
    {
        this.values = new T[0];
    }

    public Maybe(T value)
    {
        this.values = new[] { value };
    }
    public IEnumerator<T> GetEnumerator()
    {
        return this.values.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

Třída pak bude vypadat takto:

public class MaybeStorage
{
    public string GetStorageName(int id)
    {
        return Path.Combine(@"C:\", id + ".txt");
    }
    public Maybe<string> GetContent(int id)
    {
        if (id % 2 == 0)
            return new Maybe<string>();

        return new Maybe<string>(string.Concat(Enumerable.Repeat("hello ", id)));
    }
}

a její použíti pak:

var maybeStorage = new MaybeStorage();

foreach (var id in ids)
{
    Console.WriteLine("{0}: {1}",
                        maybeStorage.GetStorageName(id),
                        maybeStorage.GetContent(id)
                            .DefaultIfEmpty(string.Format("NO CONTENT"))
                        .Single());
}

Přiznám se, že se mi tento přístup líbí. Ze zápisu metody ve třídě lze odvodit, co se může stát, práce s metodou vypadá příjemně, zápis je srozumitelný. Nevím, jestli se nadobro vzdám vzoru TryToDoSt, ale určitě si to vyzkouším.

Celý prográmek je k dispozici na .NET Fiddle:


4 komentáře:

  1. Tohle je věc, která hodně zpřehlední kód :-) V F# je přímo typ Option, v poslední Javě je Optional, ve Scale Option, takže se divím, že to ještě není v C#, resp. v BCL.

    Za pozornost ještě stojí Try (http://www.scala-lang.org/api/current/index.html#scala.util.Try), který může reprezentovat hodnotu nebo chybu. Pak to je vlastně něco jako checked-exceptions v Javě, protože tě už kompilátor donutí s tím chybovým stavem něco dělat :-)

    OdpovědětVymazat
  2. Je to zajímavý nápad, ale ten Single() na konci to dost kazí. Nezmiňuje se ten pán o jiném řešení než IEnumerable<> ?

    OdpovědětVymazat
    Odpovědi
    1. V zmíněném kurzu ne, ale podobná řešení mají i jiní:

      https://github.com/AndreyTsvetkov/Functional.Maybe

      Vymazat
    2. Mrknu se na to, díky.

      Vymazat