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

Opgaver

 

 

Design idé
Rekursiv Whole-Part

Composite Pattern arbejder med en Whole-Part træ struktur; hvor klienten har et abstrakt Whole-kendskab til roden, der igen har Whole-Part undertræer med en tilsvarende abstraktion i tråd med det sædvanlige rekursive perspektiv med træer og undertræer (se evt. Whole-Part Pattern).

 

Eksempel

Lad os for en stund betragte følgende tegning:
Figur 1:
Kunst

Det er grimt, og jeg aner ikke hvad FALUMP betyder, men det er lige meget - det er kunst!

Lad os splitte dette kunstværk i en række grafiske bestandele:
Figur 2:
Kunst-stumper

Man bemærker at jeg har snydt lidt - der er flere af de grafiske stumper der går igen. F.eks. står der FALUMP to gange: En gang alene og en gang i det grønne rektangel - det grønne rektangel er der selv to gange.
Grunden er, at vi vil ordne disse brudstykker så de viser hvordan den samlede figur er opbygget af delfigurer:
Figur 3:
Figurer sammensat af delfigurer

Lad os endelig se denne opbygning med de briller man har på, når man arbejder med Composite Pattern:
Figur 4:
Det hele er figurer

Atomare Det er nok et mere kedeligt perspektiv, men meningen er god nok: Alt er figurer. En figur er opbygget af delfigurer, og på det nederste niveau har man de atomare figurer som ikke kan opdeles yderligere.
Rekursivt En anden ting man bemærker er det rekursive i træstrukturen, man kunne kalde det "en del af noget, der er en del af noget, der er..."-princippet.
 
Abstrakt at forstå Indtil videre har vi ikke berørt noget der nærmer sig et edbsystem, men det er ikke uden grund. Når man første gang stifter bekendskab med Composite Pattern, vil man finde det meget abstrakt. Vi har derfor valgt at introducere grundidéen i en ikke-edb-sammenhæng.

 

Klassediagram

Hvordan bruger man så dette "en del af noget, der er en del af noget, der er..."-princip i objektsystemer? Lad os se klassediagrammet i Composite Pattern.
Figur 5:
Klasse-diagrammet

Man genkender her betegnelsen leaf (dk.: blad) fra træstrukturer. Alle blade i vores Composite-træ er instanser af Leaf-klassen. I diagrammet er der kun én Leaf-klasse, men der kan være adskillige, alt efter hvor mange forskellige slags atomare "ting" der kan være i vores træ. I vores FALUMP-eksempel ville vi have tre Leaf-klasser; Linie, Rektangel og Tekst. Alle Leaf-klasser er karakteriseret ved, at de i modsætning til Composite-klassen ikke har referencer af superklassen Component's type.
Hvorfor har et Composite-objekt referencer?
Det har det for at kunne holde styr på sine børn. Composite-objekter repræsenterer nemlig alle de knuder i Composite-træet som har børn - i modsætning til Leaf-objekterne. Da et Composite-objekt har børn, skal den have container-metoder, der gør det muligt at tilføje og fjerne børn - disse er add- og remove-metoderne. Man ser at de tager en reference af typen Component som parameter. Det skyldes at en knudes børn kan være såvel blade som andre knuder med børn.
Hvad er operation-metoden?
Den repræsenterer én eller flere metoder som vi ønsker at kalde på samtlige knuder i Composite-træet. Når vi kalder metoden på et Composite-objekt vil det kalde den tilsvarende metode på sine børn. På den måde vil kaldet af operation-metoden propagere ned gennem, og rundt i, træet.

 

Objektdiagram

Lad os se hvordan et typisk Composite-objektsystem kan være opbygget:
Figur 6:
Objektsystem

Man bemærker at Leaf-objekter aldrig delegerer videre, i modsætning til Composite-objekter, hvor det altid er tilfældet. I eksemplet ovenfor har klienten fat i et Composite-objekt, men den kunne lige så vel have haft fat i et Leaf-objekt, da referencen er af typen Component (Det sidste vil dog normalt være en uinteressant situation).
På denne måde får klienten et abstrakt forhold til hele træet - den kan ikke se det. Objektet Comp 1 repræsenterer hele træet over for klienten, og den skal ikke forholde sig til at den indirekte kommunikerer med mange objekter - det er der andre der tager sig af!

 

Interaktionsdiagram

Følgende sekvensdiagram illustrerer hvorledes et operationskald fra klienten propageres ud gennem træstrukturen i figur 6 (Visse af objekt-navnene er forkortet afht. diagrammets bredde).
Figur 7:
Interaktionen
Man kan sige at sekvensdiagrammet prøver at gøre det to-dimensionelle træ en-dimensionelt; hvilket gør det vanskeligt at gennemskue hvad der sker. Man skal næsten forstå idéen før man ser sekvensdiagrammet, men så er det til gengæld illustrativt.

 

Implementation

Composite Composite er et container objekt, der holder styr på en række Component's. I den forbindelse skal den naturligvis have de sædvanlige container metoder, så det er muligt at tilføje og fjerne Component's fra den. Om man vil tage skridtet og implementere Collection interfacet fra java.util, er så en anden sag. Det vil forpligtige én til at implementere mange metoder; hvoraf man måske kun har brug for få.
 

