© 1999-2003, Flemming Koch Jensen
Alle rettigheder forbeholdt
Visitor Pattern

Opgaver

Forudsætninger:

Det forudsættes, at man kender java.io.File og dermed har kendskab til opbygningen af filesystemer i træstrukturer. Det er ikke optimalt at forudsætte denne viden for at kunne studere et pattern, men Visitor Pattern kommer kun til sin ret, hvis det anvendes i forbindelse med en interessant datastruktur, og i den forbindelse er filesystemet et kraftfuld og enkelt eksempel.

 

 

Motivation
Undgå at ændre i klasse Hver gang man tilføjer noget nyt til en klasse løber man den sædvanlige risiko når man ændre noget, nemlig at man indfører fejl. Så ofte forekommende er disse fejl, at man må kalkulere med, at de kommer. Når vi derfor må være forberedt på, at der kommer nye fejl, ville det være bedre, at disse fejl opstår i en ny, mindre, klasse som vi netop laver til den nye funktionalitet vi ønsker at tilføje.
Hvis vi f.eks. ønsker at udvide funktionaliteten af en container-klasse, med en ekstra metode, der gøre noget ved alle elementer i datastrukturen, kan problemet opstå. Det er i denne situation at Visitor Pattern kommer ind i billedet.

 

Problem

Man ønsker at udvide en datastrukturs funktionalitet, uden at skulle ændre i selve container-klassen.

 

Løsning

Vi vil lade containeren stå for selve traverseringen af datastrukturen. Det vil sige, at containeren fortsat skjuler selve opbygningen af datastrukturen - om det er en liste, et træ eller noget andet. Den funktionalitet vi ønsker at tilføje placerer vi derimod i en anden klasse, som vi generelt kalder Visitor.
Følgesvend Når vi ønsker at udføre funktionaliteten, giver vi containeren en instans af Visitor. Dette objekt bliver "følgesvend" for traverseringen af datastrukturen. Efterhånden som vi besøger de enkelte elementer i containeren giver vi også Visitor-objektet lejlighed til at "besøge" elementet.

 

Klassediagram

Lad os se et klassediagram, der beskriver de indgående klasser, med de metoder, der er nødvendige for at binde forløbet sammen.
Figur 1:
Klasse-diagrammet

void
Alle metoder i klassediagrammet har returtypen void.
Det er de to metoder accept og visit, der styrer Visitor's besøg hos elementerne i containeren.
accept Idéen med accept er at elementet modtager en request om at invitere en Visitorbesøg. Elementet kan vælge at lade være, men normalt vil accept altid invitere Visitor'en.
visit Elementet inviterer Visitor'en på besøg ved at kalde visit-metoden på Visitor-objektet. I dette kald sender den en reference til sig selv med som parameter. Visitor'en bruger denne reference til at kalde diverse metoder på elementet. Det er disse metodekald der udgør "besøget".
Overloaded visit Man bemærker at visit-metoden er overloaded i Visitor's interface. Generelt kan man ønske sig, at en Visitor kan besøge forskellige slags elementer. Ved at overloade metoden, opnår man bekvemt at den del af Visitor'en, der håndterer besøget af den pågældende slags element, udføres.

 

Interaktion

Lad os hvordan det mere konkret kan forløbe. Lad os antage at containeren repræsenterer en linket liste, der indeholder to elementer:
Figur 2:
Container med to elementer i linket liste

Man ser her hvorledes accept-kaldet propagerer ud til alle containerens elementer. De to elementer spiller "ping-pong" med instansen af ConVisitor1, idet visitor kalder tilbage til det element; hvor den selv er blevet kaldt fra.
 
Følgende sekvensdiagram illustrerer det tilsvarende forløb; hvor det er nemmere at se hvornår metoderne returnerer i forhold til hinanden:
Figur 3:
Inter-
aktionen

I diagrammet er de blå pile returneringer.

 

Implementation

Generelt har elementer kontakt til et vilkårligt antal andre elementer som accept-kaldet skal delegeres videre til, og det kan derfor være bekvemt at anvende en af Java's Collection-klasser til at opbevare disse i - f.eks. en LinkedList. Hvis vi simplificerer klassehierarkiet til kun at have én Element- og én Visitor-klasse, får vi følgende diagram:
Figur 4:
Element-
klasse med
LinkedList

 
Container-klassen og Element-klassen kunne være den samme; hvilket f.eks. kan være tilfældet hvis der er tale om et træ; hvor klienten har fat i rod-knuden:
Figur 5:
Element-
klasse og
Container-
klasse, den samme

 
boolsk accept Som udgangspunkt returnerer hverken accept eller visit noget (de er void), men man kunne måske i visse situationer ønske at accept-metoden returnerede boolsk om elementet havde accepteret den pågældende Visitor. Man kunne forestille sig at det kaldende element ville reagere på dette, f.eks. ved ikke at tilbyde Visitor'en som til andre elementer; hvis først én havde afvist den - variationerne er utallige!

 
Lad os se et kraftfuldt eksempel på Visitor Pattern.

 

