null-referanser


søndag 21. juni 2009 OO Patterns C#

NullReferenceExceptionPå flytoget etter NDC 2009 hadde mine kollegaer og jeg en nokså heftig diskusjon om bruk av null. Det var et nokså uskyldig spørsmål som startet det hele:

"Sjekker du alltid om parametrene som sendes inn til en metode er null, og kaster ArgumentNullException om så er tilfelle?"

Mitt svar var ganske bestemt: Nei, det gjør jeg stort sett aldri! For meg er det å sjekke for null noe man gjør i frykt, noe man gjør for å kompansere for dårlig kode. I de fleste tilfeller er det en bug å sende inn null, og jeg ønsker ikke å legge til rette for sloppy kode ved å sette på støttehjulene og teste for dette.

Kode som forventer en instans men får null vil kaste en NullReferenceException, som er mer enn godt nok for kode som er godt skrevet - den energien en eventuelt putter inn i å lage mer detaljerte feilmeldinger er ikke verdt det i dette tilfellet.

Videre hevdet jeg at null i seg selv er en uting. Kode som er skrevet slik at man gjentatte ganger må teste for null er ikke god kode. Å teste for null er en code smell. Bruk av null hører ikke hjemme i kode som befinner seg andre steder enn på det laveste abstraksjonsnivået - null er ikke noe forretningsfolk kan snakke om, og bør derfor etter min mening heller ikke eksistere i kode som beskriver forretningskonsepter.

Null Object pattern

Problemet jeg snakker om kan illustreres med følgende kodesnutt:

Employee e = DB.GetEmployee("T-Man");

if (e != null && e.IsTimeToPay(DateTime.Today))

    e.Pay();

Siden databasen kan returnere null om "T-Man" ikke eksisterer benytter vi oss av det faktum at det første uttrykket i if-testen evalueres først, og det andre uttrykket kun om det første er sant. Vi har alle skrevet kode som dette her. Vi har også alle glemt å teste for null fra tid til annen - selv om dette er et vanlig mønster så er det både uelegant og noe som fører til mange feil.

I stedet for å returnere en null-referanse kunne vi la databasen kaste en exception, men da måtte vi ha brukt try/catch ved alle kall, noe som er enda mindre elegang.

Et alternativ til å bruke null-referanser er å implementere et objekt som har det forventede grensesnittet, men som ikke gjør noen ting. Dette kaller vi for Null Object. Min kollega hevdet at dette er veldig tungvindt å implementere, men etter å ha tenkt meg litt om så finner jeg mange steder hvor jeg har brukt det selv, og hvor det er langt fra vanskelig å implementere, samtidig som det gjør koden som bruker objektene langt enklere.

Her er et eksempel fra prosjektet jeg holder på med akkurat nå. Brukeren skriver inn noe tekst som blir gjort om til en UserCommand. Poenget er å unngå å returnere null, så hvis teksten som sendes inn er gibberish så returnerer jeg en UnknownCommand:

public UserCommand GetCommand(FirstPerson person, string[] commandArguments)

{

    if (ValidCommandArguments(commandArguments))

    {

        return _commandDictionary 

            .GetParser(commandArguments[0])

            .GetCommand(person, commandArguments);

    }

    return new UnknownCommand();

}

UnknownCommand er et null-objekt: Det implementerer UserCommand-interfacet, men gjør ingen ting. Klienter av UserCommand kan bruke dette objektet på samme måte som alle andre commands - jeg trenger ikke kaste noen exceptions, og aldri teste for null.

public class UnknownCommand : UserCommand

{

    public UserCommandResult Execute()

    {

        return new UserCommandResult

        {

            Success = false,

            Text = "Did not compute!",

        };

    }

}

Jeg har flere eksempler som ligner på dette. Der hvor jeg bruker strategy pattern i kombinasjon med en eller annen form for en factory, implementerer jeg nesten alltid også et null-objekt, en dummy-klasse som kan brukes i stedet for en virkelig strategi.

Dette beviser ikke at null objects kan brukes over alt, ei heller at det er trivielt, men jeg er ganske sikker på at bruk av dette mønsteret vil føre til kode som er enklere å lese og vedlikeholde, og som er mer feilfri.

Kan vi erstatte != null testen?

Men det er ikke til å komme bort ifra at man av og til må bruke null-referanser. Det er også et faktum at jeg hater å skrive != og == null. Jeg ønsker derfor en mer deskriptiv måte å teste på - en som ikke bryter abstraksjonsnivået. Dette er hva jeg har kommet frem til:

[Test]

public void Test()

{

    object o = null;

    if (o.IsNotSetYet())

        Assert.That(o == null);

        //Cool, I called a method on a null reference

    else

        Assert.Fail("o was not null");          

}

Denne testen feiler ikke! Men hvordan er det mulig å kalle en metode på en null-referanse? Jo, det er mulig takket være extension methods:

public static class ObjectExtensions

{

    public static bool IsNotSetYet(this object o)

    {

        return o == null;

    }

    public static bool IsSet(this object o)

    {

        return o != null;

    }

}

Er det ikke vakkert?


comments powered by Disqus