Det er ofte bekvemt at implementere selve container-delen af Composite vha. af en LinkedList. Normalt har man ikke brug for nogen ordning af Component's, men skulle det være tilfældet kan man naturligvis vælge en anden containerklasse fra java.util. Hvis man laver sin egen container vil det måske være passende at bruge Iterator Pattern. operation-metoderne vil alle være gennemløb af de Component's den har, og et kald af den tilsvarende operation-metode på disse.

Leaf Leaf arver også container metoderne, men implementerer enten stubbe eller kaster evt. en NoSuchMethodException. den kan naturligvis ikke tilføje elementer, eftersom den ikke selv er en container. Der er umiddelbart lagt op til at Leaf har de operationelle opgaver mht. det der delegeres gennem træet, hvorfor implementationen af operation-metoderne vil bære det algoritmiske i dette pattern. Grunden til at Leaf må have container-metoderne i sit interface er den helt grundlæggende idé i dette pattern, at hverken klienten eller Composite må vide om de refererer til en Composite eller et Leaf. Dette abstrakte forhold er afgørende for Composite Patterns styrke.
Component

Component kan ofte implementeres som et interface. Composite og Leaf vil normalt ikke have glæde af at arve noget fra den, da de er så forskellige. Hvis man vil implementere Collection interface, skal Component naturligvis være en abstrakt klasse. Da det reelt kun er Composite der har container-egenskaber, selv om Leaf også har container-metoderne i sit interface, vil der ikke være nogen mening i at placere container-delen i Component, da Leaf ikke vil være interesseret i at arve den.

Subklasser

I nogle tilfælde kan det være nyttigt at subklasse Leaf og/eller Composite. Hvis man f.eks. har flere Leaf klasser med fælles egenskaber, kan de have glæde af at deles om en superklasse. Umiddelbart er det måske vanskeligt at forestille sig flere forskellige Composite-klasser, da implementation er generel. Dog kunne der evt. være tale om en forskellig behandling af Component's mht. at delegere operationskaldene. Det kan være at forskellige Composite's kalder dem i forskellig orden, eller at de har forskellige filtre, så det kun er visse Component's der delegeres videre til.

 

Eksempel: Hær

I dette eksempel vil vi lave en hær, som vi så vil kommandere rundt med. Composite idéen er at en hær-enhed kan bestå af andre hær-enheder, der i sidste ende består af forskellige soldater.

Component-klassen skal hedder Platoon og Composite-klassen Unit. Normalt er navngivningen af Component og Composite et spørgsmål om at finde to synonymer for den samme ting. Vores Leaf-klasser skal repræsenere forskellige soldater. Vi vil her holde os til to slags: Soldier og Grenadier. Den operation vi vil bruge som eksempel, er at flytte soldaterne rundt, ved først at give ordren til enheden, som giver den videre til dens enheder og/eller soldater. Oplysningen om hvor en soldat befinder sig, vil vi lade være en del af soldaternes tilstand, og enhederne vil ikke selv have nogen tilstand.

Klassediagrammet med vores betegnelser bliver:
Figur 8:
Klassediagram for hær-eksemplet
Vi undlader en fælles superklasse til Soldier og Grenadier, da der reelt ikke er noget at placere i den.
Unit har en samling af Platoons uden ordning; hvorfor vi blot vælger en LinkedList til formålet. Metoden der skal give ordre om at flytte enhederne vil vi give signaturen:
void moveTo( Point pos )

Vi vil lade positions-tilstandene i Soldier og Grenadier være Points (Bemærk at Point kommer fra java.awt). For bedre at kunne se hvad der sker, vil vi lade Soldiers og Grenadiers svare når de bliver kommanderet rundt. Vi vil lade svaret variere alt efter hvilken type soldat der er tale om.

Vores implementation bliver som følger:
import java.awt.Point;

public interface Platoon {
  public void add( Platoon P );
  public void moveTo( Point p );
}
Vi nøjes med en add-metode, da det tilstrækkelig for vores testanvendelse.
import java.util.LinkedList;
import java.awt.Point;

public class Unit implements Platoon {
  private LinkedList<Platoon> enheder;

  public Unit() {
    enheder = new LinkedList<Platoon>();
  }

  public void add( Platoon p ) {
    enheder.add( p );
  }

  public void moveTo( Point p ) {
    for ( Platoon enhed : enheder )
      enhed.moveTo( p );
  }
}
Man ser her at for-løkken gennemløber samtlige børn, og sender moveTo-kaldet videre til hver af dem.
Dernæst har vi de to soldater-klasser:
import java.awt.Point;

public class Soldier implements Platoon {
  private Point position;

  public void add( Platoon P ) {}

  public void moveTo( Point p ) {
    System.out.println( "Yes, Sir! Moving to " + p + ", Sir!" );
    position = p;
  }
}
import java.awt.Point;

public class Grenadier implements Platoon {
  private Point position;

  public void add( Platoon P ) {}

