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

Opgaver

Forudsætninger:

At man har et godt kendskab til Observer Pattern og Facade Pattern.

 

 

Motivation
Specielt når man laver grafiske brugergrænseflader har man behov for at en gruppe af objekter arbejder tæt sammen med hinanden. Det kræver normalt, at de enkelte objekter har referencer til de, af de andre objekter, som de skal kommunikere med. Dette resulterer ofte i et større netværk af referencer:
Figur 1:
Samarbejde mellem gruppe af objekter

Der er dog nogle problemer med dette design:
Kobling Det opstår en stærk kobling mellem objekterne, da de i så stor udstrækning kommunikerer direkte med hinanden.
Specialisering Klasserne blive normalt specialiserede til at indgå i det konkrete samarbejde, og det bliver derfor vanskeligt/umuligt at anvende dem i andre sammenhænge.
Alt i alt bliver designet en sammenfiltret knude af objekter og referencer, der bliver vanskelig at udvikle og endnu værre at vedligeholde.

 

Problem

Vi ønsker at en gruppe af objekter kan arbejde nært sammen, uden at de bliver stærkt koblede til hinanden, og uden at klasserne bliver specialiserede til at indgå i det konkrete samarbejde i gruppen.

 

Løsning

Løsningen er at placere et objekt i midten, der samler trådene - så at sige:
Figur 2:
Objekt i midten

Mægler Objektet i midten kaldes en Mediator (dk: mægler). Dens opgave er at formidle samarbejdet mellem objekterne. Den gør det ved at:
1 Delegere requests fra et objekt til et andet.
2 Distribuere events mellem objekter. Hvis f.eks. et eller flere objekter Observer et andet (se evt. Observer Pattern)
3 Have selvstændig eksistens som objekt, i forhold til objekter uden for gruppen.

Vi vil berøre disse punkter nærmere under implementationen.

 

Klassediagram

  Lad os se klassediagrammet:
Figur 3:
Klasse-diagrammet

Kollegaer Generelt kalder man de objekter som anvender en Mediator for Colleague's (dk: kollegaer).
Som man kan se er der ikke nogen fælles superklasse for hverken Mediator eller diverse Colleague-klasser. Man kunne godt forestille sig disse, men det er min erfaring at det er uhyre sjældent man kan have glæde af dem1, og at deres tilstedeværelse i det generelle klassediagram vil tilsløre, at specielt Colleague-klasserne kan være meget forskellige.
Reference til Mediator Fordi Colleague-klasserne kan være så forskellige, er det vanskeligt at påpege generelle metoder som disse skal have. Der skal dog være mulighed for at give instanserne en reference til Mediator; hvilket naturligt kan ske som parameter til konstruktoren ved instantieringen af Colleague-objekterne. Der vil sjældent være behov for set- og get-metoder i forbindelse med Mediator-referencen.

 

Interaktion

Interaktionen kan antage talrige former, så vi vil blot se et enkelt eksempel.
  En Mediator i den rent delegerende rolle er ukompliceret. Hvis derimod en ændring der indrapporteres fra en Colleague skal føre til ændringer i andre Colleague's er situationen mere interessant:
Figur 4:
Inter-
aktionen

Her indrapporterer et Colleague1-objekt en ændring til Mediator'en, som straks sender medelelser videre til Colleague2- og Colleague3-objekterne. Hvad de to sidste kald mere præcist gør, er et åbent spørgsmål. Det kan være ren event-propagering, men det kan også være, at Mediator tager den aktive rolle og gøre noget ved de to sidstnævnte Colleague's, ved at ændre deres tilstande.

 

Implementation

Vi vil inddele vores behandling af implementations-spørgsmål i de tre punkter der er nævnt under løsningen ovenfor:

 

Pkt. 1: Delegere requests fra en Colleague til en anden.

  Delegering af requests er den simpleste opgave en Mediator kan have. Man kan her vælge om Mediatoren skal være mere eller mindre Adapter.
