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

Opgaver

"Sådan én må vi også have"

- Mundheld

 

 

Motivation
Som altid, vil vi gerne have så løs en kobling som mulig mellem objekter. En af måderne at opnå dette, er ved at bruge abstrakte referencer. Det kan dog give en klient problemer, at den kun har et abstrakt kendskab til et objekt.
F.eks. kunne klienten ønske at lave nye instanser af den samme klasse som det objekt, den har en reference til. Problemet er blot, at klienten med en abstrakt reference ikke præcist ved hvilken klasse det pågældende objekt er en instans af.

 

Problem

Klienten skal lave instanser af en klasse som den ikke kender.
Vi har brug for at lave kopier af allerede eksisterende objekter.

 

Løsning

Løsningen er at placere opgaven der hvor den nødvendige viden er til stede, nemlig i objektet selv. Derfor delegeres instantieringen til objektet som returnerer den nye instans. Denne løsning gør det samtidig enkelt at lave kopier af objekter; hvor datakernen i det nye objekt bliver den samme som i det oprindelige.

 

Klassediagram

Prototype Pattern laves med en fabriksmetode, der kaldes clone, fordi den kopierer/kloner objektet.
Figur 1:
Klasse-diagrammet
Med navnene ConProto1 og ConProto2 virker det umiddelbart som om de to klasser har meget til fælles, men i virkeligheden er der ofte tale om vidt forskellige klasser, der kun har denne ene fabriksmetode som en del af deres interface.
Normalt vil en klient kende objekterne ved en anden del af deres interface - ellers kunne klienten ikke bruge objekterne til andet end at klone dem. For at kunne klone objekterne må klienten derfor caste referencen til Prototype før kaldet af clone. En alternativ løsning er at placere clone sammen med andre metoder, og dermed undgå at isolere den i sit eget interface.

 

Interaktion

Interaktionen er uhyre enkel:
Figur 2:
Kald af clone
Man vil normalt anvende en copy-konstruktor til at kopiere datakernen. Ved at sende en reference til sig selv med som parameter vil copy-konstruktoren kopiere den oprindelige datakerne.
Selve Prototype Pattern leder tanken hen på copy/paste af objekter, og interaktion kan da også illustreres med følgende diagram, der ikke skal læses alt for stringent mht. UML.
Figur 3:
Copy/Paste
Hvor man sender en copy-request til Prototypen og den svarer tilbage ved at paste med en kopi af sig selv.

 

Shallow vs. Deep Copy

Hvad skal der ske hvis Prototypen repræsenterer et objektsystem?
Hvis vi laver en almindelig kloning får vi følgende billede:
Figur 4:
Shallow copy af objekt-system
Her er den nye instans markeret med blåt, og man ser, at de referencer som det oprindelige objekt havde i sin datakerne er kopieret til det nye objekt.
Er det meningen, at de to Prototyper skal deles om det bagvedliggende objektsystem?
Hvis svaret er nej, har vi et problem!
Det resultat vi i så fald ønsker, er i stedet det følgende:
Figur 5:
Deep copy af objekt-system
Shallow eller Deep Problemet kaldes Shallow vs. Deep Copy. Shallow betyder overfladisk og deep betyder dyb. Betegnelserne beskriver udemærket de to former for kopiering af objektsystemer. Ønsker vi kun at kopiere det første objekt (shallow - figur 4) eller ønsker vi også at kopiere hele det bagvedliggende objektsystem (deep - figur 5)? Det er spørgsmålet om shallow eller deep copy.
Umiddelbart lyder det meget enkelt. Hvis vi har brug for deep copy - så laver vi det bare!
Helt så enkelt er det desværre ikke, for hvordan skal det gøres?
Man kan anlægge to forskellige strategier: Delegeret eller kontrolleret.
Delegeret Ved delegering anvendes clone-metoden delegeret. Når et objekt bliver bedt om at klone, vil det sende en tilsvarende request til de objekter den kender osv. På den måde vil clone-requesten propagere ud gennem objektsystemet og de forskellige objekter vil returnere dele af det klonede objektsystem som vil blive samlet af de forskellige clone-metoder, indtil det første objekt kan returnere det endelige resultat.
Den delegerende kloning er smuk, men der kan opstå problemer: Hvad hvis der er cycliske referencer, så clone-requests begynder at gå i ring? Så dur den delegerede kloning ikke.
Kontrolleret En kontrolleret løsning vil kræve viden om objektsystemets opbygning og det første objekt vil ikke delegere kloningen videre, men selv lave den.
Denne løsning er nemmere at overskue, men objektet der repræsenterer objektsystemet vil i kraft af sin viden være stærkt koblet til hele systemet. Vi får derfor en løsning, som er vanskelig at vedligeholde, men til gengæld samlet på et sted.
Distribueret eller centreret Man kan betegne de to løsninger som henholdsvis distribueret og centreret, idet den delegerede bygger på viden om objektsystemet som er fordelt ud over hele systemet, mens den centrerede har al viden placeret i én clone-metode, og dermed ét objekt.
Om man vælger at delegere eller kontrollere afhænger naturligvis af den konkrete situation, men ér det uproblematisk at delegere kloningen, bør man vælge det.

 

Implementation