  public void moveTo( Point p ) {
    System.out.println( "On my way Sir! Moving to " + p );
    position = p;
  }
}
De to klasser er i hovedsagen ens opbygget - kun tekststrengen er forskellig. Bemærk at vi laver en stub til add-metoden.
En testanvendelse kunne f.eks. være:

import java.awt.Point;

public class Main {

  public static void main( String argv[] ) { 
    Platoon deling = new Unit();
    
    deling.add( new Soldier() );
    deling.add( new Soldier() );
    deling.add( new Soldier() );
    
    Platoon hær = new Unit();

    hær.add( deling );
    hær.add( new Soldier() );
    hær.add( new Grenadier() );
    hær.add( new Grenadier() );

    hær.moveTo( new Point( 5, 10 ) );
  }
}

Yes, Sir! Moving to java.awt.Point[x=5,y=10], Sir!
Yes, Sir! Moving to java.awt.Point[x=5,y=10], Sir!
Yes, Sir! Moving to java.awt.Point[x=5,y=10], Sir!
Yes, Sir! Moving to java.awt.Point[x=5,y=10], Sir!
On my way Sir! Moving to java.awt.Point[x=5,y=10]
On my way Sir! Moving to java.awt.Point[x=5,y=10]

- Eksemplet med binære træer er ikke færdig -

Eksempel: Binært søgetræ

Da composite opbygger en træstruktur, vil det være nærliggende at se en anvendelse der relaterer til et almindeligt binært søgetræ. Det skal dog bemærkes at følgende eksempel i virkeligheden er en variant af composite; hvor processen er distribueret ud over hele træstrukturen og ikke kun ligger i bladene.

I de traditionelle implementationer af binære søgetræer spiller knuderne en passiv underlæggende container-rolle; hvor et overordnet container objekt traverserer træet og inspicerer de enkelte knuders tilstande efter behov. Hvis man i den forbindelse skulle udstyre knuderne med metoder, vil der derfor højest være tale om en række set/get metoder. Derimod kunne man måske forestille sig en implementation af et binært søgetræ; hvor knuderne spille en aktiv rolle ved gennem delegering at traversere strukturen. Lad os tage udgangspunkt i et almindelig binær søgning.

  Passive knuder vil typisk være instancer af klassen:
Source #:
passiv class Node
class Node {
  public int value;
  public Node left, right;
}

Man ser at knudernes passive rolle, bevirker at data ikke kan være indkapslede af hensyn til det overordnede container objekt, der skal have adgang til dem (man kunne som sagt godt lave set/get metoder, men det ville kun have teoretisk interesse).

Et eksemplificeret objektdiagram med et kald af find(40) ville se således ud:
<objektdiagram med aClient, aTree, og et træ med knuderne 35, 14, 58, 4, 20, 40, 72>

Selve find-kaldet ender i aTree som tager den aktive rolle og traverserer de passive knuder i træet.

Hvis man derimod havde knuder der kunne deltage aktivt ved at delegere find-opgaven videre til de andre, kunne forløbet være:
<objektdiagram med aClient, aTree, og et træ med knuderne 35, 14, 58, 4, 20, 40, 72>

Her er container objektets rolle blot at delegere find-kaldet videre til selve træstrukturen.

  class Node skal nu i stedet have en find-metode:
Source #:
class Node med delegering af find-metode

class Node {
  private int value;
  private Node left, right;

  public Node find( int v ) {
    if ( v < value && left != null )
      return left.find( v );
    else if ( v > value && right != null )
      return right.find( v );
    else if ( v == value )
      return this;
    else
      return null;
  }
}

Man bemærker at det nu er muligt at indkapsle knudens variable; hvilket dog i det mindste vil kræve en konstruktor med mulighed for at sætte værdierne. Vi udelader den indtil videre.

I tråd med composite pattern kan man herefter udelade det overordnede container objekt og lade client referere direkte til roden, da den kun vil kende den som enhver anden knude. Hvor abstrakt er kendskabet mellem client og roden? Vi har kun én klasse Node! I composite pattern er der en abstrakt super klasse Component med minimum to subklasser; selve Composit'en og en eller flere Leaf klasser. Grunden til at vi her kun har en klasse, er at der ikke er nogen Leaf klasser. Blade optræder ved at knuder ikke har nogen børn, ikke ved at de er instancer af en speciel Leaf klasse.

  Lad os for eksemplets skyld lave en sådan Leaf klasse med Component klasse Tree:

Source #:
Component, Composite og Leaf klasse

interface Tree {
  public Node find( int v );
}

class Node implements Tree {
  ...

class Leaf implements Tree {
  private int value;

  public Node find( int v ) {
    if ( v == value )
      return this;
    else
      return null;
  }
}

 

Med en sådan Leaf klasse vil vi som sagt kun sigte mod at bringe implementationen mere i tråd med composite pattern; der vil ikke være noget vundet ved det. Tværtimod vil f.eks. insert-metoden blive kompliceret sammenlignet en traditionel implementation.

 

Referencer

  [GoF94] s.163-173.
  [Grand98] s.165-174.