Pokud 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:
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));
}
}
{
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));
}
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));
}
{
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));
}
}
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;
}
{
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));
}
{
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();
}
}
{
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)));
}
}
{
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());
}
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:
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.
OdpovědětVymazatZa 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 :-)
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ětVymazatV zmíněném kurzu ne, ale podobná řešení mají i jiní:
Vymazathttps://github.com/AndreyTsvetkov/Functional.Maybe
Mrknu se na to, díky.
Vymazat