Som nævnt er Prototype Pattern understøttet i Java, men vi vil først se hvordan det laves "manuelt". For ikke at kollidere med Java's understøttelse, vil vi i modsætning til ovenfor stave clone med stort: Clone.
Samtidig vil vi kun implementere den ene af subklasserne, nemlig ConProto1, da spørgsmålet om en eller flere subklasser ikke spiller nogen rolle i selve implementationen af Prototype Pattern.
Selve interfacet er ukompliceret:
interface Prototype {
  
  public Prototype Clone();
}
Clone implementeres i subklassen ConProto1, og man ser hvorledes den baserer sin funktionalitet på copy-konstruktoren:
class ConProto1 implements Prototype {
  private int value;
  
  public ConProto1( int v ) {
    set( v );
  }
  
  public ConProto1( ConProto1 p ) {
    set( p.value );
  }
  
  public void set( int v ) {
    value = v;
  }
  
  public String toString() {
    return "[ConProto1: value=" + value + "]";
  }
  
  public Prototype Clone() {
    return new ConProto1( this );
  }
}
Vi bruger set-metoden til at illustrere, at kloningen reelt er sket i følgende testanvendelse:
class PrototypeEksempel {
  
  public static void main( String argv[] ) {
    
    Prototype pAbs = new ConProto1( 5 );
    
    ConProto1 pCon = (ConProto1) pAbs;
    
    pCon.set( 4 );
    
    System.out.println( pAbs );
    
    Prototype q = pAbs.Clone();
    
    pCon.set( 3 );
    
    System.out.println( pAbs );
    System.out.println( q );
  }
}
Vi oppererer her med både en abstrakt reference (af klassen Prototype) til den oprindelige instans af ConProto1, og en konkret reference til samme objekt.
Figur 6:
Referencer i test-anvendelsen
q bruges som reference til den nye instans, som klones.
I testanvendelsen ændrer vi datakernen i objekterne og konstaterer at de er uafhængige af hinanden.

[ConProto1: value=4]
[ConProto1: value=3]
[ConProto1: value=4]

 

Understøttelse i Java

  Anvendelsen af Prototype Pattern er, som nævnt ovenfor, understøttet i Java.
1. brik I class Object er der erklæret en metode:
protected Object clone()
Metoden er native (dvs. implementeret platformsafhængigt) og kopierer datakernen i objektet - og dette på "magisk vis" uanset hvilken subklasse man måtte kalde den fra. Intet under, at metoden er native!
2. brik En anden brik i spillet er interfacet Cloneable. Interfacet indeholder intet, men ved at implementere det, giver man grønt lys for at kloning er tilladt for instanser af klassen.
3. brik Laver man alligevel en clone-metode, der kalder Object's clone-metode, uden at implementere Cloneable, vil Object's clone-metode kaste en CloneNotSupportedException.
Ovennævnte tre elementer: Object's clone-metode, interfacet Cloneable og CloneNotSupportedException, udgør til sammen Java's understøttelse af Prototype Pattern.
I forbindelse med anvendelsen af denne understøttelse er der en række problemer.
Kaster ingen exception! Først og fremmest skal vi gribe CloneNotSupportedException, selvom vi har implementeret Cloneable og exception derfor aldrig kan blive kastet. Ikke noget større problem, men selv med java-farvede briller, vel næppe et positivt træk.
 
Vi kunne f.eks. lave følgende simple klasse fra før:
class Prototype implements Cloneable {
  private int value;
  
  public Prototype( int v ) {
    set( v );
  }
  
  public void set( int v ) {
    value = v;
  }
  
  public String toString() {
    return "[Prototype: value=" + value + "]";
  }
  
  public Object clone() {
    try {
      return super.clone();
    }
    catch ( CloneNotSupportedException e ) {
      return null;
    }
  }
}
  Med en tilsvarende testanvendelse:
class CloneEksempel {
  
  public static void main( String argv[] ) {
    
    Prototype p = new Prototype( 5 );
    
    p.set( 4 );
    
    System.out.println( p );
    
    Prototype q = (Prototype) p.clone();
    
    p.set( 3 );
    
    System.out.println( p );
    System.out.println( q );
  }
}

[Prototype: value=4]
[Prototype: value=3]
[Prototype: value=4]

Konkret Der er et problem som ikke umiddelbart fremgår af ovenstående eksempel. For at kunne anvende clone-metoden skal klienten have et konkret kendskab til objektets klasse.
  Hvis klienten har en reference af klassen Object til de objekter den vil klone, kan den ikke kalde clone-metoden, da den er protected i Object's interface. I eksemplet har vi i stedet valgt at gøre clone-metoden public i subklassen og kalder derved indirekte den oprindelige clone-metode med et super-kald.
  Problemet kan løses ved på vanlig vis at lave et interface Prototype som alle objekter, der skal klones implementerer:
interface Prototype extends Cloneable {
  
  public Object clone();
}
  Med denne mixin-klasse forsvinder lidt af fordelen ved java's understøttelse, men det vigtigste er stadig tilbage: Den automatiske kopiering af datakernen.
Én fordel Den automatiske kopiering af datakernen er til gengæld også den eneste fordel ved Java's understøttelse af Prototype Pattern, og har man ikke brug for denne, er det normalt bedst at lave det hele selv, som det er gjort i afsnittet "Implementation".

 

Referencer

  [GoF94] s.117-126.