Prečo sa nezahrávať s null návratovými hodnotami alebo Special Case design pattern

V diskusii ku správičke na http://www.aspnet.sk/ na tému "Mali by sme vracať z metód null", sme sa dostali do slepej uličky. Chýbal dobrý príklad, na ktorom by sa problematika a možné prístupy riešenia dali ilustrovať. Skúsme si taký vymyslieť.(Do scenáru som vložil veľkú dávku fantázie, pravda je však taká, že paralelizujem reálny scenár, s ktorým som sa stretol v praxi)

Vymýšľame scenár...

V krajine, kde sa piesok lial a kde sa sypal vodopád, nebývala včielka a ani iná nezbedná žienka, ale prevádzkoval svoje služby mobilný operátor BzzzTel. BzzzTel, a viem, že neuveríte, si vážil svojich zákazníkov. Oslovil nás, ako jedného z dvorných dodávateľov aplikácií, aby sme pomohli vybudovať nový internetový portál pre zákazníkov. Jednou z verejne dostupných funckionalít bude služba umožnujúca identifikovať majiteľa strateného mobilného telefónu. Dodávateľom služby je iná spoločnosť, tiež dvorný dodávateľ. Poctivý nálezca bude mať možnosť na základe identifikačného kódu - IMEI získať kontaktné údaje a postarať sa o jeho vrátenie smutnému majiteľovi.

Takto nejako nám popísal situáciu nás obchodník po príchode z obchodného stretnutia z BzzzTelu. My prichádzame na dohodnuté stretnutie s doménovým expertom BzzzTelu pre túto službu a necháme ho hovoriť, aby sme sa o doméne dozvedeli viac:

...identifikácia môže končiť nasledujúcim výsledkom

  • Vlastník je neznámy: Vlastníka nevieme žial identifikovať, s najväčšou pravdepodbnosťou ide o staršie zariadenie poskytnuté našou spoločnosťou, kedy sa neviedla takáto evidencia. Odporúčanie pre nálezcu: Obráťte sa na políciu.
  • Vlastník bol úspešne identifikovaný: Vlastníka sa podarilo identifikovať, zobrazia sa kontaktné údaje 
    Odporúčanie nálezcovi: Doručte ako poctivá duša zariadenie majiteľovi a prípadne môžete požiadať o 10% z nálezného.

Implementujeme...

Prichádzame do officeu. Opomeňme analýzu. Nie je síce nám jasné, akým presným spôsobom budeme komunikovať s identifikačnou službou, ale doménový model nie je problém rozšíť a vytvoriť aj stub implementáciu, napísať unit testy, vytvoriť prezentáčnú vrstvu.

Vytvoríme teda domain service triedu - OwnerIdentificationService  a entitu Owner (alebo CellPhoneOwner).

public class Owner
{
    private readonly string _name;

    public Owner(string name)
    {
        if(name == null)
            throw new ArgumentNullException("name");
        _name = name;
    }

    public string Name
    {
        get { return _name; }
    }
}

public interface ICellPhoneOwnerIdentificationService
{
    Owner Identify(string imei);
}

Stub implementácia môže vyzerať takto:
public class CellPhoneOwnerIdentificationServiceStub
    : ICellPhoneOwnerIdentificationService
{
    public Owner Identify(string imei)
    {
        if(imei == null)
            throw new ArgumentException("imei");

        if(imei.StartsWith("a", StringComparison.CurrentCultureIgnoreCase))
            return new Owner(imei);

        return null;
    }
}

Život je niekedy pes...

Fajn. Čakáme už len na zverenenie rozhrania pre identifikáčnú službu, aby sme ju mohli plne integrovať. A aj keď sme v krajine kde sa sype vodopád, zákazník zmení špecifikáciu služby. Stretnutie s doménovým experom pre identifikačnú službu BzzzTelu je opäť nevyhnutné.

