Articolo originale: https://www.freecodecamp.org/news/the-basic-design-patterns-all-developers-need-to-know/

Cos'è un Design Pattern?

I design pattern (schemi progettuali) sono soluzioni a livello di progettazione per problemi ricorrenti con i quali gli ingegneri informatici hanno spesso a che fare. Non è codice. Lo ripeto, ❌ CODICE. È come una descrizione di come affrontare questi problemi e progettare una soluzione.

L'uso di questi pattern è considerato una buona pratica, visto che la progettazione della soluzione è stata abbondantemente provata e testata, risultando in una maggiore leggibilità del codice finale. I design pattern sono creati piuttosto spesso per e usati da linguaggi di programmazione orientati agli oggetti (OOP), come Java, linguaggio nel quale sarà scritta la maggior parte degli esempi da qui in avanti.

Tipi di design pattern

Ci sono circa 26 design pattern attualmente noti (dubito fortemente che li esaminerò tutti...).

Questi 26 possono essere classificati in 3 tipi:

1. Creazionali: progettati per creare un'istanza di una classe. Possono essere sia pattern di creazione di classi che di creazione di oggetti.

2. Strutturali: progettati in relazione alla struttura e la composizione di una classe. L'obiettivo principale della maggior parte di questi schemi è aumentare la funzionalità della classe (o classi) coinvolta, senza cambiare più di tanto la sua composizione.

3. Comportamentali: progettati a seconda di come una classe comunica con le altre.

In questo post, esamineremo un design pattern di base per ciascun tipo di classificazione.

Tipo 1: Creazionali - Il Design Pattern Singleton

Il  design pattern Singleton ha l'obiettivo di creare una sola istanza di una classe e di fornire un solo punto di accesso globale a quell'oggetto. Un esempio comunemente citato di tale classe in Java è Calendar, dove non puoi creare un'istanza di quella classe. Utilizza anche il suo metodo getInstance() per ottenere l'oggetto da usare.

Una classe che usa il design pattern Singleton includerà:

singleton-class-diagram
Diagramma della classe Singleton
  1. Una variabile privata statica, associata all'istanza della classe.
  2. Un costruttore privato, in modo che non possa essere istanziato da nessun'altra parte.
  3. Un metodo statico pubblico, per ritornare la singola istanza della classe.

Ci sono molte diverse implementazioni di questo pattern. Oggi esamineremo le seguenti:

1. Creazione immediata (Eager)

2. Creazione differita (Lazy)

3. Creazione Thread-safe

Creazione Immediata (Eager)

public class EagerSingleton {
	// crea un'istanza della classe.
	private static EagerSingleton instance = new EagerSingleton();

	// costruttore privato, in modo che non possa essere istanziato al di fuori di questa classe.
	private EagerSingleton() {  }

	// ottiene l'unica istanza dell'oggetto creato.
	public static EagerSingleton getInstance() {
		return instance;
	}
}

La creazione dell'istanza avviene durante il caricamento della classe, visto che l'istanza della classe viene associata alla variabile al di fuori di qualsiasi metodo. Questo comporta un pesante svantaggio se questa classe non viene mai usata dall'applicazione client. Il piano di emergenza, se questa classe non viene usata, è usare il metodo di creazione differita (Lazy).

Creazione Differita (Lazy)

Non c'è molta differenza rispetto all'implementazione qui sopra. Le differenze principali sono che la variabile statica viene dichiarata inizialmente null e viene istanziata solo all'interno del metodo getInstance() se, e solo se, la variabile di istanza risulta null al tempo della verifica.

public class LazySingleton {
	// inizializza l'istanza come  null.
	private static LazySingleton instance = null;

	// costruttore privato, in modo che non possa essere istanziato al di fuori di questa classe.
	private LazySingleton() {  }