Lidt Adapter Hvis den er lidt Adapter, er det tanken at Mediator'en skal være passiv, idet den blot sender kaldene videre til de tilhørende Colleague's. Man skal være forberedt på at en sådan Mediator's interface let kan få et betydelig omgang, idet alle metoder som de forskellige Colleague's har behov for at kalde på hinanden skal være repræsenteret i Mediator'ens interface. Disse metoders implementation vil blot være delegerende kald, på samme måde som operation-metoderne i et Handle (se evt. Handle Pattern).
Mere Adapter

Skal Mediator'en være mere Adapter, vil den ikke blot sende kaldene videre. Hvert kald til Mediator'en vil resultere i et eller flere kald til en eller flere Colleague's. På den måde er der ikke nogen 1-til-1 sammenhæng mellem signaturerne i Mediator's interface og kaldene til Colleague's.

 

Pkt. 2: Distribuere events.

Ikke kun GUI-events Distribuering af events er straks en mere kompliceret affære. Med events tænker vi ikke kun på events i GUI-sammenhæng, men også generelt på tilstandændringer, som bør give anledning til reaktioner i objektsystemet.
Delegeret Observer Pattern Eventhåndtering i GUI-forstand laves med listeners, der er Observere i et Observer Pattern; hvor Subject er event-kilden (se evt. Observer Pattern for betegnelserne Observer og Subject). Denne anvendelse af Observer Pattern kan naturligvis bruges generelt. Derfor kan man opbygge et delegeret Observer Pattern ved at lade Mediator'en være Observer på den Colleague som andre er interesseret i at observere, mens de Colleague's der er interesseret i at observere, kan melde sig som Observere hos Mediator'en, der så vil delegere update-kaldene videre når den modtager dem.
Figur 5:
Delegeret Observer Pattern

Man kan også vælge at give Mediator'en en mere aktiv rolle i dette samspil, og lade den kalde metoder på de Colleague's, der skal reagere på tilstandændringer. Det vil så være Mediator'ens opgave at realisere den effekt tilstandsændringen skal have i forbindelse med de pågældende Colleague's:
Figur 6:
Aktiv Mediator

 

Pkt. 3: Selvstændig eksistens overfor andre objekter.

Mediator ligner en Facade Her kunne man måske konstatere, at eftersom enhver Colleague samarbejder med de andre Colleague's gennem Mediator'en, er den altid i denne rolle. Enhver Colleague kan nemlig vælge at se på Mediator'en som en Facade der repræsenterer de andre, og sig selv som et eksternt objekt - en traditionel klient (se evt. Facade Pattern).
Ikke kun envejs Der er dog en forskel. Det samarbejde Mediator'en formidler er ikke nødvendigvis envejs. En Facade tilbyder en funktionalitet til omverdenen som den realiserer ved at anvende objekter i det bagvedliggende objektsystem. En Mediator er mere end det, den kan henvende sig til enhver Colleagueeget initiativ - en klient i Facade Pattern vil derimod aldrig opleve at Facaden pludselig begynder at sende den requests!
Udadtil Over for omverdenen kan Mediator'en derimod godt påtage sig en Facade-rolle; hvor den repræsenterer gruppen af Colleague's og den funktionalitet de kan tilbyde, mere eller mindre adaptet gennem Mediatoren.

 

Eksempel

Lad os se et eksempel med en aktiv Mediator.
Vi vil lave flg. simple GUI:
Figur 7:
Download-
panel i frame

Idéen med denne GUI er at man kan angive et filenavn i tekstfeltet og downloade den pågældende file ved at trykke på Download. Hvis man dernæst skulle ønske at afbryde før filen er blevet downloaded, kan man trykke på Cancel.
I ovenstående situation er tekstfeltet tomt, og de to knapper er derfor disabled, da det ikke vil give mening at trykke på dem.
  Er der derimod indtastet noget i feltet vil billedet være:
Figur 8:
Indtastet tekst i feltet

Download en nu enabled.
Sætter vi download i gang vil billedet blive:
Figur 9:
I gang med download

