1-2-3 dispatch


søndag 3. oktober 2010 OO Patterns C# Clojure

Har du lest A Brief, Incomplete, and Mostly Wrong History of Programming Languages ? Blogposten beskriver bl.a. Java's fødsel på denne måten:

1996 - James Gosling invents Java. Java is a relatively verbose, garbage collected, class based, statically typed, single dispatch, object oriented language with single implementation inheritance and multiple interface inheritance. Sun loudly heralds Java's novelty.

Dette blir veldig morsomt da det etterfølges av følgende:

2001 - Anders Hejlsberg invents C#. C# is a relatively verbose, garbage collected, class based, statically typed, single dispatch, object oriented language with single implementation inheritance and multiple interface inheritance. Microsoft loudly heralds C#'s novelty.

Da jeg leste dette slo det meg at jeg ikke ante hva single dispatch var for noe. Jeg skjønte at det var noe grunnleggende i hvordan programmeringsspråk fungerer, og ante at det hadde noe med metode/funksjonskall å gjøre, men der stoppet min kunnskap. Etter litt googling føler jeg meg nå bedre – og jeg vil nå gjerne dele det jeg har lært!

Single dispatch gir ikke så mye mening før vi ser på hva alternativet er. Multiple dispatch (eller multimethods) er en feature hvor hvilken metode som kjøres når man gjør et metodekall kan avgjøres basert på informasjon som bare er tilgjengelig under kjøring (dvs. dynamisk). I denne artikkelen vil jeg eksemplifisere både single og multiple dispatch, men også demonstrere VISITOR PATTERN, som er en teknikk vi av og til bruker for å omgå utfordringene med single dispatch.

Single dispatch

C# er et eksempel på et single dispatch-språk. "Problemet" med single dispatch, om jeg kan kalle det det, kan illustreres med koden nedenfor. Her har vi en Person-klasse som inneholder fornavn og etternavn. Deretter arver vi fra denne for å definere en russer, som i tillegg har et "patrynomisk" navn – et navn russere har som utkommer av farens fornavn. Til slutt har jeg samlet forretningslogikken for å produsere "full display name" for en person i FullNameFormatter.

public class Person
{
    public string GivenName { get; set; }
    public string Surname { get; set; }
}
 
public class Russian : Person
{
    public string PatronymicName { get; set; }
}
 
public static class FullNameFormatter
{
    public static string FullName(Person p)
    {
        return string.Format("{0} {1}", p.GivenName, p.Surname);
    }
    public static string FullName(Russian p)
    {
        return string.Format("{0} {1} {2}", 
            p.GivenName, p.PatronymicName, p.Surname);
    }
}

Jeg bruker så disse klassene til å definere et par personer, og skrive ut navnene deres..

static void Main(string[] args)
{
    List<Person> people = new List<Person>
    {
        new Person { GivenName = "Jens", Surname = "Stoltenberg" },
        new Russian { GivenName = "Boris", PatronymicName = "Nikolayevich", 
            Surname = "Yeltsin" }
    };
 
    people.ForEach(p => Console.WriteLine(FullNameFormatter.FullName(p)));
}
Output:
Jens Stoltenberg
Boris Yeltsin

Resultatet er ikke hva jeg ønsker – russerens mellomnavn kommer ikke med! Dette er fordi C# har single (statisk) dispatch: Kompilatoren kan ikke skille mellom personene i listen, behandler alle som instanser av Person, og dispatcher til "feil" metode for russeren.

Det finnes flere løsninger på dette problemet. Den mest åpenbare er kanskje å ha logikken for full name i Person-klassen, og overstyre den i Russian. Av og til er det det mest fornuftige, og føles veldig objektorientert. Men man kan ikke plassere all forretningslogikk inn i klassene, og dessuten gjør dette at vi sprer full name-logikken mellom mange, ulike filer.

En annen, vanlig løsning er å ha bare én FullName-metode, og bruke reflection - dvs. sjekke objektets egentlige type. FullName vil da typisk ha en switch med én case for hver type. Det vil si at man egentlig gjør sin egen, reflection-baserte dispatch.

Men vi har også et objektoerientert mønster for å løse problemet – det heter Visitor pattern, og det vil jeg presentere nå..

Sidebar: Multiple dispatch i C# 4.0
Interessant nok kan man med C# 4.0 enkelt "fikse" koden over til å bruke multiple dispatch, slik at fullname blir formatert riktig for russeren. Det gjør man ved å endre deklerasjonen av people fra List<Person> til List<dynamic>. .NET vil da vente til runtime med å avjøre hvilket type objekt dette er, og vet dermed at det har å gjøre med en russer når kallet til FullName skal dispatches – slik at riktig overload kalles.

Dual dispatch

I Visitor pattern bruker vi en teknikk som kalles dual dispatch – som i dette eksempelet vil sørge for at riktig overload av FullName skal bli kalt. Jeg legger til et IFullNameFormatter-interface (dette er visitoren), og FullNameFormatter må arve fra dette. I tillegg må jeg gi Person en FullName-metode som tar imot en formatter/visitor og kaller den med seg selv som parameter. Denne må russeren overstyre.