	// verifica se l'istanza è null, e in questo caso, crea l'oggetto.
	public static LazySingleton getInstance() {
		if (instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
}

Questo risolve un problema, ma ne esiste ancora un altro. Cosa succede se due client diversi accedono alla classe Singleton allo stesso tempo, al millisecondo?. Entrambi verificheranno se l'istanza è null allo stesso tempo e ciò sarà vero, pertanto verranno create due istanze della classe, una per richiesta fatta da ciascun client. Per risolvere questo problema si deve implementare un modo di istanziare di tipo Thread Safe.

Creazione Thread-safe - La sicurezza è la Chiave

In Java, la parola chiave synchronized viene usata su metodi o oggetti per implementare la thread-safety, in modo che solo un thread abbia accesso a una particolare risorsa a un dato momento. La creazione dell'istanza della classe viene posta all'interno di un blocco synchronized in modo che un solo client alla volta possa accedere a quel metodo in un dato momento.

public class ThreadSafeSingleton {
	// inizializza l'istanza come null.
	private static ThreadSafeSingleton instance = null;

	// costruttore privato, in modo che non possa essere istanziato al di fuori di questa classe.
	private ThreadSafeSingleton() {  }

	// verifica se l'istanza è null, all'interno di un blocco synchronized block. Se vero, crea l'oggetto
	public static ThreadSafeSingleton getInstance() {
		synchronized (ThreadSafeSingleton.class) {
			if (instance == null) {
				instance = new ThreadSafeSingleton();
			}
		}
		return instance;
	}
}

Il sovraccarico per il metodo synchronized è elevato e riduce le prestazioni dell'intera operazione.

Per esempio, se la variabile di istanza è già stata istanziata, ogni volta che un qualunque client accede al metodo getInstance(), viene eseguito il metodo synchronized e le prestazioni calano. Questo succede in quanto si deve verificare se il valore della variabile  instance è null. Se la verifica è vera, si esce dal metodo.

Per ridurre questo sovraccarico, viene usato un sistema chiamato double locking (doppio blocco). La verifica viene effettuata anche prima della chiamata del metodo  synchronized, e solo se il valore è null il metodo synchronized viene eseguito.

// Il doppio blocco viene utilizzato per ridurre il sovraccarico del metodo sincronizzato
public static ThreadSafeSingleton getInstanceDoubleLocking() {
	if (instance == null) {
		synchronized (ThreadSafeSingleton.class) {
			if (instance == null) {
				instance = new ThreadSafeSingleton();
			}
		}
	}
	return instance;
}

Passiamo alla prossima classificazione.

Tipo 2: Strutturale - Il Design Pattern Decorator

Ti prospetterò un piccolo scenario per offrirti un miglior contesto per capire perché e dove dovresti usare il pattern Decorator.

Diciamo che possiedi un coffee shop, e come qualunque altro che parta con un'attività, inizi con solo due tipi di semplice caffè: la miscela della casa e il tostato scuro. Nel software che gestisce il tuo listino prezzi, c'è solo una classe per diverse miscele di caffè, che eredita dalla classe astratta Beverage (bevanda). I clienti iniziano ad affluire e consumano il tuo meraviglioso (anche se un po' amaro?) caffè. Poi c'è chi ha appena iniziato a bere caffè che vorrebbe aggiungere, Dio ce ne scampi, zucchero o latte. Che obbrobrio!!

Ora devi creare anche queste due aggiunte, inserendole sfortunatamente sia nel menù che nel listino prezzi. All'inizio il tuo tecnico informatico ha creato una sottoclasse per entrambi i tipi di caffè, una che include lo zucchero, e un'altra che include il latte. Poi visto che il cliente ha sempre ragione, uno pronuncia questa temuta frase:

Posso avere un caffè con latte, e zucchero per favore?”

???

Ecco il tuo listino prezzi che ti ride in faccia nuovamente. Va bene, torniamo alla lavagna….

Il tuo programmatore aggiunge caffè con latte e zucchero come sottoclasse di ciascuna classe genitore che rappresenta un tipo di caffè. Il resto del mese va via che è un piacere, con i clienti in coda per consumare il tuo caffè, stai addirittura facendo soldi. ??

Un momento, c'è dell'altro!

Ancora una volta il mondo cospira ai tuoi danni. Un concorrente apre dall'altra parte della strada, non solo con 4 tipi di caffè, ma anche con più di 10 aggiunte!

Adotti anche tu queste aggiunte, per vendere un caffè migliore, e ti sei appena ricordato che hai dimenticato di aggiornare il tuo maledetto listino prezzi. Non è certamente possibile creare un numero infinito di sottoclassi per qualunque combinazione di tutte le integrazioni, includendo anche le nuove miscele di caffè. Senza contare la dimensione finale del sistema.

È ora di investire in un sistema di listino prezzi appropriato. Trovi dei nuovi programmatori, che in effetti sanno cosa stanno facendo e ti dicono:

Sarebbe stato molto più facile e ridotto se si fosse usato il pattern decorator.”

Cosa diamine è?

Il design pattern decorator rientra nella categoria strutturale, che ha a che fare con l'effettiva struttura di una classe, sia per ereditarietà che per composizione o entrambe. Lo scopo è di modificare la funzionalità di un oggetto in fase di esecuzione. Questo è uno di molti altri design pattern che utilizzano classi astratte e interfacce con la composizione per ottenere il risultato desiderato.

Diamo una possibilità alla matematica (hai i brividi?) per portare tutto questo in prospettiva.

Ipotizziamo 4 miscele di caffè e 1o aggiunte. Se continuiamo con la tattica della generazione di sottoclassi per ciascuna diversa combinazione di tutte le aggiunte per ciascun tipo di miscela avremo:

(10–1)² = 9² = 81 sottoclassi

Ne togliamo una dalle 1o in quanto non puoi combinare un'aggiunta con un'altra dello stesso tipo, zucchero con zucchero sembra stupido. E questo è per solo una miscela. Moltiplica  81 per 4 e ottieni un mostruoso totale di 324 sottoclassi diverse! Pensa a tutto il codice che si dovrebbe scrivere…

Tuttavia con il pattern decorator ti serviranno solo 16 classi in questo scenario. Scommettiamo?

decorator-class-diagram
Diagramma delle classi per il pattern decorator
decorator-coffee-class-diagram
Diagramma delle classi in base allo scenario coffee shop

Se mappiamo il nostro scenario in base all'ipotesi sopra citata, abbiamo 4 classi per le quattro miscele di caffè, 10 per ciascuna aggiunta (AddOn), 1 per il componente astratto e una ulteriore per il decorator astratto. Vedi! 16! Ora dammi $100 (sto scherzando, ma se me li dai non li rifiuto... giusto per dire).

Come puoi vedere qui sopra, così come le classi concrete che rappresentano le miscele di caffè  (HouseBlend e DarkRoast ad esempio) sono sottoclassi della classe astratta Beverage, anche la classe astratta AddOn (aggiunta) eredita da essa i suoi metodi. Le aggiunte (le classi Milk e Sugar nel diagramma), che sono sottoclassi di AddOn, a loro volta erediteranno qualsiasi nuovo metodo creato per aggiungere funzionalità all'oggetto base quando necessario.

Scriviamo un po' di codice per vedere il pattern in azione.

Per prima cosa creiamo la classe astratta Beverage, dalla quale erediteranno tutte le classi che rappresentano le diverse miscele di caffè:

public abstract class Beverage {
	private String description;
    
	public Beverage(String description) {
		super();
		this.description = description;
	}
    
	public String getDescription() {
		return description;
	}
    
	public abstract double cost();
}

Poi aggiungiamo le due classi concrete per le miscele di caffè.

public class HouseBlend extends Beverage {
	public HouseBlend() {
		super(“House blend”);
	}

	@Override
	public double cost() {
		return 250;
	}
}

public class DarkRoast extends Beverage {
	public DarkRoast() {
		super(“Dark roast”);
	}

	@Override
	public double cost() {
		return 300;
	}
}

Anche la classe astratta AddOn eredita da Beverage (maggiori dettagli qui sotto).

public abstract class AddOn extends Beverage {
	protected Beverage beverage;

	public AddOn(String description, Beverage bev) {
		super(description);
		this.beverage = bev;
	}

	public abstract String getDescription();
}

Ora le implementazioni concrete di questa classe astratta:

public class Sugar extends AddOn {
	public Sugar(Beverage bev) {
		super(“Sugar”, bev);
	}

	@Override
	public String getDescription() {
		return beverage.getDescription() + “ with Mocha”;
	}

	@Override
	public double cost() {
		return beverage.cost() + 50;
	}
}

public class Milk extends AddOn {
	public Milk(Beverage bev) {
		super(“Milk”, bev);
	}

	@Override
	public String getDescription() {
		return beverage.getDescription() + “ with Milk”;
	}

	@Override  public double cost() {
		return beverage.cost() + 100;
	}
}

Come puoi vedere qui sopra, possiamo passare qualsiasi sottoclasse di Beverage a qualsiasi sottoclasse di AddOn e ottenere il costo aggiuntivo assieme alla descrizione aggiornata. Poi, visto che la classe AddOn è essenzialmente di tipo Beverage, possiamo passare un AddOn in un altro AddOn. In questo modo possiamo usare un qualsiasi numero di aggiunte (AddOn) per una specifica miscela di caffè.

Ora scriviamo del codice per testare tutto quanto.

public class CoffeeShop {
	public static void main(String[] args) {
		HouseBlend houseblend = new HouseBlend();
		System.out.println(houseblend.getDescription() + “: “ + houseblend.cost());

		Milk milkAddOn = new Milk(houseblend);
		System.out.println(milkAddOn.getDescription() + “: “ + milkAddOn.cost());

		Sugar sugarAddOn = new Sugar(milkAddOn);
		System.out.println(sugarAddOn.getDescription() + “: “ + sugarAddOn.cost());
	}
}

Il risultato finale è:

decorator-final
P.S. prezzi in Rupie dello SriLanka

Funziona! Siamo stati in grado di aggiungere più di una aggiunta a una miscela di caffè e aggiornare con successo il suo costo e descrizione finale, senza dove creare un numero infinito di sottoclassi per ciascuna combinazione di aggiunte per tutte le miscele di caffè.

Infine, passiamo all'ultima categoria.

Tipo 3: Comportamentale - Il Design Pattern Command

Un design pattern di tipo comportamentale si focalizza su come le classi e gli oggetti comunicano tra loro. Lo scopo principale del pattern command è di inculcare un alto grado di loose coupling tra le parti coinvolte (leggi: classi).

Uhhhh… Cos'è?

L'accoppiamento (coupling) è il modo nel quale due (o più) classi interagiscono tra loro. Lo scenario ideale sarebbe quello nel quale le classi che interagiscono non debbano dipendere molto strettamente le une dalle altre. Questo è il loose coupling. Quindi una definizione migliore di loose coupling sarebbe, classi che sono interconnesse, facendo il minor uso possibile le une delle altre.

La necessità di questo pattern nacque quando occorreva inviare delle richieste senza conoscere esplicitamente cosa stavi chiedendo e chi fosse il destinatario.

In questo modello, la classe chiamante è disaccoppiata dalla classe che esegue realmente un'azione. La classe chiamata deve avere solo un metodo (in genere chiamato execute()) che esegua i comandi necessari, quando il client lo richiede.

Facciamo un semplice esempio reale, ordinare un pasto in un bel ristorante. Seguendo il flusso delle azioni, tu ordini un piatto (dai un comando) al cameriere (il chiamante), il quale a sua volta lo passa allo chef (il destinatario), in modo che tu possa avere quanto ordinato. Potrebbe sembrare semplice... ma così così da programmare.

chain-of-command-be-like-pop-snoke-im-going-to-27790631

L'idea è piuttosto semplice, ma il codice non tanto.

command-class-diagram
Diagramma di classi del Design Pattern Command

Dal punto di vista tecnico, il flusso delle operazioni è: tu, il cliente, crei un comando concreto, che implementa l'interfaccia Command, chiedendo al destinatario (lo chef) di completare un'azione, e invii il comando al chiamante (il cameriere). Il chiamante è la persona che sa quando impartire questo comando. Lo chef  è l'unico che sa cosa fare quando riceve uno specifico comando/ordine. Quindi quando il metodo execute del chiamante viene eseguito, a sua volta fa si che il metodo execute degli oggetti derivati dalla classe Command venga eseguito sul destinatario, pertanto completando le azioni necessarie.

Quello che serve implementare è;

  1. Una interfaccia Command
  2. Una classe  Order - ordine - che implementi l'interfaccia Command
  3. Una classe  Waiter - cameriere - (chiamante)
  4. Una classe Chef (destinatario)

Quindi il codice è il seguente:

Chef, il destinatario

public class Chef {
	public void cookPasta() {
		System.out.println(“Chef is cooking Chicken Alfredo…”);
	}

	public void bakeCake() {
		System.out.println(“Chef is baking Chocolate Fudge Cake…”);
	}
}

Command, l'interfaccia

public interface Command {
	public abstract void execute();
}

Order, il comando concreto

public class Order implements Command {
	private Chef chef;
	private String food;

	public Order(Chef chef, String food) {
		this.chef = chef;
		this.food = food;
	}

	@Override
	public void execute() {
		if (this.food.equals(“Pasta”)) {
			this.chef.cookPasta();
		} else {
			this.chef.bakeCake();
		}
	}
}

Waiter, il chiamante

public class Waiter {
	private Order order;

	public Waiter(Order ord) {
		this.order = ord;
	}

	public void execute() {
		this.order.execute();
	}
}

Tu, il cliente

public class Client {
	public static void main(String[] args) {
		Chef chef = new Chef();
        
		Order order = new Order(chef, “Pasta”);
		Waiter waiter = new Waiter(order);
		waiter.execute();

		order = new Order(chef, “Cake”);
		waiter = new Waiter(order);
		waiter.execute();
	}
}

Come puoi vedere qui sopra, il cliente (Client) dà un ordine (Order) e imposta il destinatario (Chef) L'ordine è inviato al cameriere (Waiter), che saprà quando eseguirlo, vale a dire quando dare allo chef l'ordine di preparare il piatto. Quando viene eseguito il chiamante, viene invocato il metodo execute dell'ordine (Order) sul destinatario (vale a dire che Chef riceve il comando di cucinare la pasta - cookPasta() - oppure di infornare un dolce - bakeCake()).

command-pattern-run-example-1
Risultato dell'esempio del pattern Command

In questo post abbiamo esaminato:

  1. Cosa è un design pattern,
  2. I diversi tipi di design pattern e perché sono differenti
  3. Un design pattern comune o di base per ciascun tipo

Spero che questo sia stato utile.

Puoi trovare il repository del codice per questo post qui.