Na obsah stránky

Zatočíme s null

Aleš Roubíček | | # permalink

Chtěl bych se v takové kratké sérii blogů dostat k jednotlivým bodům NOOO manifesta a jeho principům. V této sérii, bych chtěl vyjádřit, proč se k tomuto manifestu hlásím, a i ukázat pár praktických důsledků v hodnotách a principech obsažených.

Dnes se zastavíme u toho, proč preferuji Option před null a proč:

Upřednostňujeme reprezentaci invariant na úrovni typů před reprezentací na úrovni hodnot.

Problematika null

Snad každý, kdo se zabývá programováním narazil na problém s null hodnotami. Většinou se projevuje tak, že z ničeho nic dostaneme někde NullReferenceException (NRE) a máme po ptákách. Tento problém je celkem častý a odhaduje se, že každý rok má za následek ztrátu $1B. To je pro představu 1 000 000 000 USD. Ztráta je způsobena jak samotným projevem, tak i náklady vynaloženými na obranu před NRE.

Obrana

Jistě znáte techniky defenzivního programování, jako jsou Code Contracts v jejich různých mutacích, které efektivně zvyšují náklady na tvorbu a správu přebujelé code base, v který aby se čert vyznal. Další se snaží o zavedení nového syntaktického prvku, který vypadá následovně: foo?bar() a dělá to samé co if (foo != null) foo.bar();. Pořád jen to jen maskování symptomů. Ne řešení problému.

Prevence

Další možností je používat jazyky, jako je třeba F#, které null nemají. (Ok, F# zná null, ale musíte si explicitně o něj říct a označit tak potenciálně nebezpečný kód, jinak by asi moc nemohl pracovat s již existujícími .net knihovnami.) Většinou se jedná o funkcionální jazyky se sofistikovanými typovými systémy.

Nechme se inspirovat některými dobrými vzory:

Null Object

Pokud děláte TDD, nebo se zajímáte o návrhové vzory, jistě už jste na tento vzor narazili. Jde o to, že ve vašem systému máte definované „implementační“ třídy, které nemají žádnou implementaci, nebo prostě jen nikdy nevyhazují výjimky. Slouží pouze k tomu, že pokud systému nenabídneme nějakou jinou funkční implementaci, nemusíme se obávat NRE, protože tu máme bezpečný Null Object.

Já ho třeba používám vždycky, když implementuji logování:

ILogger logger = NullLogger.Instance;
public ILogger Log {
  get { return logger; }
  set { logger = value ?? NullLogger.Instance; }
}

Po inicializaci je logger nastaven na instanci NullLoggeru. Logger je možné injektovat, ale pokud se někdo pokusí podstrčit null, tak si podržíme NullLogger. Za předpokladu, že se logger nepoužívá nikde jinde, než v property, máme bezpečno a nemusíme psát spoustu defenzivního kódu jako:

if (Log != null) Log.Info("foo");

Samozřejmě to není jediný případ užití. Další, z možných implementací, je vyjádření speciálního stavu, jako třeba:

public class AsyncResult {
  public AsyncResult(XElement response) {
    Response = response;
  }
  public XElement Response { get; set; }
}

public class QueryInProgress : AsyncResult {
  public QueryInProgress() : base(null) { }
}

Tohle je Null Object implementovaný v SOA. Systém dostane ke zpracování nějaký požadavek (dotaz) a když se následně zeptá na výsledek, dostane buď Null Object QueryInProgress, který říká, že dotaz se ještě zpracovává, nebo už samostatnou odpověď. Null Object může mít i sémantický význam.

Option

Jazyky, které podporují pattern-matching, umožňuji práci s typem Option:

let exists (x : int option) =
    match x with
    | Some(x) -> true
    | None -> false

None nám nahrazuje null, ale nejedná se o hodnotu, ale o typ. Typ Option je totiž definován jako generické disjunktivní sjednocení:

type Option<'T> =
  | None
  | Some of 'T

Na rozdíl od vzoru Null Object tu neztrácíme nutnost rozhodování se, jak se v případě None zachovat. Nadruhou stranu, máme speciální případ explicitně sémanticky vyjádřen na úrovni typu.

Maybe

Maybe je v případě užití celkem podobný Option s tím rozdílem, že jde o monad. Krom toho, že je opět parametricky polymorfní (generický), splňuje i požadavky na monadičnost. Monadičnost nám umožňuje kompozici a můžeme i využít vlastostí jazyka jako je LINQ, který je postavený nad sekvenčním monadem, ale nic nám nebrání ho použít i nad jinými. Třeba právě nad maybe.

Nepotřebujeme však nutně Haskell, abychom mohli využívat všech výhod. Tohle je třeba C#:

return TryGetCachedData(message, metadata, request).Match(
  some: data => CachedResult(message.RequestId, data, stats),
  none: () => Result(message, context, metadata, request, stats));

Implementaci Maybe monadu z ukázky najdete v tomto gistu.

Závěr

Null je zlo. Je možné s ním bojovat silou nebo inteligencí. Já byl vždycky slabý a líný, proto preferuji výše zmíněné možnosti prevence před samotnou obranou. Náš projekt sice není úplně imuní vůči null, ale díky Null Objectu a Maybe monadu, které se dají krásně kombinovat, je kód čistší a méně náchylný k chybám. Snad. :)

Našli jste v článku chybu? Máte námět na reportáž? Založte mi ticket.