Factory Object-Oriented in JavaScript: creare istanze con classi e prototipi

by Anastasia P.
SHARE
Factory Object-Oriented in JavaScript: creare istanze con classi e prototipi
© Guida-HTML5.it

Introduzione

Quando un’applicazione JavaScript deve creare molti oggetti simili, usare direttamente new in ogni punto del codice può rendere il progetto difficile da mantenere. La logica di costruzione rischia di essere duplicata, i dati possono essere validati in modo incoerente e diventa complicato modificare il tipo concreto dell’oggetto prodotto.

Un approccio pratico consiste nell’utilizzare il Factory Pattern, cioè un componente responsabile della creazione delle istanze. In JavaScript questo pattern può essere realizzato con le classi, i costruttori e i prototipi. L’obiettivo non è nascondere completamente le classi, ma centralizzare la loro istanziazione e restituire oggetti con un’interfaccia comune.

Nel tutorial costruiremo un piccolo sistema per gestire notifiche di tipo email e SMS. Entrambi gli oggetti avranno metodi condivisi, ma saranno creati dalla stessa factory in base al tipo richiesto.

Codice completo

// Classe base per rappresentare una notifica
class Notification {
  constructor(recipient, message) {
    if (!recipient || !message) {
      throw new Error("Destinatario e messaggio sono obbligatori");
    }

    this.recipient = recipient;
    this.message = message;
    this.createdAt = new Date();
  }

  // Metodo condiviso tramite Notification.prototype
  getSummary() {
    return `Notifica per ${this.recipient}: ${this.message}`;
  }

  // Metodo pensato per essere specializzato dalle sottoclassi
  send() {
    throw new Error("Il metodo send() deve essere implementato");
  }
}

// Sottotipo concreto per le notifiche email
class EmailNotification extends Notification {
  constructor(recipient, message, subject) {
    super(recipient, message);

    if (!subject) {
      throw new Error("L´oggetto dell´email è obbligatorio");
    }

    this.subject = subject;
  }

  send() {
    return `Email inviata a ${this.recipient} con oggetto "${this.subject}"`;
  }
}

// Sottotipo concreto per le notifiche SMS
class SmsNotification extends Notification {
  constructor(recipient, message, senderId) {
    super(recipient, message);

    this.senderId = senderId || "APP";
  }

  send() {
    return `SMS inviato a ${this.recipient} dal mittente ${this.senderId}`;
  }
}

// Factory centralizzata
class NotificationFactory {
  static create(type, options) {
    const normalizedType = type.toLowerCase();

    switch (normalizedType) {
      case "email":
        return new EmailNotification(
          options.recipient,
          options.message,
          options.subject
        );

      case "sms":
        return new SmsNotification(
          options.recipient,
          options.message,
          options.senderId
        );

      default:
        throw new Error(`Tipo di notifica non supportato: ${type}`);
    }
  }
}

// Il codice client non deve conoscere i costruttori concreti
const email = NotificationFactory.create("email", {
  recipient: "[email protected]",
  message: "Il tuo ordine è stato spedito",
  subject: "Aggiornamento ordine"
});

const sms = NotificationFactory.create("sms", {
  recipient: "+391234567890",
  message: "Il tuo codice è 4821",
  senderId: "SHOP"
});

console.log(email.getSummary());
console.log(email.send());

console.log(sms.getSummary());
console.log(sms.send());

// Tutti gli oggetti condividono i metodi della classe base
console.log(email instanceof Notification); // true
console.log(sms instanceof Notification);   // true

Spiegazione

La classe base

Notification rappresenta le proprietà comuni a tutte le notifiche: il destinatario, il messaggio e la data di creazione. Il metodo getSummary() viene definito nel prototipo della classe. Di conseguenza, non viene ricreato separatamente per ogni istanza: tutte le notifiche possono riutilizzare lo stesso metodo.

Il metodo send(), invece, genera un errore. Questa scelta rende esplicito che la classe base non conosce il meccanismo concreto di invio. Un’email e un SMS hanno comportamenti differenti, quindi le sottoclassi devono fornire la propria implementazione.

Le classi specializzate

EmailNotification estende Notification usando extends. La chiamata super() esegue il costruttore della classe padre e inizializza i dati comuni. Dopo questa fase, il costruttore aggiunge la proprietà specifica subject.

SmsNotification segue lo stesso modello, ma utilizza un identificativo del mittente. Entrambe le classi ridefiniscono il metodo send(). Questo permette al codice client di invocare lo stesso metodo senza dover conoscere i dettagli dell’oggetto concreto.

Il ruolo della factory

NotificationFactory espone il metodo statico create(). Questo metodo riceve un tipo e un oggetto di configurazione, normalizza il tipo e seleziona il costruttore corretto.

Il vantaggio principale è la separazione delle responsabilità. Il codice che usa le notifiche non deve importare o conoscere direttamente EmailNotification e SmsNotification. Se in futuro venisse aggiunto un tipo push, sarebbe sufficiente creare una nuova classe e aggiornare la factory.

Prototipi e memoria

Anche se la sintassi moderna utilizza le classi, JavaScript continua a basarsi sui prototipi. I metodi dichiarati nel corpo di una classe vengono inseriti nel suo oggetto prototype. Possiamo verificarlo direttamente:

console.log(
  Object.hasOwn(EmailNotification.prototype, "send")
); // true

console.log(
  Object.hasOwn(email, "getSummary")
); // false: il metodo è ereditato

La proprietà email appartiene all’istanza, mentre getSummary() viene cercato lungo la catena dei prototipi. Questo meccanismo evita la duplicazione dei metodi in memoria.

Best practice

  • Centralizza la creazione: usa una factory quando la costruzione degli oggetti richiede validazione, scelta del tipo o più parametri.
  • Valida i dati nei costruttori: un’istanza non valida dovrebbe essere rifiutata subito, evitando errori più difficili da individuare in seguito.
  • Mantieni un’interfaccia comune: se tutti i tipi espongono send(), il codice client può lavorare con qualsiasi notifica.
  • Evita factory troppo grandi: quando i casi diventano numerosi, valuta una mappa di creatori o più factory specializzate.
  • Non abusare dell’ereditarietà: usa una classe base solo per comportamenti realmente condivisi. Altrimenti potresti creare gerarchie rigide.
  • Proteggi i tipi sconosciuti: genera un errore chiaro quando la factory riceve un valore non supportato.
  • Usa oggetti di configurazione: passare un oggetto come options è più leggibile e facilita l’aggiunta di nuove proprietà.

Riepilogo

Il Factory Pattern permette di incapsulare la logica con cui vengono create le istanze. In JavaScript può essere combinato efficacemente con classi, costruttori, ereditarietà e prototipi.

Nell’esempio, il client chiede una notifica alla factory senza occuparsi del costruttore concreto. Le classi specializzate implementano il proprio comportamento, mentre i metodi comuni restano condivisi attraverso i prototipi. Questo approccio riduce la duplicazione, migliora la leggibilità e rende più semplice estendere l’applicazione.

Approfondisci con risorse ufficiali

SHARE