Closures e funzioni come cittadini di prima classe: creare un sistema di plugin per pipeline di elaborazione

by Anastasia P.
SHARE
Closures e funzioni come cittadini di prima classe: creare un sistema di plugin per pipeline di elaborazione
© Guida-HTML5.it

Introduzione

In JavaScript, le funzioni sono cittadini di prima classe: puoi assegnarle a variabili, passarle come argomenti, restituirle da altre funzioni e salvarle in strutture dati. Le closures, invece, permettono a una funzione di “ricordare” l’ambiente in cui è stata creata, anche quando quell’ambiente esterno ha già terminato l’esecuzione.

Un uso molto pratico di questi due concetti è la creazione di un sistema di plugin per pipeline di elaborazione. Immagina di dover trasformare dati in più passaggi: pulizia, normalizzazione, arricchimento, logging, controllo qualità. Invece di scrivere una funzione enorme e rigida, puoi comporre piccoli plugin riutilizzabili, ognuno configurabile tramite closure.

Questo approccio è utile in scenari reali come:

  • elaborazione di dati provenienti da API;
  • normalizzazione di input utente;
  • workflow di import/export;
  • trasformazioni testabili e modulari;
  • estensione di comportamenti senza modificare il core.

In questo tutorial vedremo come costruire una pipeline semplice ma solida, sfruttando funzioni di prima classe e closures per ottenere plugin flessibili e riutilizzabili.

Codice completo

// Pipeline di elaborazione basata su plugin
// Ogni plugin è una funzione che riceve un valore e restituisce un nuovo valore.
// Alcuni plugin sono "factory" che usano closure per conservare configurazione interna.

function createPipeline(...plugins) {
  return function runPipeline(input) {
    return plugins.reduce((value, plugin) => {
      return plugin(value);
    }, input);
  };
}

// Plugin 1: trim su stringhe
function trimText(value) {
  if (typeof value !== "string") return value;
  return value.trim();
}

// Plugin 2: converte in minuscolo
function toLowerCase(value) {
  if (typeof value !== "string") return value;
  return value.toLowerCase();
}

// Plugin 3: sostituisce spazi multipli con uno singolo
function collapseSpaces(value) {
  if (typeof value !== "string") return value;
  return value.replace(/s+/g, " ");
}

// Plugin 4: aggiunge un prefisso configurabile
function createPrefixPlugin(prefix) {
  // closure: il plugin "ricorda" il prefisso passato alla factory
  return function addPrefix(value) {
    if (typeof value !== "string") return value;
    return `${prefix}${value}`;
  };
}

// Plugin 5: tronca una stringa a una lunghezza massima
function createTruncatePlugin(maxLength) {
  // closure: mantiene maxLength disponibile anche dopo la chiamata alla factory
  return function truncate(value) {
    if (typeof value !== "string") return value;

    if (value.length <= maxLength) {
      return value;
    }

    return value.slice(0, maxLength) + "...";
  };
}

// Plugin 6: aggiunge metadati a un oggetto
function createTagPlugin(tagName) {
  // closure: il tagName resta accessibile nel plugin restituito
  return function addTag(value) {
    if (typeof value !== "object" || value === null) return value;

    return {
      ...value,
      tag: tagName
    };
  };
}

// Plugin 7: logger configurabile
function createLoggerPlugin(label) {
  // closure: label resta disponibile nel logger
  return function logValue(value) {
    console.log(`[${label}]`, value);
    return value;
  };
}

// Esempio 1: pipeline per testo
const textPipeline = createPipeline(
  trimText,
  collapseSpaces,
  toLowerCase,
  createPrefixPlugin("utente: "),
  createTruncatePlugin(20)
);

const rawText = "   Ciao     MONDO     DI   JAVASCRIPT   ";
const processedText = textPipeline(rawText);

console.log("Risultato testo:", processedText);
// Output possibile: "utente: ciao mond..."

console.log("----");

// Esempio 2: pipeline per oggetti
const objectPipeline = createPipeline(
  createLoggerPlugin("input"),
  function ensureName(value) {
    if (typeof value !== "object" || value === null) return value;

    return {
      ...value,
      name: typeof value.name === "string" ? value.name.trim() : "Sconosciuto"
    };
  },
  createTagPlugin("imported"),
  createLoggerPlugin("output")
);

const rawUser = {
  name: "   Mario Rossi   ",
  age: 34
};

const processedUser = objectPipeline(rawUser);

console.log("Risultato oggetto:", processedUser);
// Output:
// [input] { name: ´   Mario Rossi   ´, age: 34 }
// [output] { name: ´Mario Rossi´, age: 34, tag: ´imported´ }

Spiegazione