public interface IFullNameFormatter
{
    string FullName(Person p);
    string FullName(Russian r);
}
 
public class Person
{
    public string GivenName { get; set; }
    public string Surname { get; set; }
    public virtual string FullName(IFullNameFormatter formatter)
    {
        return formatter.FullName(this);
    }
}
 
public class Russian : Person
{
    public string PatronymicName { get; set; }
    public override string FullName(IFullNameFormatter formatter)
    {
        return formatter.FullName(this); // Have to override, or this will not work!
    }
}
 
public class FullNameFormatter : IFullNameFormatter
{
    public string FullName(Person p)
    {
        return string.Format("{0} {1}", p.GivenName, p.Surname);
    }
    public string FullName(Russian r)
    {
        return string.Format("{0} {1} {2}",
            r.GivenName, r.PatronymicName, r.Surname);
    }
}

Jeg kan nå formaterer navnene til personene i listen min på følgende måte:

static void Main(string[] args)
{
    List<Person> people = new List<Person>
    {
        new Person { GivenName = "Jens", Surname = "Stoltenberg" },
        new Russian { GivenName = "Boris", PatronymicName = "Nikolayevich", 
            Surname = "Yeltsin" }
    };
 
    var formatter = new FullNameFormatter();
    people.ForEach(p => Console.WriteLine(p.FullName(formatter)));
}
Output:
Jens Stoltenberg
Boris Nikolayevich Yeltsin

Når jeg nå kaller FullName på russer-objektet, vil russeren kalle riktig Fullname i formatteren – og problemet er løst. FullName-logikken er nå samlet i én klasse, men splittet tydelig opp i ulike overloads i denne klassen. Ulempen er at vi må overstyre en metode i hver subklasse av Person.

Visitor er et relativt komplisert mønster, og man bør tenke seg om to ganger før man tar det i bruk. Dual dispatch er ikke alltid helt intuitivt, spesielt for mindre erfarne utviklere på teamet, og mange kan bli nervøse når de ser visitor-klasser og accept-metoder. God navngiving, som den jeg har brukt her, kan hjelpe på dette problemet.

Multiple dispatch

Og så har vi endelig kommet til multiple dispatch. I motsetning til single dispatch vil man nå avgjøre hvilken funksjon/metode som kalles basert på data som er tilgjengelig når programmet kjøres. I stedet for at det er et en-til-en forhold mellom et metodekall og en metode, kan et metodekall nå resultere i eksekveringen av ett av en hel rekke metoder.

Clojure har støtte for multiple dispatch, og her er et eksempel på hvordan det kan utnyttes. Først definerer jeg en variabel, people, som inneholder et array av fire personer. Personene er rett og slett maps (distionaries). Alle disse fire vil kreve ulike full name-regler.

(def people
     [{:given-name "Jens" :surname "Stoltenberg" :nationality "Norwegian"}
      {:given-name "Dong" :surname "Lee" :nationality "Korean"}
      {:given-name "Boris" :surname "Yeltsin" :nationality "Russian" 
       :patronymic-name "Nikolayevich"}
      {:given-name "Dung" :surname "Nguyen" :nationality "Vietnamese" 
       :middle-name "Tan"}])

Deretter skal jeg definere en multimethod for å formatere navnet. defmulti sier hva metoden heter, og hva den skal dispatche på. I dette tilfellet vil jeg dispatche på nasjonaliteten til personen, som er en nøkkel (et felt om du vil) i person-mapen. Man kan dispatche på andre ting også, inkludert type (for eksempel java-objekt type).

Til slutt kommer de ulike "overloadene" av full-name, hvor jeg spesifiserer hvilken verdi av nasjonalitet hver enkelt metode håndterer.

(defmulti full-name :nationality)

(defmethod full-name "Korean" [user]
  (str (:surname user) " " (:given-name user)))

(defmethod full-name "Russian" [user]
  (str (:given-name user) " " (:patronymic-name user) " " (:surname user)))

(defmethod full-name "Vietnamese" [user]
  (str (:surname user) " " (:middle-name user) " " (:given-name user)))

(defmethod full-name :default [user]
  (str (:given-name user) " " (:surname user)))

Den siste overloaded er spesifisert som default, og vil håndtere alle kall til full-name hvor det ikke finnes en bedre match.

Jeg kan nå iterere over alle personene og skrive ut nasjonaliteten og full name for hver på denne måten:

(doseq [p people]
       (println (:nationality p) ":" (full-name p)))
Output:
Norwegian : Jens Stoltenberg
Korean : Lee Dong
Russian : Boris Nikolayevich Yeltsin
Vietnamese : Nguyen Tan Dung

Jeg håper du lærte noe fra denne gjennomgangen. Jeg vet i alle fall nå hva forskjellen mellom single og multiple dispatch er for noe. Jeg lærte meg også mer om hvorfor vi av og til trenger å bruke VISITOR pattern, og vet nå om én ekstra ting jeg kan bruke .NET's nye dynamic feature til.


comments powered by Disqus