Composite pattern: modello di soluzione per la rappresentazione delle gerarchie parte/tutto
Le strutture di dati dinamiche come le strutture ad albero di un file management o di un’interfaccia di programma richiedono una struttura gerarchica chiara e il più possibile inequivocabile. L’implementazione di tali strutture spesso non è per niente semplice. Ad esempio, è importante assicurarsi che il tipo di oggetto non debba essere richiamato ogni volta prima dell’effettiva elaborazione dei dati, poiché un tale scenario non sarebbe né efficiente né performante. Soprattutto nel caso in cui molti oggetti primitivi incontrino oggetti compositi, si consiglia l’utilizzo del cosiddetto Composite design pattern (in italiano “modello di progettazione composito”). L’approccio di progettazione del software consente ai client di trattare gli oggetti individuali e compositi in modo uniforme, rendendo le differenze ininfluenti per il client.
Che cos’è il Composite pattern?
Il Composite design pattern, abbreviato in Composite pattern, è uno dei 23 design pattern per lo sviluppo software che sono stati rilasciati nel 1994 da Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides (noti anche come “Gang of Four”). Come il Facade pattern e il Decorator pattern, il Composite design pattern si annovera tra i modelli strutturali la cui funzione fondamentale è quella di riunire oggetti e classi in strutture di maggiori dimensioni.
Quali problemi risolve il Composite design pattern?
Il fine principale del Composite design pattern è, come in tutti i modelli della GoF, quello di affrontare al meglio i problemi ricorrenti di progettazione nello sviluppo orientato agli oggetti. Il risultato che si desidera ottenere è un software il più possibile flessibile che si contraddistingue per oggetti facili da implementare, testabili, sostituibili e riutilizzabili. A questo scopo, il Composite pattern descrive una possibile strada per trattare singoli oggetti e oggetti compositi alla stessa maniera. In questo modo si ottiene una struttura a oggetti che è semplice da comprendere e consente un accesso di massima efficienza al client. Inoltre viene minimizzata anche la suscettibilità del codice agli errori.
Composite design pattern: rappresentazione grafica (UML)
Per implementare le efficienti gerarchie parte-tutto già menzionate, il modello Composite prevede l’implementazione di un’interfaccia dei componenti unitaria per semplici oggetti-parte, chiamati anche oggetti Leaf (inglese per “foglia”, ci si riferisce infatti alla metafora dell’albero dove le foglie sono una parte del tutto) e degli oggetti Composite (appunto compositi). I singoli oggetti Leaf integrano direttamente questa interfaccia, gli oggetti Composite trasmettono automaticamente le concrete richieste del client all’interfaccia ai loro componenti subordinati. Per il client non ha importanza con quale tipo di oggetto abbia a che fare (se parte o tutto), perché si tratta semplicemente di indirizzarsi all’interfaccia.
Il seguente diagramma di classe nel linguaggio di modellazione UML aiuta a chiarire i rapporti e le gerarchie in un software basato sul Composite pattern.
Quali sono i vantaggi e gli svantaggi del modello Composite?
Il Composite pattern è una costante nello sviluppo del software. In particolare i progetti con strutture fortemente annidate possono approfittare dell’approccio pratico per l’organizzazione di oggetti: non importa che si tratti di un oggetto primitivo o composito, con dipendenze semplici o complesse, perché la profondità e larghezza degli annidamenti è fondamentalmente irrilevante nel Composite design pattern. Il client può tranquillamente ignorare la differenza tra i tipi di oggetto e in questo modo non sono necessarie funzioni separate per l’accesso. In questo modo si ha il vantaggio che il codice del client rimane snello.
Un altro punto a favore del modello Composite è la flessibilità e l’estensibilità che il pattern conferisce a un software: l’interfaccia dei componenti universale consente di collegare nuovi oggetti Leaf e Composite senza variazioni del codice, sia dal lato client che nel caso in cui si tratti di strutture oggetti preesistenti.
Nonostante il Composite pattern e la sua interfaccia unitaria offrano numerosi vantaggi, questa soluzione non è esente da punti deboli: infatti, in particolare l’interfaccia può causare non poche difficoltà agli sviluppatori. Già l’implementazione pone grandi sfide per i responsabili, perché bisogna per esempio scegliere quali operazioni devono essere definite nell’interfaccia e quali nelle classi Composite. Inoltre eventuali successive modifiche delle proprietà del Composite (ad esempio le limitazioni, quali elementi figlio sono ammessi) si rivelano complicati e difficili da realizzare.
Vantaggi | Svantaggi |
---|---|
Ha tutto ciò che serve per rappresentare strutture oggetti fortemente annidate | L’implementazione dell’interfaccia dei componenti è molto complicata |
Codice di programmazione snello e facilmente comprensibile | Le successive modifiche delle proprietà del Composite sono complicate e difficili da implementare |
Buona estensibilità |
Scenari di applicazione per il Composite pattern
L’utilizzo dei modelli Composite è particolarmente vantaggioso quando occorre effettuare operazioni su strutture di dati dinamici, le cui gerarchie sono di larghezza e/o profondità complesse. Si parla in questi casi anche di struttura ad albero binaria, che è interessante per i più svariati scenari software e viene utilizzata frequentemente. Alcuni tipici esempi sono:
Sistemi di file: i sistemi di file sono tra i componenti più importanti del software dei dispositivi. Con il Composite pattern si possono mappare in modo ottimale: i singoli file come oggetti Leaf e le cartelle, che a propria volta possono contenere file o ulteriori cartelle, come oggetti Composite.
Menu dei software: anche i menu dei programmi rappresentano un caso d’utilizzo tipico per una struttura ad albero binaria basata sul Composite design pattern. Nella barra del menu si trovano una o più voci di base (oggetti Composite) come “file”. Essi consentono l’accesso a varie voci del menu che sono direttamente cliccabili (Leaf) o contengono ulteriori menu (Composite).
Interfacce utente grafiche (GUI): le strutture ad albero e il modello Composite possono giocare un ruolo importante anche nella progettazione di interfacce utente grafiche. Accanto ai semplici elementi Leaf come pulsanti, campi di testo o caselle di controllo, ci sono i contenitori Composite, come frame o panel, che assicurano una struttura chiara e una maggiore comprensibilità.
Esempio di codice: Composite pattern
Java è sicuramente il linguaggio di programmazione dove l’approccio del Composite pattern è più saldamente consolidato. Il modello è tra l’altro anche la base degli Abstract Window Toolkits (AWT), un’API pratica e popolare con circa 50 classi Java pronte all’uso per lo sviluppo di interfacce Java multipiattaforma. Nel seguente esempio di codice, che si basa sull’articolo “Composite Design Pattern in Java” su baeldung.com, abbiamo quindi scelto questo amato linguaggio di programmazione.
Nell’esempio pratico viene mostrata la struttura gerarchica dei reparti (departments) in un’azienda (company). Innanzitutto viene definita perciò l’interfaccia del componente “department”:
public interface Department {
void printDepartmentName();
}
In seguito vengono definite le due semplici classi Leaf “FinancialDepartment” (per il reparto finanze) e “SalesDepartment” (per il reparto vendite). Entrambe implementano il metodo printDepartmentName() dall’interfaccia del componente, ma non contengono altri oggetti department.
public class FinancialDepartment implements Department {
private Integer id;
private String name;
public void printDepartmentName() {
System.out.println(getClass().getSimpleName());
}
// standard constructor, getters, setters
}
public class SalesDepartment implements Department {
private Integer id;
private String name;
public void printDepartmentName() {
System.out.println(getClass().getSimpleName());
}
// standard constructor, getters, setters
}
Una classe Composite corrispondente alla gerarchia viene infine definita come “HeadDepartment”. Questa classe è composta da più componenti Department e contiene metodi per aggiungere ulteriori elementi (addDepartment) o rimuovere elementi esistenti (removeDepartment), in aggiunta al metodo printDepartmentName():
public class HeadDepartment implements Department {
private Integer id;
private String name;
private List<department> childDepartments;</department>
public HeadDepartment(Integer id, String name) {
this.id = id;
this.name = name;
this.childDepartments = new ArrayList<>();
}
public void printDepartmentName() {
childDepartments.forEach(Department::printDepartmentName);
}
public void addDepartment(Department department) {
childDepartments.add(department);
}
public void removeDepartment(Department department) {
childDepartments.remove(department);
}
}