Eksempel: class FileVisitor

Vi ønsker rekursivt at kunne traversere directories. Det rekusive ligger i, at vi vil gennemløbe samtlige filer i det pågældende directory, filerne i dens subdirectories osv.
Til at repræsentere filer og directories vil vi anvende java.io.File.
Lad os først se det generelle Visitor-interface:
import java.io.*;

interface FileVisitor {
  public void visit( File f );
}
Vi har her valgt at kalde interfacet FileVisitor, da de Visitor's vi her har i tanke, alle skal dreje sig om filer og directories (Vi skal lave flere FileVisitor's i opgave 1).
 
Vi realiserer dette interface med klassen ListVisitor:
import java.io.*;

class ListVisitor implements FileVisitor {
  
  public void visit( File f ) {
    if ( f.isDirectory() )
      System.out.println( "[" + f + "]" );
    else
      System.out.println( f.length() + " " + f );
  }
}
Vi har kaldet den ListVisitor, fordi den skal "liste" samtlige filer og directories den møder på sin vej.
I udskriften skelnes der mellem filer og directories ved at directories indrammes i kantede paranteser. length-metoden returnerer som bekendt en files størrelse (i bytes).
Som vi kan se, er vores elementer File-objekter, mere præcist er de instanser af følgende subklasse til File:
import java.io.*;

class FileNode extends File {

  public FileNode( File f ) {
    super( f, "" );
  }
  
  public void accept( FileVisitor visitor ) {
    visitor.visit( this );
    
    if ( this.isDirectory() ) {
      File[] files = listFiles();
      
      for ( int i=0; i<files.length; i++ ) {
        FileNode node = new FileNode( files[i] );
        node.accept( visitor );
      }
    }
  }
}
Vi har lavet denne subklasse, fordi File har alle de egenskaber vi kunne ønske for vores elementer - pånær én. Den har ikke nogen accept-metode - derfor subklassen.
Ved kald af accept-metoden, inviteres FileVisitor'en først til at besøge denne FileNode. Hvis FileNode'n er et directory, vil den efterfølgende opfordre alle dens filer og subdirectories til at invitere FileVisitor'en. Som bekendt returnerer listFiles-metoden et array af File's, som repræsenterer de filer og directories, der findes i det pågældende directory.
 
To skridt tilbage og ned Endelig har vi en testanvendelse, der træder to skridt tilbage på den sti der fører ned til hvor class-filerne befinder sig, og udskriver samlige directories og filer derfra og ned.
import java.io.*;

class Main {
  
  public static void main( String[] argv ) {

    FileNode root = new FileNode( new File( "../.." ) );
    root.accept( new ListVisitor() );
  }
}

...
[..\..]
[..\..\Visitor]
[..\..\Visitor\Eksempel - FileVisitor]
524 ..\..\Visitor\Eksempel - FileVisitor\FileNode.class
424 ..\..\Visitor\Eksempel - FileVisitor\FileNode.java
139 ..\..\Visitor\Eksempel - FileVisitor\FileVisitor.class
79 ..\..\Visitor\Eksempel - FileVisitor\FileVisitor.java
831 ..\..\Visitor\Eksempel - FileVisitor\ListVisitor.class
241 ..\..\Visitor\Eksempel - FileVisitor\ListVisitor.java
456 ..\..\Visitor\Eksempel - FileVisitor\Main.class
190 ..\..\Visitor\Eksempel - FileVisitor\Main.java
...

I udskriften er kun vist et mindre udsnit af den udskrift jeg fik ved at køre testanvendelsen.
 

- Afsnittet om varianter er under research -

Varianter

 

Flere Visitor's

<Det er mit oprindelige eksempel med DirectoryTree-klassen>

Kildetekster:

DirectoryTree.java
FileVisitor.java
ListVisitor.java
TestVisitor.java

 

Ændre datastrukturen


 

Relationer til andre patterns

 

Iterator Pattern

Da Visitor Pattern er stærkt knytte til traversering af elementerne i en container, er relationen til Iterator Pattern nærliggende. Man kan anvende et Iterator Pattern; hvor iteratoren modtager en Visitor, som den lader besøge samtlige elementer i containeren. Dette vil være en intern iterator, da traverseringen ikke vil være styrret af klienten, men af iteratoren.

 

Composite Pattern

accept-metodens måde at kalde den tilsvarende metode, på alle sine elementer, svarer til operation-metoden i Composite Pattern.

 

Referencer

  [GoF94] s.311-344.
  [Grand98] s.385-395.
 

Eksempel med filer og directories:

FileNode.java
FileVisitor.java
ListVisitor.java
Main.java