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

Opgaver

Forudsætninger:

Det forudsættes, at man har kendskab til menuer i Swing (i forbindelse med eksemplet). Da menuer er det primære anvendelsesområde for Command Pattern vil det være uhensigtsmæssigt at beskæftige sig med dette pattern uden at se det anvendt i forbindelse med netop menuer.

 

 

Motivation

Det kan være bekvemt at kunne ændre, hvad der sker når man aktiverer visse ting i et program, og at disse ændringer kan ske mens programmet udføres. F.eks. kan det være rart at kunne ændre i en applikations menuer - hvad der sker når man vælger et menupunkt - mens applikationen kører, og at denne ændring får effekt med det samme.

Ofte er den slags systemer opbygges med følgende design for øje:
Figur 1:
At hændelse opstår i ét objekt, fører til udførelse af funktionalitet i et andet objekt

Designet har et grundlæggende problem. Koblingen mellem de to objekter vil ofte være stærk, og det giver derfor problemer at udskifte det udførende objekt med et andet.
Ingen af dem kan/vil tilpasse sig den anden En nærliggende måde at løse problemet på, ville være ved at lade de forskellige "funktions-objekter" realisere det samme interface, men vi ønsker at kunne anvende vidt forskellige objekter som "funktions-objekter". Situationen er derfor den, at objektet hvor hændelsen opstår ikke kan have noget at gøre med hvad der konkret skal ske, da vi ellers ikke ville kunne ændre funktionaliteten dynamisk. Samtidig vil "funktions-objektet" ikke tilpasse sig, at det bliver brugt i ovenstående sammenhæng. Alt i alt to objekter der ikke er smidige i vores design.

 

Problem

Vi ønsker nemt at kunne udskifte en funktionalitet, der udspringer af en hændelse i ét objekt, men udføres i et andet objekt. Dette besværliggøres af en stærk kobling mellem objekterne.

 

Løsning

Løsningen er at placere et objekt mellem de to objekter:

Figur 2:
Ønsket om udførelse af funktionalitet delegeres via et mellemliggende objekt

Når der opstår en hændelse, der skal føre til udførelsen af en funktionalitet, vil requesten blive delegeret via et mellemliggende objekt. På den måde kan vi opnå et abstrakt kendskab mellem det objekt hvor hændelsen opstår og det hvor funktionaliteten udføres - vi har placeret det konkrete kendskab i det delegerende objekt

 

Klassediagram

  Generelt bliver klassediagrammet for denne konstruktion følgende:
Figur 3:
Klasse-
diagrammet

Objektet hvor hændelsen opstår, er en instans af Invoker, og det objekt der skal udføre funktionaliteten en instans af Receiver.
Det delegerende objekt realiserer Command-interfacet, og vil i diagrammet ovenfor være en instans af ConCommand. Vi har kun vist én realisering af Command, men i en konkret anvendelse vil der naturligvis være flere, da det ellers ikke ville være muligt at udskifte én Command med en anden.
Klientens rolle er at opbygge objektsystemet. Efter systemet er opbygget træder klienten kun til hvis Command-objektet skal udskiftes
action-metoden repræsenterer en eller flere metoder I klasse-diagrammet er der ikke nogen super-klasse til Receiver. Det skyldes at Receiver'ne kan være meget forskellige, og at det kun er Command-objektet, som ved hvad action-metoden er. Man skal derfor ikke tænke i baner af action-metoden som kommende fra et interface som Receiver-klasserne realiserer. action-metoden kan udemærket være flere metoder, og execute-metoden kan have adskillige kald af diverse metoder på Receiver'en for at implementere den ønskede kommando. Man kan her drage en analogi til begrebet "mere eller mindre adapter", som det kendes fra Adapter Pattern.

 

Interaktion

Forløbet i følgende sekvensdiagram er relativ kort, men skal alligevel læses i to dele:
Figur 4:
Inter-
aktionen

Først har vi opbygningen af systemet; hvor klienten laver en instans af ConCommand og giver den til Invoker-objektet. Forbindelsen fra ConCommand-objektet til Receiver-objektet etableres ved kaldet af ConCommand's konstruktor; hvilket ofte er bekvemt.
På et senere tidspunkt opstår der en hændelse i Invoker'en og den sender en request til ConCommand-objektet, om at få udført funktionaliteten. ConCommand'en kalder videre til Receiver-objektet som gør det der skal gøres.

 

Implementation

Vi vil først lave en simpel implementation af det foregående.
Først har vi Command-interfacet og realiseringen ConCommand:
interface Command {
  
  public void execute();
}
class ConCommand implements Command {
  
  private Receiver receiver;
  
  public ConCommand( Receiver receiver ) {
    this.receiver = receiver;
  }
  
  public void execute() {
    receiver.action();
  }
}
I det generaliserede eksempel er disse meget simple, men i praksis behøver execute ikke kun delegere metode-kaldet videre, á la Handle Pattern, men kan (som nævnt ovenfor) være mere over i Adapter Pattern, idet den gøre et større arbejde i forbindelse med anvendelsen af Receiver.
Dernæste følger Invoker'en som ConCommand'en tilmeldes:
class Invoker {
  
  private Command command;
  
  public void setCommand( Command command ) {
    this.command = command;
  }
  
  public void event() {
    command.execute();
  }
}
Igen simpelt - en reference sættes og en metode kaldes via denne.
Receiver'en er endnu mere simpel:
class Receiver {

  public void action() {
    System.out.println( "Gør noget..." );
  }
}
Da dette ikke er et konkret eksempel har vi reelt ikke nogen funktionalitet, og klassen er derfor uhyre enkel.
En test-anvendelse kunne være følgende:
class TestCommand {
  
