Salta al contenuto principale

01 - Singleton e Factory Pattern: Creazione Oggetti Controllata

Pattern creazionali fondamentali: Singleton per istanza unica, Factory Method e Abstract Factory per creazione flessibile di oggetti con esempi TypeScript.

Cosa ne pensi di questo articolo?

01 - Singleton e Factory Pattern: Creazione Oggetti Controllata
Design PatternsFoto:Ruben BoekelooPexels

In Breve

Pattern creazionali fondamentali: Singleton per istanza unica, Factory Method e Abstract Factory per creazione flessibile di oggetti con esempi TypeScript.

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.

TypeScript - Singleton Base
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:

Lazy Singleton
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.

TypeScript - Factory Method
// 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:

Simple Factory
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.

Abstract Factory - UI Theme
// 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

Factory + Singleton Combinati
// 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.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti