Design Patterns: Guida Completa ai Principali Pattern di Progettazione
I design patterns sono soluzioni standardizzate a problemi comuni che emergono durante la progettazione del software. Utilizzando questi pattern, gli sviluppatori possono creare applicazioni che siano scalabili, manutenibili e facili da comprendere. In questa guida, esploreremo alcuni dei design patterns più comuni, suddivisi in categorie, con esempi pratici che mostrano come implementarli nel codice.
1. Categorie di Design Patterns
I design patterns sono generalmente suddivisi in tre categorie principali:
- Creational Patterns: Si concentrano sul processo di creazione degli oggetti.
- Structural Patterns: Si occupano della composizione delle classi e degli oggetti.
- Behavioral Patterns: Riguardano l’interazione e la responsabilità tra gli oggetti.
2. Creational Patterns
2.1. Singleton
Il Singleton Pattern garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale a quell’istanza.
Esempio in JavaScript
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
}
getInstance() {
return this;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
2.2. Factory Method
Il Factory Method Pattern fornisce un’interfaccia per creare oggetti in una superclasse, ma consente alle sottoclassi di alterare il tipo di oggetti che verranno creati.
Esempio in JavaScript
class Product {
constructor(name) {
this.name = name;
}
display() {
console.log(`Product: ${this.name}`);
}
}
class ProductFactory {
static createProduct(type) {
if (type === "A") {
return new Product("Product A");
} else if (type === "B") {
return new Product("Product B");
}
}
}
const productA = ProductFactory.createProduct("A");
productA.display(); // Product: Product A
2.3. Builder
Il Builder Pattern separa la costruzione di un complesso oggetto dalla sua rappresentazione, in modo che lo stesso processo di costruzione possa creare diverse rappresentazioni.
Esempio in JavaScript
class Car {
constructor() {
this.engine = "";
this.wheels = 0;
}
setEngine(engine) {
this.engine = engine;
return this;
}
setWheels(wheels) {
this.wheels = wheels;
return this;
}
build() {
return `Car with ${this.engine} engine and ${this.wheels} wheels.`;
}
}
const car = new Car().setEngine("V8").setWheels(4).build();
console.log(car); // Car with V8 engine and 4 wheels.
3. Structural Patterns
3.1. Adapter
L’Adapter Pattern permette a interfacce incompatibili di lavorare insieme. È spesso utilizzato per rendere compatibili due interfacce che altrimenti non potrebbero interagire.
Esempio in JavaScript
class OldAPI {
request() {
return "Old API response";
}
}
class NewAPI {
specificRequest() {
return "New API response";
}
}
class Adapter {
constructor(newAPI) {
this.newAPI = newAPI;
}
request() {
return this.newAPI.specificRequest();
}
}
const oldAPI = new OldAPI();
console.log(oldAPI.request()); // Old API response
const newAPI = new NewAPI();
const adaptedAPI = new Adapter(newAPI);
console.log(adaptedAPI.request()); // New API response
3.2. Decorator
Il Decorator Pattern permette di aggiungere comportamenti o responsabilità agli oggetti in modo dinamico. I decoratori forniscono una soluzione flessibile rispetto all’ereditarietà per estendere le funzionalità.
Esempio in JavaScript
class Coffee {
cost() {
return 5;
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1.5;
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 0.5;
}
}
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 7
3.3. Composite
Il Composite Pattern permette di trattare oggetti individuali e composizioni di oggetti in modo uniforme. È particolarmente utile per rappresentare strutture gerarchiche come alberi.
Esempio in JavaScript
class Leaf {
constructor(name) {
this.name = name;
}
display() {
console.log(this.name);
}
}
class Composite {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
}
display() {
console.log(this.name);
this.children.forEach((child) => child.display());
}
}
const tree = new Composite("root");
const branch1 = new Composite("branch1");
const leaf1 = new Leaf("leaf1");
tree.add(branch1);
branch1.add(leaf1);
tree.display();
// Output:
// root
// branch1
// leaf1
4. Behavioral Patterns
4.1. Observer
L’Observer Pattern è un pattern in cui un oggetto (l’osservato) notifica a un insieme di osservatori quando avviene un cambiamento di stato. Questo è utile per implementare una logica di eventi.
Esempio in JavaScript
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notifyObservers(message) {
this.observers.forEach((observer) => observer.update(message));
}
}
class Observer {
update(message) {
console.log(`Observer received: ${message}`);
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers("Event occurred");
// Observer received: Event occurred
// Observer received: Event occurred
4.2. Strategy
Il Strategy Pattern permette di definire una famiglia di algoritmi, incapsularli, e renderli intercambiabili. Questo pattern permette di selezionare l’algoritmo da utilizzare in fase di esecuzione.
Esempio in JavaScript
class Context {
constructor(strategy) {
this.strategy = strategy;
}
executeStrategy(num1, num2) {
return this.strategy.doOperation(num1, num2);
}
}
class AddOperation {
doOperation(num1, num2) {
return num1 + num2;
}
}
class MultiplyOperation {
doOperation(num1, num2) {
return num1 * num2;
}
}
const context = new Context(new AddOperation());
console.log(context.executeStrategy(10, 5)); // 15
context.strategy = new MultiplyOperation();
console.log(context.executeStrategy(10, 5)); // 50
4.3. Command
Il Command Pattern incapsula una richiesta come un oggetto, consentendo di parametrizzare i client con richieste diverse, fare logging, o supportare operazioni annullabili.
Esempio in JavaScript
class Light {
turnOn() {
console.log("Light is on");
}
turnOff() {
console.log("Light is off");
}
}
class LightOnCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.turnOn();
}
}
class LightOffCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.turnOff();
}
}
class RemoteControl {
constructor() {
this.commands = [];
}
setCommand(command) {
this.commands.push(command);
}
pressButton() {
this.commands.forEach((command) => command.execute());
}
}
const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);
const remote = new RemoteControl();
remote.setCommand(lightOn);
remote.setCommand(lightOff);
remote.pressButton();
// Output:
// Light is on
// Light is off
Conclusione
I design patterns sono strumenti essenziali per la progettazione di software robusto, scalabile e manutenibile. Comprendere e applicare correttamente questi pattern può migliorare notevol
mente la qualità del tuo codice, rendendo il software più flessibile e facile da estendere. Sia che tu stia lavorando su un piccolo progetto o su una grande applicazione enterprise, i design patterns forniscono soluzioni collaudate per i problemi comuni di progettazione software.