Nel vasto panorama dello sviluppo del software, esistono strumenti e concetti fondamentali che ogni programmatore dovrebbe conoscere per scrivere codice efficiente e manutenibile. Tra questi, i Design Pattern Strutturali emergono come pilastri essenziali, offrendo un approccio sistematico alla composizione di classi e oggetti per creare strutture flessibili e scalabili.
I Design Pattern Strutturali rappresentano una serie di soluzioni progettuali ricorrenti per problemi comuni nell’organizzazione delle relazioni tra le entità software; essi si concentrano sul modo in cui classi e oggetti vengono composti per formare strutture più ampie, garantendo una separazione efficace delle responsabilità e promuovendo il riutilizzo del codice.
Modelli strutturali di classe e oggetti
Iniziamo esplorando i modelli strutturali di classe, che si basano sull’ereditarietà per comporre interfacce o implementazioni, un esempio lampante di questo approccio è il pattern Adapter perché fornisce un’astrazione uniforme tra interfacce diverse, garantendo così una maggiore interoperabilità tra i componenti del software.
Passando ai modelli strutturali di oggetti, ci immergiamo nella composizione dinamica di oggetti per realizzare nuove funzionalità; per esempio, il pattern Composite, permette di costruire gerarchie di classi composte da oggetti primitivi e compositi, consentendo la creazione di strutture complesse in modo flessibile; il pattern Proxy, invece, agisce come un surrogato per un altro oggetto, permettendo di controllare l’accesso a risorse sensibili o di gestire la comunicazione con oggetti remoti.
Esploriamo i design pattern strutturali in Java
Ora che abbiamo una comprensione di base dei Design Pattern Strutturali, è il momento di esplorare come vengono implementati in Java.
Saranno proposti alcuni dei design pattern strutturali con del codice esemplificativo per poter spiegare l’utilità, l’efficacia e le funzionalità di questi strumenti davvero utili.
Pattern Adapter
Lo scopo è convertire un’interfaccia di una classe in un’altra interfaccia che i clients si aspettano, l’adapter consente di far lavorare insieme classi che altrimenti non potrebbero farlo a causa di interfacce incompatibili; di fatto funziona come un adattatore schuko per adattare un cavo che ha una spina fatta in un certo modo ad una presa che è stata pensata per altre spine.
Nell’esempio seguente, supponiamo di avere due librerie di terze parti con interfacce diverse ma funzionalità simili: utilizzando il pattern Adapter, possiamo creare un adattatore che renda uniforme l’interfaccia di entrambe le librerie, semplificando l’integrazione nel nostro codice.
public interface LibreriaUno {
void metodoUno();
}
public interface LibreriaDue {
void metodoDue();
}
public class Adattatore implements LibreriaUno {
private LibreriaDue libreriaDue;
public Adattatore(LibreriaDue libreriaDue) {
this.libreriaDue = libreriaDue;
}
public void metodoUno() {
libreriaDue.metodoDue();
}
}
//Esempio di utilizzo
public class Main {
public static void main(String[] args) {
// Creazione di un'istanza della libreria uno
LibreriaUno libreriaUno = new LibreriaUnoImpl();
// Utilizzo diretto della libreria uno
libreriaUno.metodoUno();
// Creazione di un'istanza della libreria due
LibreriaDue libreriaDue = new LibreriaDueImpl();
// Utilizzo diretto della libreria due
libreriaDue.metodoDue();
// Utilizzo dell'adattatore per integrare la libreria due con l'interfaccia della libreria uno
Adattatore adattatore = new Adattatore(libreriaDue);
adattatore.metodoUno(); // Utilizzo dell'adattatore come se fosse la libreria uno
}
}
Code language: PHP (php)
In questo esempio, abbiamo un metodo main
che:
- Crea un’istanza della
LibreriaUno
e chiama il metodometodoUno()
direttamente su di essa. - Crea un’istanza della
LibreriaDue
e chiama il metodometodoDue()
direttamente su di essa. - Utilizza l’adattatore per integrare la
LibreriaDue
con l’interfaccia dellaLibreriaUno
e chiama il metodometodoUno()
sull’adattatore, che di fatto esegue il metodo corrispondente dellaLibreriaDue
.
In questo modo, l’adattatore permette di utilizzare la funzionalità della LibreriaDue
come se fosse la LibreriaUno
, facilitando l’integrazione di librerie con interfacce diverse nel tuo codice.
Pattern Composite
Il composite è un modello di progettazione strutturale che consente di comporre gli oggetti in strutture ad albero e di lavorare con queste strutture come se fossero oggetti singoli: Composite consente ai client di trattare in modo uniforme i singoli oggetti e le composizioni di oggetti.
Per l’esempio qui di seguito: immaginiamo di dover gestire una struttura gerarchica di documenti, dove sia possibile combinare documenti singoli in documenti composti, utilizzando il pattern Composite, possiamo rappresentare questa struttura in modo uniforme, semplificando l’operazione di navigazione e manipolazione.
public interface Documento {
void stampa();
}
public class DocumentoSingolo implements Documento {
public void stampa() {
System.out.println("Stampa documento singolo");
}
}
public class DocumentoComposto implements Documento {
private List<Documento> documenti = new ArrayList<>();
public void aggiungiDocumento(Documento documento) {
documenti.add(documento);
}
public void stampa() {
for (Documento documento : documenti) {
documento.stampa();
}
}
}
//Esempio di utilizzo:
public class Main {
public static void main(String[] args) {
// Creazione di documenti singoli
Documento documento1 = new DocumentoSingolo();
Documento documento2 = new DocumentoSingolo();
Documento documento3 = new DocumentoSingolo();
// Creazione di un documento composto
Documento documentoComposto = new DocumentoComposto();
documentoComposto.aggiungiDocumento(documento1);
documentoComposto.aggiungiDocumento(documento2);
documentoComposto.aggiungiDocumento(documento3);
// Chiamata al metodo stampa() sul documento composto
documentoComposto.stampa();
}
}
Code language: PHP (php)
In questo esempio, abbiamo un metodo main
che:
- Crea tre documenti singoli (
documento1
,documento2
,documento3
) utilizzando la classeDocumentoSingolo
. - Crea un documento composto (
documentoComposto
) utilizzando la classeDocumentoComposto
. - Aggiunge i tre documenti singoli al documento composto utilizzando il metodo
aggiungiDocumento()
. - Chiama il metodo
stampa()
sul documento composto, che a sua volta chiama il metodostampa()
su ciascun documento singolo aggiunto, stampando così il contenuto di tutti i documenti nella struttura gerarchica.
In questo modo, il pattern Composite consente di trattare in modo uniforme sia i documenti singoli che i documenti composti, semplificando la gestione di strutture gerarchiche complesse nel tuo codice.
Pattern Decorator
Il pattern decorator fornisce un’alternativa alternativa flessibile all’ereditarietà per estendere le funzionalità di una classe; il decorator è un modello di progettazione strutturale che consente di associare nuovi comportamenti agli oggetti, collocandoli all’interno di speciali oggetti wrapper che contengono i comportamenti.
Immagina di avere un’interfaccia Componente
che rappresenta un oggetto base e vuoi aggiungere diverse decorazioni a questo oggetto, come un bordo, un’ombra o altre caratteristiche, utilizzando il pattern Decorator, puoi creare una serie di classi Decoratore che estendono l’interfaccia Componente
e aggiungono le funzionalità desiderate senza dover modificare direttamente l’oggetto base.
// Interfaccia Componente
public interface Componente {
void disegna();
}
// Implementazione di base del componente
public class ComponenteBase implements Componente {
@Override
public void disegna() {
System.out.println("Disegno del componente base");
}
}
// Decoratore astratto
public abstract class Decoratore implements Componente {
protected Componente componente;
public Decoratore(Componente componente) {
this.componente = componente;
}
@Override
public void disegna() {
componente.disegna();
}
}
// Decoratore che aggiunge un bordo al componente
public class DecoratoreBordo extends Decoratore {
public DecoratoreBordo(Componente componente) {
super(componente);
}
@Override
public void disegna() {
super.disegna();
aggiungiBordo();
}
private void aggiungiBordo() {
System.out.println("Aggiunta di un bordo al componente");
}
}
// Decoratore che aggiunge un'ombra al componente
public class DecoratoreOmbra extends Decoratore {
public DecoratoreOmbra(Componente componente) {
super(componente);
}
@Override
public void disegna() {
super.disegna();
aggiungiOmbra();
}
private void aggiungiOmbra() {
System.out.println("Aggiunta di un'ombra al componente");
}
}
// Esempio sull'utilizzo
public class Main {
public static void main(String[] args) {
// Creazione di un componente base
Componente componenteBase = new ComponenteBase();
// Aggiunta di un bordo al componente base utilizzando il decoratore
Componente componenteConBordo = new DecoratoreBordo(componenteBase);
componenteConBordo.disegna();
// Aggiunta di un'ombra al componente base utilizzando il decoratore
Componente componenteConOmbra = new DecoratoreOmbra(componenteBase);
componenteConOmbra.disegna();
// Aggiunta di entrambi bordo e ombra al componente base
Componente componenteConBordoEOmbra = new DecoratoreOmbra(new DecoratoreBordo(componenteBase));
componenteConBordoEOmbra.disegna();
}
}
Code language: PHP (php)
In questo esempio:
Componente
rappresenta l’interfaccia di base per tutti i componenti.ComponenteBase
è l’implementazione di base diComponente
.Decoratore
è una classe astratta che implementaComponente
e contiene un riferimento a un altro oggettoComponente
. Questo è il fulcro del pattern Decorator.DecoratoreBordo
eDecoratoreOmbra
sono classi concrete che estendonoDecoratore
e aggiungono funzionalità specifiche.- Nel metodo
main
, creiamo un componente base e aggiungiamo dinamicamente bordi, ombre o entrambi utilizzando i decoratori.
In questo modo, il pattern Decorator ci permette di estendere le funzionalità di un oggetto in modo flessibile e modulare, mantenendo il codice pulito e facilmente manutenibile.
Semplicità ed importanza dei design pattern strutturali
In questo articolo abbiamo esplorato i Design Pattern Strutturali, scoprendo soluzioni eleganti per problemi comuni nello sviluppo del software, la loro semplicità d’uso e flessibilità li rende strumenti preziosi per scrivere codice leggibile e manutenibile: comprendere e utilizzare questi pattern è fondamentale per migliorare la qualità del proprio codice e diventare sviluppatori migliori.
Unisciti a noi nella prossima parte della nostra saga, dove esamineremo i Design Pattern Comportamentali, completando così il quadro delle best practice nello sviluppo del software.