Composite

Pattern descritto in Design Pattern.

Lo scopo del pattern Composite é permettere al client di gestire oggetti e gruppi di oggetti uniformemente in una struttura ad albero.

Partecipanti
  • Client: gestisce gli oggetti nella struttura per mezzo dell'interfaccia Component.
  • Component: dichiara l'interfaccia degli oggetti nel gruppo composto; implementa il comportamento di default per tutte le classi nella struttura; dichiara un'interfaccia per accedere e gestire le componenti figlie; se necessario può definire un'interfaccia per risalire la struttura ad albero.
  • Foglia: elemento terminale della struttura ad albero.
  • Composite: definisce il comportamento per componenti che hanno figli; contiene i le componenti figlie; implementa le operazioni per la gestione dei figli dichiarate nella interfaccia Component.
Un punto delicato di questo pattern é il fatto che Component deve nascondere al client il tipo reale dell'oggetto sottostante, sia esso un Composite o una foglia. Ma questo va in conflitto con un criterio di base della progettazione Object-Oriented, dato che siamo forzati a dichiarare in Component metodi che non hanno senso a quel livello. E infatti la classe foglia si trova a dover implementare metodi che non hanno alcuna utilità in quel contesto. Una semplice soluzione é di dare una implementazione di default ai metodi in Component che sollevi un'eccezione.

Esempio in Java

Torniamo al nostro progetto fantasy. Vogliamo poter create compagnie che possano partire per favolose missioni. Dal punto di vista del giocatore, la sua compagnia é a volte vista come un unico complesso che agisce ai suoi comandi, anche se spesso é composta da elementi che hanno caratteristiche ben differenziate.

In questo caso potrebbe essere opportuno implementare la compagnia usando questo pattern.

Implementiamo un caso molto semplice, in cui gli elementi della nostra compagnia son tutti umani, e vogliamo distinguerli in due soli sottogruppi, uno composto da persone normali e uno da guerrieri. Questi ultimi sono allenati al combattimento ma, per quel che ci interessa ora, sono molto più lenti nei movimenti, in quanto appesantiti dalle loro armi.

Le classi coinvolte nel nostro esempio saranno dunque queste:
  • Man: é una foglia del pattern, definisce il comportamento degli umani
  • Warrior: anch'essa foglia del pattern, specializza Man
  • Group: é il Composite del pattern, gestisce i gruppi all'interno della compagnia
  • GroupComponent: é il Component del pattern
Vediamo il codice, iniziando da GroupComponent:

public abstract class GroupComponent {
protected String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void add(GroupComponent component) {
throw new UnsupportedOperationException();
}

public void remove(GroupComponent component) {
throw new UnsupportedOperationException();
}

public void list() {
throw new UnsupportedOperationException();
}

public int getVelocity() {
throw new UnsupportedOperationException();
}
}

La classe é astratta, dunque non possiamo istanziare oggetti di questo tipo. Ogni elemento nella compagnia ha un nome, e quindi mettiamo qui il membro name e i suoi getter e setter. C'é poi ci sono i metodi con la loro implementazione di default, che si riduce a lanciare un'eccezione.

Vediamo ora l'implementazione della foglia:

public class Man extends GroupComponent {
private final int DEFAULT_VELOCITY = 8;
int velocity;

public Man(String name, int bonus) {
super.name = name;
this.velocity = DEFAULT_VELOCITY + bonus;
}

@Override
public int getVelocity() {
return velocity;
}

public void setVelocity(int velocity) {
this.velocity = velocity;
}

@Override
public void list() {
System.out.println("Man name: " + name + ", velocity " + velocity);
}
}

I membri "reali" della compagnia hanno una certa velocità, che é tipica della classe, ma che può essere modificata dal costruttore. Due metodi della GroupComponent vengono ridefiniti qui, chiamare altri metodi di quella classe in questo contesto genererà un eccezione.

Per i guerrieri useremo questa specializzazione:

public class Warrior extends Man {
public Warrior(String name, int bonus) {
super(name, bonus);
super.velocity /= 2;
}
}

Cambia solo il costruttore, che causa una penalità alla velocità del personaggio.

La classe Group ci permette di rappresentare i diversi gruppi omogenei all'interno della nostra compagnia. Ridefinirà quindi i metodi di GroupComponent per permettere di gestire i suoi nodi:

import java.util.ArrayList;
import java.util.Iterator;

public class Group extends GroupComponent {
ArrayList components = new ArrayList();

public Group(String name) {
super.name = name;
}

@Override
public void add(GroupComponent component) {
components.add(component);
}

@Override
public void remove(GroupComponent component) {
components.remove(component);
}

@Override
public void list() {
System.out.println("\nGroup Name: " + name);

Iterator it = components.iterator();
while(it.hasNext()) {
it.next().list();
}
}

@Override
public int getVelocity() {
int velocity = Integer.MAX_VALUE;
Iterator it = components.iterator();
while(it.hasNext()) {
int candidate = it.next().getVelocity();
if(velocity > candidate)
velocity = candidate;
}
return velocity;
}
}

Notiamo che il metodo list() itera su tutti i nodi posseduti da questo gruppo, mostrando quindi i dati di base per tutti gli elementi.
La getVelocity() ritorna la velocità dell'elemento più lento del gruppo, dato che sarà lui a determinare la velocità di tutto il gruppo. Nota che se non ci fosse alcun elemento all'interno del gruppo, la velocità del gruppo sarebbe la massima consentita, caratteristica bizzarra che potrebbe anche essere utile in un contesto fantasy.

Testiamo questo schema con questo piccolo client di prova:

Group thieves = new Group("Thieves");
Man tom = new Man("Tom", 3);
Man bill = new Man("Bill", 5);
Man will = new Man("Will", 4);
thieves.add(tom);
thieves.add(bill);
thieves.add(will);

Group warriors = new Group("Warriors");
Warrior mark = new Warrior("Mark", 4);
Warrior bud = new Warrior("Bud", 2);
Warrior jepp = new Warrior("Jepp", 6);
warriors.add(mark);
warriors.add(bud);
warriors.add(jepp);

Group group = new Group("Looking for something");
group.add(thieves);
group.add(warriors);

group.list();
System.out.println("\nThe company velocity: " + group.getVelocity());

Gran parte del codice rappresenta il setup della struttura, le ultime due righe sono il testing vero e proprio. Viene fatto un printout di tutti gli elementi inseriti nel nostro albero e poi viene stampata la velocità della compagnia.

Nessun commento:

Posta un commento