Vi kan nu ikke rette i tekstfeltet, og Cancel er den eneste mulighed - ud over at afvente download.
Trykker vi på Cancel vender situationen tilbage til figur 8.
Alt i alt er det en meget kompleks sammenhæng mellem tilstande og funktionalitet - specielt når man tager i betragtning, at der kun er tre komponenter.
Hvis vi placerede sammenhængene ud på de tre komponenter ville der kommer en stærk kobling mellem dem. Vi vil i stedet lade en Mediator implementere sammenhængene, så de tre komponenter ikke vil være specialiserede, men kunne genbruges. Faktisk er det sidste lidt flot sagt, for eksemplet er så simpelt, at vi kun bruger komponenter (JTextField og JButton), som i forvejen findes i Java (java.swing).
Man kan også fornulere det mere bramfrit - vi placerer alt rodet i Mediator'en!
Vi vil lade vores Mediator være en subklasse til JPanel, da den alligevel skal kende de tre komponenter:
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;

class DownloadPanel extends JPanel implements ActionListener, KeyListener {
  
  private JTextField tekstFelt;
  private JButton download, cancel;
  
  public DownloadPanel() {
    
    tekstFelt = new JTextField( 12 );
    tekstFelt.addKeyListener( this );
    
    download = new JButton( "Download" );
    download.setEnabled( false );
    download.addActionListener( this );
    
    cancel = new JButton( "Cancel" );
    cancel.setEnabled( false );
    cancel.addActionListener( this );
    
    setLayout( new FlowLayout() );
    add( tekstFelt );
    add( download );
    add( cancel );
  }
  
  public void actionPerformed( ActionEvent e ) {
    Object source = e.getSource();
    
    if ( source == download ) {
      download.setEnabled( false );
      cancel.setEnabled( true );
      tekstFelt.setEnabled( false );
    } else if ( source == cancel ) {
      cancel.setEnabled( false );
      download.setEnabled( true );
      tekstFelt.setEnabled( true );
    }
  }
  
  public void keyReleased( KeyEvent e ) {
    // Kan kun være tekstFelt'et
    
    if ( tekstFelt.getText().length() > 0 )
      download.setEnabled( true );
    else
      download.setEnabled( false );
  }
  
  /*
   * Desværre nødvendige stubbe
   */
   
  public void keyPressed( KeyEvent e ) {
  }
   
  public void keyTyped( KeyEvent e ) {
  }
}
I konstruktoren melder vi os som listener på komponenterne, og vi sætter deres initielle tilstand (diabled/enabled).
I actionPerformed-metoden håndterer vi events vedrørende de to knapper, mens keyReleased tager sig af tekstfeltet. Da vi er KeyListener, er vi desværre nød til at implementere hele tre metoder; hvoraf vi kun er interesseret i den ene.
Man bemærker at Mediator'en har hele initiativet - de tre Colleague's leverer kun events til Mediator'en; hvilket vi rent kodemæssigt ikke ser meget til, da vi ikke selv har lavet JTextField og JButton.

 

Varianter

 

ResourceCentral

  Der er måske tvivlsomt om ResourceCentral egentlig er en variant af Mediator Pattern, men det er en klasse jeg ofte anvender når en gruppe objekter har behov for at kende hinanden, så jeg kunne ikke finde et mere naturligt sted at omtale den, end her.
  ResourceCentral fjerner ikke de mange referencer mellem Colleague's, den forenkler i stedet tilgangen/initialiseringen af disse referencer. På den måde bliver Colleague's ikke nemmere at anvende i andre sammenhænge, men det besværlige arbejde med at lære diverse Colleague's hinanden at kende, bliver forenklet.
  Umiddelbart har man to muligheder når man skal skabe en bekendskab mellem to Colleague's:
 
1 Man kan anvende den enes konstruktor, ved at sende en reference med til den anden som parameter. Dette betyder at Colleague's skal instantieres i en bestemt rækkefølge.
 
Colleague1 c1 = new Colleague1( ... );
Colleague2 c2 = new Colleague2( ..., c1, ... );
  Det er nødvendigt for at man kan have referencen til den første, før man instantierer den anden.