...na základe hlbšej analýzy a v súčinnosti s dodávateľom služby sme identifikovali ďalšie situácie, s ktorými sa bude treba vysporiadať 

  • Vlastník sa nenašiel: Zariadenie bolo pravdepodobne poskytnuté iným operátorom.
    Odporúčanie pre nálezcu: Skúste podobnú službu iného operátora alebo sa obráťte na políciu.
  • Vlastník je neznámy: Vlastníka nevieme žial identifikovať, s najväčšou pravdepodbnosťou ide o staršie zariadenie poskytnuté našou spoločnosťou, kedy sa ešte neviedla takáto evidencia.
    Odporúčanie pre nálezcu: Obrátte sa na políciu.
  • Chýbajúca informácia o vlastníkovi: Vieme, že vlastník existuje, túto informáciu máme, ale chýba asociácia na konkrétneho zákazníka. Táto asociácia sa do systému dostáva zložitejším spôsobom - zbierame tieto informácie z predajní, chodia nám na papieroch, ručne ich pracovníci prepisujú do systému.
    Odporúčanie pre nálezcu: Pokúsime sa dodatočne identifikovať zákazníka a budeme vás kontaktovat. Aplikácia musí umožniť zanechanie kontaktných údajov.
  • Vlastník nepovolil poskytnutie informácie:
    Údaje o vlastníkovi sú chránené:

    Zaznamenanie ochrany údajov má vyššiu prioritu ako poskytnutie súhlasu.  
    V oboch prípadoch máme rovnaké odporúčanie pre nálezcu: Zariadenie nám prineste, my sa pokúsime o identifikáciu majiteľa a diskrétne odovzdanie zariadenia.

Analyzujeme... 

Prichádzame do firmy, otvárame svoj kód a rozmýšlame, čo s tým. Vidíme, že musíme nielen obslúžiť viacero možných výsledkov identifikácie, ale máme aj viacero takých výsledkov, ktoré sú kandidátmi na zamaskovanie za null návratovú hodnotu. Najprirodzenejšie sa nám teraz asi bude javiť "Vlastník sa nenašiel". Tu je zrejme jasné, že rafactor v tomto zmysle smrdí. Napadá nás viacero spôsobov ako problém riešiť. Starší skúsenejší kolega nás upozorní na Special Case pattern. (čo je Fowlerovo všeobecnejšie poňatie Null Object Patternu, prípadne sa na problém môžeme pozrieť cez ďalšiu variáciu - State Pattern). Keby sme s tým tak rátali už predtým...

Refactorujeme...

Vytvoríme triedu pre každý špecifický prípad:

Čo nás možno poteší, že naše GUI bude schopné zobraziť ihneď aj špeciálne prípady a to bez toho, aby sme museli čokoľvek dorábať a riešiť špeciálne null.

ownerDetailView.Populate(owner);

Na miestach, kde sa potrebujeme rozhodovať na základe výsledku, nahrádzame nič nehovoriace

if(owner == null)
  //
else
   //

za oveľa čitatelnejšie

if(owner is UnknownOwner)
//
else
//

Ešte nás straší reflection. Vieme si pomôcť, aj keď za cenu toho, že stratíme trochu z otvorenosti designu

public class Owner
{
    public static readonly UnknownOwner Unknown= new UnknownOwner();
}

if(Owner == Owner.Unknown)
//
else
//

Ďalšia trochu sofistikovanejšia možnosť, ako toto vyriešiť(ale s rovnakým dopadom na polymorfizmus) je flyweight pattern (Gang of Four).

Ešte otvorenejší design získame, ak prestaneme uvažovať o rôznych "typoch vlastníkov", ale budeme vracať pre každý výsledok inštanciu odvodenej triedy z abstraktného IdentificationResult. (keďže to ako názov domain entity smrdí, lepšie sa to bude vynímať v query modely ako DTO) V prípade úspechu bude výsledok obsahovať ownera. Doplnením properties alebo behavior do bázovej abstraktnej triedy dokážeme naraz ovlyvniť všetky result triedy. Na druhej strane vieme vytvoriť špecifický behavior a správanie pre "úspešný" výsledok a ostantné výsledky toto automaticky nezdenia.

Robíme záver...

Ukázali sme si, ako vďaka Special Case, oproti maskovaniu jednej z návratových hodnôť môžeme profitovať z:

  1. Polymorfizmus
  2. Čitateľnosť kódu
  3. Zjednodušenie logiky kódu
  4. Otvorenosť designu pre zmeny
  5. Profit pri tvorbe prezentácie (v dôsledoku bodu a)
  6. Predchádzanie skrytiu sa neošetrenej vetvy kódu za null,
    keď null je jednou z očakávaných návratových hodnôť z metódy.
    public Owner Identify(string imei)
    {
        Owner owner= null;
       
        //logika vyhodnotenia výsledku - vetvenie kódu

              return owner;
          }
          V prípade Special Case prístupu ju môžeme v unit teste alebo kóde okamžite vylúčiť.

 

Komentáre

Bez komentárov

Prihlásiť | Registrovať | Pomoc