(Testa dig själv, svar på fråga 1)
Statisk bindning och designmönstret Visitor
Så här såg programkoden ut i fråga 1:
![]()
Frågan var om programkoden kompilerar. Och i så fall, vad händer när man kör programmet?
Svaret är: d (markera texten för att se svaret!) Läs nedan om du vill veta varför!
Funktionsanrop använder statisk uppslagningAntag att du har en klass A med en virtuell funktion foo. Funktionen foo överskrivs sedan i den nedärvda klassen B (engelska: override). Skriver man då kod av typen B b; A &ref_to_b = b; ref_to_b.foo(); så anropas foo i B. Man får alltså polymorfi tack vare dynamisk uppslagning. I Kodexempel 1 är situationen en annan. Där har vi en överlagrad funktion bar (dvs flera funktioner med namnet bar men med olika argument/signatur). När vi gör anropet bar(ref_to_b) på sista raden så måste kompilatorn ta reda på vilken av de två varianterna av bar som passar bäst för anropet. Den gör det genom att titta på den statiska typen på argumentet. I detta fall är den statiska typen "referens till A". Det sker alltså ingen dynamisk uppslagning. Så även om det är ett B-objekt som döljer sig bakom A-referensen, så är det bar(const A &) som kommer anropas! Visitor, när du får oväntat besökEtt intressant exempel där man använder både dynamisk och statisk uppslagning är designmönstret Visitor (the Visitor pattern, från boken Gamma et al. "Design patterns"). Antag att du håller på att skriva en C++-kompilator. Det första din kompilator gör när den får kod att kompilera är att skapa ett träd som motsvarar C++-grammatiken. De olika elementen i ditt program (tokens, operatorer, variabler, funktioner etc.) representeras av varsin klass. Alla klasser ärver från en interfaceklass TreeNode som definierar användargränssnittet för trädnoder (se artikel om interfaceklasser!). Eftersom C++-grammatiken är rättså omfattande blir det många klasser. När du väl skapat trädet har du alla möjligheter att göra intressanta analyser av ditt program. Några exempel är syntaxkontroll, semantisk kontroll, statisk kodanalys, kompilering, utskrift av trädet etc. Om man tar utskrift som ett exempel kan man tänka sig att implementera detta som en virtuell funktion print i basklassen TreeNode. Man överskriver sedan print i alla nedärvda klasser. Alla noder i trädet skriver ut sig själva och sina barn. Inga problem där. För varje ny analys du gör av ditt träd måste du lägga till en ny funktion i alla trädklasser, vilket innebär mycket jobb. Förmodligen leder det till att många filer måste kompileras om, vilket tar tid. Till slut börjar du önska att du slapp ändra alla klasser varje gång du inför en ny analys. Det är här Visitor kommer till undsättning! Visitor gör din arvshierarki till en klippa
Vi börjar bakifrån. Vi vill att vår arvshierarki ska förbli stabil och att inget ska ändras. Vi måste därför införa något som löser våra problem en gång för alla. Lösningen måste samtidigt kunna rymma alla framtida typer av analyser på vårt träd. Knepigt fall. Samarbete mellan statisk och dynamisk uppslagningVi lägger till en strikt virtuell funktion void visit_tree_node(Visitor &visitor) i basklasen TreeNode. Sen implementerar vi vår funktion i alla nedärvda klasser i trädet med samma kod (se Kodexempel 2). Tanken är att detta är ett engångsjobb och att visit_tree_node inte ska behöva ändras igen.
Det finns förstås en bra förklaring till varför vi har en kopia av visit_tree_node i varje klass. Det är här den statiska typen på argumenten kommer in i bilden. Vi ser att do_visit tar *this som argument. Säg t.ex. att visit_tree_node anropas i klassen Variable. Då har *this den statiska typen Variable &. Vi skapar nu en funktion do_visit(const Variable &) i vår klass Visitor. Vi anropar sen do_visit med Variable-objektet som argument. Och här kommer det fina. Vi har nu fått över objektet till Visitor-klassen med rätt typ (Variable i vårt exempel)! Därmed kan vi skriva ut objektet precis som vi vill. Vi skapar en funktion do_visit för alla andra typer i arvshierarkin som behöver specialiserad utskrift. De typer som inte behöver specialiserad utskrift kan vi utelämna och istället skapa en do_visit(const TreeNode &) som har en defaultutskrift!
Interfaceklass ger oss flera besökareVill vi lägga till ytterligare en typ av analys, såsom syntaxkontroll, kan vi göra om Visitor till en interfaceklass genom att göra alla funktioner strikt virtuella. Vi skapar sedan nedärvda klasser PrinterVisitor (för våra utskrifter) och SyntaxCheckVisitor som gör jobbet. Även här kommer dynamisk uppslagning in i bilden. Visitor är ett av de mer komplexa designmönstren och man måste ju erkänna att det är smart uttänkt! Det är dessutom väldigt nyttigt i vissa speciella situationer. Lycka till med designmönstren!
Relaterade artiklar:
© Johnny Bigert Data |
De la Gardies gränd 22, 135 63 Tyresö |
076-782 74 00
johnny@johnnybigert.se | www.johnnybigert.se |