2 Man kan anvende set-metoder i en eller begge Colleague's alt efter om kendskabet skal være gensidigt, eller det blot er den ene der skal kende den anden.
  Hvis man i den første løsning (den med konstruktoren), også skal have gensidigt kendskab, bliver den rigtig grim, da den andens konstruktor så skal kalde en set-metode på den første. På den måde vil den ene Collague opbygge kendskaberne, mens den anden passivt følger med.
  Den anden løsning er den bedste, men der findes en bedre løsning. Det er en løsning med løsere kobling mellem dén der instantierer objektsystemet, og selve Colleague's.
Container Løsningen er at lade et tredie objekt håndtere kendskabet til samtlige Colleague's. Den skal være en container, der indholder en reference til alle i gruppen. De forskellige Colleague's henvender sig til containeren når de ønsker en reference til en af de andre. Vi kalder denne container: ResourceCentral.
Navne Hvordan identificerers de enkelte Colleague's. Den enkleste løsning er at bruge navne i form af tekststrenge. Når en Colleague melder sig hos ResourceCentral, anfører den samtidig det navn som den vil/skal være kendt som. Når andre Colleague's får brug for en reference til den, anfører de det samme navn i kaldet af get-metoden.
Én reference er nok
Overførslen af referencer kan nu reduceres til én, nemlig den til ResourceCentral; hvilket vil være naturligt at gøre i forbindelse med instantieringen af de enkelte Colleague's. Dvs. at overføre referencen som parameter til konstruktorerne.
Melde sig selv Hvorfra får ResourceCentral kendskab til de enkelte Colleague's? Den mest distribuerede løsning er at lade dem selv "melde" sig hos den ResouceCentral de får, dvs. som en af de første linier i deres konstruktorer at kalde en add-metode på deres RecourceCentral.
 
class Colleague1 {
			  
  private ResourceCentral refs;
  
  public Colleague1( ..., ResourceCentral refs, ... ) {
    ...
	this.refs = refs;
	refs.add( "Kollega nr.1", this );
	...
  }
  
  ...
}
  Dette skaber dog én begrænsning i konstruktorernes virke: De må ikke anvende nogen af de andre Colleague's i forbindelse med instantieringen.
Caste reference
Når en Colleague får brug for en reference til en af de andre, kalder den en get-metode på ResourceCentral. Den må selv caste den returnerede reference til Colleague'ens klasse, da ResourceCentral generisk vil opbevare alle referencer som Object-referencer.
Colleague1 kollega = (Colleague1) refs.get( "Kollega nr.1" );
HashMap Selve implementationen af ResourceCentral er en relativ simpel anvendelse af HashMap (fra java.util). Der er en link til kildeteksten i slutningen af dette kapitel.

 

Relationer til andre patterns

 

Facade Pattern

Flerdimen-
sionel facade
Ser man alene på Mediator'ens delegering af requests fra én Colleague til de andre, er den en Facade. Den er en Facade som repræsenterer de andre Colleague's i gruppen. Alt efter hvilken Colleague man tager udgangspunkt i, er det skiftende hvilke objekter den er Facade for - man kunne kalde det en flerdimensionel Facade.
Overfor andre objekter kan Mediator'en fungere som Facade for hele objekt-gruppen.

 

Observer Pattern

Delegeret Observer-
forhold
Hvis Mediator'en skal holde øje med tilstandændringer i en Colleague, for igen at holde andre Colleague's underrettet om ændringer, kan man anvende Observer Pattern, idet Mediator'en vil observere den pågældende Colleague. Man kan igen lade de Colleague's der er interesseret i tilstandændringen være ObservereMediator, der vil delegere update-kaldene videre.

 

Referencer

  [GoF94] s.273-282.
  [Grand98] s.315-327.
 
fodnoter:  
1 I et sprog med rigtig multipel nedarvning (eg. C++) kan Colleague-klasserne dog have glæde af at nedarve selve referencen til Mediator, og eventuelle set- og get-metoder vedrørende denne.
 
 

Klassen ResourceCentral:

ResourceCentral.zip