  public static void main( String[] argv ) {
    
    // Klientens opbygning af systemet
    
    Receiver rec = new Receiver();
    
    Command com = new ConCommand( rec );
    
    Invoker inv = new Invoker();
    inv.setCommand( com );
    
    // Test
    
    inv.event();
  }
}

Gør noget...

 

Eksempel: Menuer i Swing

I forbindelse med menuer i Swing kan vi anvende Command Pattern. Vi kan gøre det generelt med følgende klasse-diagram, hvor klasserne er placeret så de afspejler klasserne i figur 3.
Figur 5:
Action-
Listener
som Command

Her er JMenuItem Invoker'en og ActionListener er Command-interfacet.
Idéen med at anvende Command Pattern kan bruges i forbindelse med alle Listener's, men den er specielt nærliggende med JMenuItem, da man nogle gange ønsker dynamisk at ændre, ikke alene hvilke menupunkter der optræder på en menu, men også hvad de gør.
I vores eksempel vil vi ikke tage udgangspunkt i en bestemt applikation, men generelt vise hvordan man kan implementere ovenstående og dynamisk udskifte Command-objektet.
Vi vil lave to subklasser til ActionListener, med konkret kendskab til hver deres Receiver-klasse.
Den første benytter sig af en Receiver, der kan udskrive dags dato (og klokkeslet). Bemærk at Command'en får kendskab til Receiver'en gennem konstruktoren.
import javax.swing.*;
import java.awt.event.*;

class VorCommand1 implements ActionListener {
  private Receiver1 receiver;
  
  public VorCommand1( Receiver1 receiver ) {
    this.receiver = receiver;
  }
  
  public void actionPerformed( ActionEvent e ) {
    receiver.currentDate();
  }
}
import java.util.*;

class Receiver1 {
  
  public void currentDate() {
    System.out.println( new Date() );
  }
}
Den anden Command gør selv et større arbejde, idet den anvender Receiver'eren fire gange, for at udskrive en tekst på fire linier:
import javax.swing.*;
import java.awt.event.*;

class VorCommand2 implements ActionListener {
  private Receiver2 receiver;
  
  public VorCommand2( Receiver2 receiver ) {
    this.receiver = receiver;
  }

  public void actionPerformed( ActionEvent e ) {
    receiver.println( "Denne konkrete Command" );
    receiver.println( "laver en række kald" );
    receiver.println( "af Receiver'en, som" );
    receiver.println( "udskriver nogle linier." );
  }
}
class Receiver2 {
  
  public void println( String s ) {
    System.out.println( s );
  }
}
Ovenstående fire klasser er naturligvis meget enkle, men de viser hvordan Command-objekter bruger tilhørende Receiver-objekter.
Vi anvender nu de to Command's som ActionListener's på et menu-punkter (dog kun én af gangen):
import javax.swing.*;
import java.awt.event.*;

class CommandFrame extends JFrame implements ActionListener {
  
  private JMenuItem item1;
  
  private ActionListener command1, command2;
  private ActionListener current;
  
  public CommandFrame( String title ) {
    super( title );
    
    JMenuBar bar = new JMenuBar();
    
    JMenu menu = new JMenu( "Menu" );
      
      command1 = new VorCommand1( new Receiver1() );
      command2 = new VorCommand2( new Receiver2() );
      
      item1 = new JMenuItem( "Do something" );
      item1.addActionListener( command1 );
      current = command1;
      menu.add( item1 );
    
      JMenuItem item2 = new JMenuItem( "Swap command" );
      item2.addActionListener( this );
      menu.add( item2 );
    
    bar.add( menu );
    
    setJMenuBar( bar );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    setSize( 400, 300 );
    setVisible( true );
  }
  
  public void actionPerformed( ActionEvent e ) {
    item1.removeActionListener( current );

    if ( current == command1 ) {
      item1.addActionListener( command2 );
      current = command2;
    } else {
      item1.addActionListener( command1 );
      current = command1;
    }
  }
}
Det første menu-punkt - Do something - har tilknyttet én ActionListener, der enten er en VorCommand1 eller en VorCommand2. Hver gang det andet menu-punkt - Swap Command - vælges, skifter vi mellem hvilken der er ActionListener. Med referencen current har vi hele tiden fat i den aktuelle ActionListener på det første menu-punkt.
Figur 6:
Action-
Listener
som Command

 

Relationer til andre patterns

Handle Pattern Command Pattern ligner en hel del Handle Pattern. I Command Pattern er vi dog ikke så fokuserede på at flere, af hinanden uafhængige objekter, kan være kilde til de hændelser der skal udløse en funktionalitet. Det er mere klientens manglende rolle i forbindelse med udskiftningen, der interesserer os. I Command Pattern taler vi dog ikke om nogen egentlig klient, da vi også tager opbygningen af objektsystemet ind i billedet.
  Selv om vi ikke fokuserer så meget på flere objekter, som "hændelses-kilde" til samme funktionalitet, vil en sådan konstruktion fungere problemfrit i Command Pattern. F.eks. ser man ofte at et menu-punkt også findes som icon i en toolbar, så brugeren kan vælge at aktivere funktionaliteten med den hændelse han finder mest bekvem.
Adapter Pattern Command-objektets anvendelse af Receiver kan være mere end blot ét kald af en action-metode. Command-objektet kan realisere en funktionalitet ved at kalde flere forskellige metoder på Receiver'en, og på den måde fungere som en slags "mere adapter"

 

Referencer

  [GoF94] s.233-242.
  [Grand98] s.277-287.

Generelt eksempel:

Invoker.java
Command.java
ConCommand.java
Receiver.java
TestCommand.java

Swing eksempel:

VorCommand1.java
VorCommand2.java
Receiver1.java
Receiver2.java
CommandFrame.java
TestCommand.java