Singleton e Factory Pattern: Creazione Oggetti Controllata
I design pattern creazionali gestiscono la creazione di oggetti in modo flessibile e riutilizzabile. Singleton garantisce una sola istanza di una classe, mentre Factory delega la creazione di oggetti a metodi specializzati. Entrambi sono fondamentali per un'architettura software solida.
🎯 Cosa Imparerai
- Singleton pattern: istanza unica globale
- Factory Method: delega creazione a sottoclassi
- Abstract Factory: famiglie di oggetti correlati
- Quando usare ciascun pattern
Singleton Pattern
Il Singleton garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale ad essa. Utile per risorse condivise come connessioni database, logger, cache.
class DatabaseConnection {{ '{' }}
private static instance: DatabaseConnection;
private connected: boolean = false;
// Costruttore privato: impedisce `new DatabaseConnection()`
private constructor() {{ '{' }}
console.log("DatabaseConnection created");
{{ '}' }}
// Metodo statico per ottenere l'istanza unica
public static getInstance(): DatabaseConnection {{ '{' }}
if (!DatabaseConnection.instance) {{ '{' }}
DatabaseConnection.instance = new DatabaseConnection();
{{ '}' }}
return DatabaseConnection.instance;
{{ '}' }}
public connect(): void {{ '{' }}
if (!this.connected) {{ '{' }}
console.log("Connecting to database...");
this.connected = true;
{{ '}' }}
{{ '}' }}
public query(sql: string): void {{ '{' }}
console.log(`Executing: ${{ '{' }}sql{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Utilizzo
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - stessa istanza!
db1.connect();
db2.query("SELECT * FROM users"); // Usa la stessa connessione
Singleton con Lazy Initialization
L'istanza viene creata solo quando serve, non all'avvio dell'applicazione:
class Logger {{ '{' }}
private static instance: Logger;
private logs: string[] = [];
private constructor() {{ '{' }}{{ '}' }}
public static getInstance(): Logger {{ '{' }}
// Creato solo alla prima chiamata
if (!Logger.instance) {{ '{' }}
console.log("Logger instance created");
Logger.instance = new Logger();
{{ '}' }}
return Logger.instance;
{{ '}' }}
public log(message: string): void {{ '{' }}
const timestamp = new Date().toISOString();
this.logs.push(`[${{ '{' }}timestamp{{ '}' }}] ${{ '{' }}message{{ '}' }}`);
console.log(this.logs[this.logs.length - 1]);
{{ '}' }}
public getLogs(): string[] {{ '{' }}
return [...this.logs];
{{ '}' }}
{{ '}' }}
// Prima chiamata: crea istanza
const logger = Logger.getInstance(); // "Logger instance created"
logger.log("Application started");
// Chiamate successive: riutilizza istanza
const logger2 = Logger.getInstance(); // Nessun log di creazione
logger2.log("User logged in");
console.log(logger.getLogs());
// ["[2024-12-26T...] Application started", "[2024-12-26T...] User logged in"]
Singleton: Pro e Contro
✅ Vantaggi:
- Garantisce istanza unica
- Accesso globale
- Lazy initialization
- Controllo su risorse condivise
❌ Svantaggi:
- Stato globale (difficile da testare)
- Viola Single Responsibility
- Accoppiamento forte
- Problemi con multi-threading
⚠️ Alternative al Singleton
In molti casi, Dependency Injection è preferibile al Singleton. Framework come Angular usano DI per fornire istanze singole senza i problemi del Singleton pattern.
Factory Method Pattern
Il Factory Method definisce un'interfaccia per creare oggetti, ma delega alle sottoclassi la decisione su quale classe concreta istanziare.
// Interfaccia prodotto
interface Vehicle {{ '{' }}
drive(): void;
getType(): string;
{{ '}' }}
// Prodotti concreti
class Car implements Vehicle {{ '{' }}
drive(): void {{ '{' }}
console.log("Driving a car 🚗");
{{ '}' }}
getType(): string {{ '{' }}
return "Car";
{{ '}' }}
{{ '}' }}
class Motorcycle implements Vehicle {{ '{' }}
drive(): void {{ '{' }}
console.log("Riding a motorcycle 🏍️");
{{ '}' }}
getType(): string {{ '{' }}
return "Motorcycle";
{{ '}' }}
{{ '}' }}
class Truck implements Vehicle {{ '{' }}
drive(): void {{ '{' }}
console.log("Driving a truck 🚚");
{{ '}' }}
getType(): string {{ '{' }}
return "Truck";
{{ '}' }}
{{ '}' }}
// Factory astratta
abstract class VehicleFactory {{ '{' }}
// Factory method (da implementare nelle sottoclassi)
abstract createVehicle(): Vehicle;
// Metodo che usa il factory method
public deliverVehicle(): void {{ '{' }}
const vehicle = this.createVehicle();
console.log(`Delivering a ${{ '{' }}vehicle.getType(){{ '}' }}`);
vehicle.drive();
{{ '}' }}
{{ '}' }}
// Factory concrete
class CarFactory extends VehicleFactory {{ '{' }}
createVehicle(): Vehicle {{ '{' }}
return new Car();
{{ '}' }}
{{ '}' }}
class MotorcycleFactory extends VehicleFactory {{ '{' }}
createVehicle(): Vehicle {{ '{' }}
return new Motorcycle();
{{ '}' }}
{{ '}' }}
class TruckFactory extends VehicleFactory {{ '{' }}
createVehicle(): Vehicle {{ '{' }}
return new Truck();
{{ '}' }}
{{ '}' }}
// Utilizzo
function clientCode(factory: VehicleFactory) {{ '{' }}
factory.deliverVehicle();
{{ '}' }}
clientCode(new CarFactory());
// "Delivering a Car"
// "Driving a car 🚗"
clientCode(new MotorcycleFactory());
// "Delivering a Motorcycle"
// "Riding a motorcycle 🏍️"
Simple Factory (Factory Pattern)
Una variante più semplice: un singolo metodo factory decide quale classe istanziare:
class VehicleSimpleFactory {{ '{' }}
public static createVehicle(type: string): Vehicle {{ '{' }}
switch (type) {{ '{' }}
case 'car':
return new Car();
case 'motorcycle':
return new Motorcycle();
case 'truck':
return new Truck();
default:
throw new Error(`Unknown vehicle type: ${{ '{' }}type{{ '}' }}`);
{{ '}' }}
{{ '}' }}
{{ '}' }}
// Utilizzo
const car = VehicleSimpleFactory.createVehicle('car');
car.drive(); // "Driving a car 🚗"
const truck = VehicleSimpleFactory.createVehicle('truck');
truck.drive(); // "Driving a truck 🚚"
Abstract Factory Pattern
Abstract Factory crea famiglie di oggetti correlati senza specificare le classi concrete. Utile per temi UI, database multi-vendor, ecc.
// Interfacce prodotti
interface Button {{ '{' }}
render(): void;
{{ '}' }}
interface Checkbox {{ '{' }}
render(): void;
{{ '}' }}
// Prodotti concreti - Tema Dark
class DarkButton implements Button {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering dark button");
{{ '}' }}
{{ '}' }}
class DarkCheckbox implements Checkbox {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering dark checkbox");
{{ '}' }}
{{ '}' }}
// Prodotti concreti - Tema Light
class LightButton implements Button {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering light button");
{{ '}' }}
{{ '}' }}
class LightCheckbox implements Checkbox {{ '{' }}
render(): void {{ '{' }}
console.log("Rendering light checkbox");
{{ '}' }}
{{ '}' }}
// Abstract Factory
interface UIFactory {{ '{' }}
createButton(): Button;
createCheckbox(): Checkbox;
{{ '}' }}
// Factory concrete
class DarkThemeFactory implements UIFactory {{ '{' }}
createButton(): Button {{ '{' }}
return new DarkButton();
{{ '}' }}
createCheckbox(): Checkbox {{ '{' }}
return new DarkCheckbox();
{{ '}' }}
{{ '}' }}
class LightThemeFactory implements UIFactory {{ '{' }}
createButton(): Button {{ '{' }}
return new LightButton();
{{ '}' }}
createCheckbox(): Checkbox {{ '{' }}
return new LightCheckbox();
{{ '}' }}
{{ '}' }}
// Client code
class Application {{ '{' }}
private button: Button;
private checkbox: Checkbox;
constructor(factory: UIFactory) {{ '{' }}
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
{{ '}' }}
render(): void {{ '{' }}
this.button.render();
this.checkbox.render();
{{ '}' }}
{{ '}' }}
// Utilizzo - Switch tema senza modificare Application
const darkApp = new Application(new DarkThemeFactory());
darkApp.render();
// "Rendering dark button"
// "Rendering dark checkbox"
const lightApp = new Application(new LightThemeFactory());
lightApp.render();
// "Rendering light button"
// "Rendering light checkbox"
Quando Usare Ogni Pattern
✅ Usa Singleton quando:
- Serve una sola istanza globale (logger, cache, config)
- Controllo su risorsa condivisa (connessione DB)
- Meglio: Usa Dependency Injection invece se possibile
✅ Usa Factory Method quando:
- La classe non sa in anticipo quale oggetto creare
- Vuoi delegare creazione alle sottoclassi
- Serve estensibilità (nuovi tipi senza modificare codice esistente)
✅ Usa Abstract Factory quando:
- Serve creare famiglie di oggetti correlati
- Oggetti devono essere usati insieme (UI theme, cross-platform UI)
- Vuoi nascondere dettagli implementativi
Esempio Pratico: Sistema di Notifiche
// Singleton Logger
class NotificationLogger {{ '{' }}
private static instance: NotificationLogger;
private constructor() {{ '{' }}{{ '}' }}
static getInstance(): NotificationLogger {{ '{' }}
if (!this.instance) this.instance = new NotificationLogger();
return this.instance;
{{ '}' }}
log(msg: string): void {{ '{' }}
console.log(`[NOTIFICATION] ${{ '{' }}msg{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// Factory per Notifiche
interface Notification {{ '{' }}
send(message: string): void;
{{ '}' }}
class EmailNotification implements Notification {{ '{' }}
send(message: string): void {{ '{' }}
NotificationLogger.getInstance().log(`Email sent: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class SMSNotification implements Notification {{ '{' }}
send(message: string): void {{ '{' }}
NotificationLogger.getInstance().log(`SMS sent: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class NotificationFactory {{ '{' }}
static create(type: 'email' | 'sms'): Notification {{ '{' }}
switch (type) {{ '{' }}
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
{{ '}' }}
{{ '}' }}
{{ '}' }}
// Utilizzo
const emailNotif = NotificationFactory.create('email');
emailNotif.send("Welcome!"); // [NOTIFICATION] Email sent: Welcome!
const smsNotif = NotificationFactory.create('sms');
smsNotif.send("Code: 1234"); // [NOTIFICATION] SMS sent: Code: 1234
Conclusione
Singleton e Factory sono pattern creazionali fondamentali. Singleton garantisce un'unica istanza globale, mentre Factory delega la creazione per flessibilità. Abstract Factory estende il concetto per famiglie di oggetti. Usali con giudizio: Dependency Injection è spesso preferibile al Singleton, e Simple Factory può bastare invece di Factory Method complesso.
🎯 Punti Chiave
- Singleton = una sola istanza globale (costruttore privato)
- Factory Method = delega creazione a sottoclassi
- Simple Factory = metodo statico decide cosa creare
- Abstract Factory = famiglie di oggetti correlati
- Preferisci Dependency Injection al Singleton quando possibile
Domande frequenti
Quali sono i vantaggi del Singleton Pattern?
Il Singleton garantisce che una classe abbia solo un'istanza e fornisce un punto di accesso globale ad essa, utile per risorse condivise come connessioni database, logger, cache.
In quali casi il Dependency Injection è preferibile al Singleton?
Il Dependency Injection è spesso preferibile al Singleton quando si desidera evitare i problemi associati al pattern di design Singleton.
Qual è la differenza tra Factory Method Pattern e Simple Factory (Factory Pattern)?
Il Factory Method Pattern definisce un'interfaccia per creare oggetti, ma delega alle sottoclassi la decisione su quale classe concreta istanziare, mentre il Simple Factory è una variante più semplice che utilizza un singolo metodo factory per decidere quale classe istanziare.
Qual è l'utilizzo principale dell'Abstract Factory Pattern?
L'Abstract Factory crea famiglie di oggetti correlati senza specificare le classi concrete, utile per temi UI, database multi-vendor, ecc.

Commenti
Caricamento commenti...
Accedi per commentare