Il cuore dell’esempio è la funzione createPipeline. Riceve un numero variabile di plugin tramite rest parameters (...plugins) e restituisce una nuova funzione, runPipeline, che applica ogni plugin in sequenza usando reduce.

1. Funzioni come valori

In JavaScript una funzione può essere trattata come un dato qualsiasi. Questo ci permette di:

  • passarla a createPipeline;
  • salvarla in un array;
  • restituirla da una factory;
  • comporla con altre funzioni.

Nel nostro caso, i plugin sono funzioni normali oppure funzioni generate da factory. La pipeline non ha bisogno di sapere come ogni plugin funzioni internamente: le basta chiamarlo con un valore e ricevere il risultato.

2. Closures per configurare i plugin

Le factory createPrefixPlugin, createTruncatePlugin, createTagPlugin e createLoggerPlugin restituiscono funzioni che usano variabili definite nel loro scope esterno. Questa è una closure.

Per esempio, createPrefixPlugin("utente: ") restituisce una funzione che non riceve più il prefisso come argomento, ma continua ad averlo disponibile grazie alla closure. Questo rende il plugin:

  • più semplice da usare;
  • più riutilizzabile;
  • facile da comporre con altri plugin.

3. Composizione e ordine dei plugin

L’ordine nella pipeline conta molto. Nel caso del testo:

  • prima rimuoviamo gli spazi iniziali e finali con trimText;
  • poi comprimiamo gli spazi multipli;
  • poi convertiamo in minuscolo;
  • infine aggiungiamo un prefisso e tronchiamo il risultato.

Se invertissi alcuni passaggi, il risultato potrebbe cambiare. Ad esempio, troncare prima di aggiungere il prefisso produrrebbe un output diverso. Questo è uno dei motivi per cui le pipeline sono utili: il comportamento è esplicito e leggibile.

4. Gestione di tipi diversi

Noterai che ogni plugin controlla il tipo del valore in ingresso. È una buona pratica importante: una pipeline reale può ricevere stringhe, oggetti, numeri o valori nulli. Se un plugin non è adatto a quel tipo, può semplicemente restituire il valore invariato oppure gestire il caso in modo specifico.

Nel plugin createTagPlugin, per esempio, trasformiamo solo gli oggetti. In questo modo evitiamo errori e rendiamo il plugin più robusto.

5. Logging come plugin trasversale

Il plugin logger è interessante perché mostra un caso molto comune: il comportamento trasversale. Invece di spargere console.log in tutto il codice, puoi creare un plugin dedicato e inserirlo solo dove serve. La closure conserva l’etichetta del log, così puoi distinguere facilmente input e output.

Best practice

  • Preferisci plugin piccoli e focalizzati: ogni funzione dovrebbe fare una sola cosa bene.
  • Rendi esplicito l’ordine: in una pipeline, la sequenza di esecuzione è parte del contratto.
  • Valida sempre gli input: non dare per scontato il tipo del valore ricevuto.
  • Usa closure per configurazione, non per nascondere complessità: se un plugin dipende da molti parametri, valuta di passare un oggetto di configurazione chiaro.
  • Mantieni i plugin puri quando possibile: una funzione pura è più facile da testare e prevedere.
  • Evita effetti collaterali inutili: il logger è un effetto collaterale accettabile, ma la trasformazione dei dati dovrebbe idealmente restare deterministica.
  • Nomina bene le factory: il nome deve far capire che restituiscono una funzione configurata, non un valore finale.

Riepilogo

Le funzioni come cittadini di prima classe e le closures sono due pilastri fondamentali di JavaScript. In questo tutorial abbiamo visto come usarli per costruire un sistema di plugin per pipeline di elaborazione, un pattern utile e molto pratico per gestire trasformazioni modulari dei dati.

Il vantaggio principale è la componibilità: puoi creare funzioni semplici, configurarle con factory basate su closure e combinarle in sequenza per ottenere comportamenti complessi senza scrivere codice monolitico.

Questo approccio migliora:

  • leggibilità;
  • riusabilità;
  • testabilità;
  • manutenibilità;
  • separazione delle responsabilità.

Se stai lavorando su trasformazioni di dati, validazioni di flussi, normalizzazione di input o estensione di comportamenti, una pipeline di plugin è una soluzione elegante e molto vicina allo stile funzionale di JavaScript.

Approfondisci con risorse ufficiali

  • MDN - Closures: documentazione chiara sul funzionamento delle closure in JavaScript.
  • MDN - Funzioni: panoramica completa su dichiarazioni, espressioni e funzioni arrow.
  • MDN - Array.prototype.reduce(): utile per capire come costruire pipeline e trasformazioni a catena.
  • MDN - Rest parameters: approfondimento su ...args e raccolta di argomenti variabili.
  • ECMAScript Language Specification: riferimento ufficiale per chi vuole andare oltre la pratica quotidiana.

SHARE