Paradigmi di Programmazione

17 febbraio 2026
14 min di lettura

Introduzione

I paradigmi di programmazione definiscono approcci diversi per organizzare e strutturare il codice. Non sono funzionalità del linguaggio o API del browser, ma modi di pensare e organizzare la logica dell’applicazione. Comprendere i diversi paradigmi permette di scegliere lo stile più adatto per ogni progetto e di comprendere meglio il codice esistente.

In questo capitolo si approfondiscono:

  • Cos’è un paradigma di programmazione: definizione e importanza
  • Programmazione procedurale: codice sequenziale step-by-step
  • Programmazione orientata agli oggetti: organizzazione in classi e oggetti
  • Programmazione funzionale: organizzazione in funzioni pure
  • Confronto tra paradigmi: vantaggi e svantaggi di ciascuno
  • Quando usare quale paradigma: criteri per la scelta

Cos’è un Paradigma di Programmazione

Definizione

Un paradigma di programmazione è un modo di scrivere e organizzare il codice. Non riguarda la sintassi del linguaggio o funzionalità specifiche, ma piuttosto:

  • Come si struttura il codice
  • Come si ragiona sul programma
  • Come si organizzano dati e logica
  • Come si modella la soluzione al problema

I Tre Paradigmi Principali

In JavaScript, i tre paradigmi principali sono:

1. Programmazione Orientata agli Oggetti (OOP)

  • Organizza dati e logica in oggetti o classi
  • I dati sono memorizzati in proprietà degli oggetti
  • La logica è memorizzata in metodi degli oggetti
  • Ogni entità dell’applicazione è rappresentata come classe e poi come oggetto
  • Traduce concetti del mondo reale in entità software

2. Programmazione Procedurale

  • Scrittura di una serie sequenziale di passi da eseguire
  • Esecuzione top-to-bottom del codice
  • Elenco di passi che il motore JavaScript deve eseguire
  • Non organizza il codice in gruppi logici o entità
  • Non utilizza oggetti per strutturare il codice

3. Programmazione Funzionale

  • Organizza il codice in funzioni pure con compiti ben definiti
  • I dati necessari vengono passati come parametri
  • Le funzioni spesso restituiscono qualcosa di nuovo
  • I dati vengono passati tra funzioni tramite parametri
  • Le funzioni sono il modo principale di organizzare il codice

Differenze Chiave

Orientamento agli Oggetti:

  • Pensiero in termini di entità del mondo reale
  • Esempio: in un’applicazione di e-commerce, si pensa a Prodotti, Carrello, Utente
  • Codice organizzato in classi che rappresentano questi concetti

Procedurale:

  • Pensiero in termini di passi da eseguire
  • Esempio: “prima ottieni l’input, poi valida, poi crea l’oggetto”
  • Codice organizzato come sequenza di istruzioni

Funzionale:

  • Pensiero in termini di trasformazioni di dati
  • Esempio: “una funzione che valida, una che crea utenti, una che saluta”
  • Codice organizzato in funzioni che trasformano input in output

Esecuzione del Codice

Indipendentemente dal paradigma utilizzato, il codice viene sempre eseguito top-to-bottom. La differenza non è nell’esecuzione, ma nell’organizzazione e nella struttura del codice prima dell’esecuzione.


Programmazione Procedurale

Caratteristiche

La programmazione procedurale è probabilmente lo stile con cui si inizia quando si impara JavaScript. È lo stile più diretto e immediato: si pensa ai passi da eseguire e si scrivono in sequenza.

Caratteristiche principali:

  • Codice sequenziale step-by-step
  • Esecuzione lineare dall’alto verso il basso
  • Utilizzo di funzioni quando necessario, ma non come organizzazione principale
  • Utilizzo di oggetti quando necessario, ma non come struttura principale
  • Focus sulla sequenza di operazioni da eseguire

Esempio Pratico: Form di Registrazione

Consideriamo un’applicazione con un form di registrazione che contiene due input (username e password) e un pulsante di submit.

Obiettivi:

  1. Ascoltare il submit del form
  2. Ottenere i valori inseriti dall’utente
  3. Validare i valori (non vuoti, password almeno 6 caratteri)
  4. Mostrare errori se la validazione fallisce
  5. Creare un oggetto utente se la validazione ha successo

Implementazione procedurale:

// Ottenere accesso agli elementi del DOM
const form = document.getElementById('user-input')
const usernameInput = document.getElementById('username')
const passwordInput = document.getElementById('password')
// Definire la funzione handler per il submit
function signupHandler(event) {
// Prevenire il comportamento di default del form
event.preventDefault()
// Ottenere i valori inseriti dall'utente
const enteredUsername = usernameInput.value
const enteredPassword = passwordInput.value
// Validare l'username
if (enteredUsername.trim().length === 0) {
alert('Invalid input: username must not be empty')
return // Interrompere l'esecuzione se non valido
}
// Validare la password
if (enteredPassword.trim().length <= 5) {
alert('Invalid input: password must be six characters or longer')
return // Interrompere l'esecuzione se non valido
}
// Creare l'oggetto utente se la validazione ha successo
const user = {
username: enteredUsername,
password: enteredPassword
}
// Log dell'utente creato
console.log(user)
console.log('Hi I am ' + user.username)
}
// Aggiungere l'event listener al form
form.addEventListener('submit', signupHandler)

Analisi del Codice Procedurale

Organizzazione:

  • Il codice è organizzato come sequenza di passi
  • Prima si ottengono i riferimenti agli elementi DOM
  • Poi si definisce la funzione handler
  • Infine si registra l’event listener

Flusso di esecuzione:

  1. Ottenere riferimenti agli elementi
  2. Definire la funzione di gestione
  3. Registrare l’event listener
  4. Quando il form viene inviato:
    • Prevenire il comportamento di default
    • Ottenere i valori
    • Validare passo dopo passo
    • Creare l’oggetto se valido

Vantaggi:

  • Facile da comprendere per principianti
  • Diretto e immediato
  • Nessuna astrazione aggiuntiva
  • Perfetto per script semplici

Limitazioni:

  • Può diventare difficile da gestire per applicazioni complesse
  • Difficile riutilizzare codice senza copiare e incollare
  • Nessuna organizzazione logica in entità

Programmazione Orientata agli Oggetti

Caratteristiche

La programmazione orientata agli oggetti organizza il codice in classi e oggetti che rappresentano entità del mondo reale. I dati sono memorizzati in proprietà e la logica in metodi.

Caratteristiche principali:

  • Organizzazione in classi e oggetti
  • Dati e logica raggruppati insieme
  • Rappresentazione di entità del mondo reale
  • Riusabilità attraverso l’ereditarietà e la composizione
  • Encapsulation: dati e metodi correlati sono raggruppati

Esempio Pratico: Form di Registrazione in OOP

La stessa applicazione può essere riscritta utilizzando un approccio orientato agli oggetti.

Classi necessarie:

  1. UserInputForm: gestisce il form e i suoi input
  2. Validator: contiene la logica di validazione riutilizzabile
  3. User: rappresenta un utente con i suoi dati e metodi

Implementazione OOP:

// Classe per la validazione
class Validator {
// Costanti per identificare il tipo di validazione
static REQUIRED = 'REQUIRED'
static MIN_LENGTH = 'MIN_LENGTH'
// Metodo statico per validare un valore
static validate(value, flag, validatorValue) {
if (flag === Validator.REQUIRED) {
// Validazione: campo obbligatorio
return value.trim().length > 0
}
if (flag === Validator.MIN_LENGTH) {
// Validazione: lunghezza minima
return value.trim().length > validatorValue
}
return false
}
}
// Classe per rappresentare un utente
class User {
constructor(username, password) {
this.username = username
this.password = password
}
// Metodo per salutare l'utente
greet() {
console.log('Hi I am ' + this.username)
}
}
// Classe per gestire il form di input
class UserInputForm {
constructor() {
// Ottenere riferimenti agli elementi DOM
this.form = document.getElementById('user-input')
this.usernameInput = document.getElementById('username')
this.passwordInput = document.getElementById('password')
// Aggiungere l'event listener
// Usare bind per mantenere il contesto 'this'
this.form.addEventListener('submit', this.signupHandler.bind(this))
}
signupHandler(event) {
event.preventDefault()
// Ottenere i valori inseriti
const enteredUsername = this.usernameInput.value
const enteredPassword = this.passwordInput.value
// Validare utilizzando la classe Validator
if (
!Validator.validate(enteredUsername, Validator.REQUIRED) ||
!Validator.validate(enteredPassword, Validator.MIN_LENGTH, 5)
) {
alert('Invalid input: username or password is wrong, password should be at least six characters')
return
}
// Creare un nuovo utente utilizzando la classe User
const newUser = new User(enteredUsername, enteredPassword)
// Log dell'utente e saluto
console.log(newUser)
newUser.greet()
}
}
// Creare un'istanza del form per inizializzare l'applicazione
new UserInputForm()

Analisi del Codice OOP

Organizzazione:

  • Il codice è organizzato in classi che rappresentano entità logiche
  • Ogni classe ha una responsabilità specifica
  • La logica è incapsulata nei metodi delle classi

Classi e responsabilità:

Validator:

  • Contiene solo logica di validazione
  • Metodo statico riutilizzabile in tutta l’applicazione
  • Può essere utilizzato da altri form senza modifiche

User:

  • Rappresenta un utente con i suoi dati
  • Contiene metodi relativi all’utente (es. greet)
  • Può essere esteso con altre funzionalità

UserInputForm:

  • Gestisce il form e l’interazione con il DOM
  • Utilizza Validator per la validazione
  • Crea istanze di User quando necessario

Vantaggi:

  • Codice organizzato in entità logiche
  • Facile da comprendere pensando al mondo reale
  • Riusabilità attraverso classi e metodi statici
  • Encapsulation: dati e metodi correlati sono raggruppati

Limitazioni:

  • Può avere overhead per applicazioni semplici
  • Richiede comprensione di classi e oggetti
  • Può essere verboso per operazioni semplici

Binding del Contesto

Un aspetto importante nell’OOP JavaScript è il binding del contesto this. Quando un metodo viene passato come callback (es. event listener), this non punta più all’istanza della classe.

Soluzione:

// Usare bind per mantenere il contesto corretto
this.form.addEventListener('submit', this.signupHandler.bind(this))

Questo garantisce che this all’interno di signupHandler punti all’istanza di UserInputForm.


Programmazione Funzionale

Caratteristiche

La programmazione funzionale organizza il codice in funzioni pure che trasformano input in output. Le funzioni dovrebbero essere il più possibile prive di effetti collaterali (side effects) e ricevere tutti i dati necessari come parametri.

Caratteristiche principali:

  • Organizzazione in funzioni pure quando possibile
  • Dati passati come parametri
  • Funzioni che restituiscono nuovi valori
  • Minimizzazione degli effetti collaterali
  • Funzioni riutilizzabili e testabili

Funzioni Pure vs Impure

Funzione pura:

  • Riceve tutti i dati necessari come parametri
  • Restituisce sempre lo stesso output per lo stesso input
  • Non modifica variabili esterne
  • Non ha effetti collaterali (DOM, HTTP, console.log, ecc.)

Funzione impura:

  • Può accedere a variabili esterne
  • Può avere effetti collaterali
  • Può restituire risultati diversi per lo stesso input
  • Necessaria per interagire con il mondo esterno (DOM, API, ecc.)

Esempio Pratico: Form di Registrazione Funzionale

La stessa applicazione può essere riscritta utilizzando un approccio funzionale.

Funzioni necessarie:

  1. connectForm: connette un form a un handler
  2. getUserInput: ottiene il valore di un input
  3. validate: valida un valore secondo criteri specifici
  4. createUser: crea un oggetto utente se la validazione ha successo
  5. greetUser: saluta un utente

Implementazione funzionale:

// Costanti per i flag di validazione
const REQUIRED = 'REQUIRED'
const MIN_LENGTH = 'MIN_LENGTH'
// Funzione per connettere un form a un handler
function connectForm(formId, submitHandler) {
const form = document.getElementById(formId)
form.addEventListener('submit', submitHandler)
}
// Funzione per ottenere il valore di un input
function getUserInput(inputElementId) {
const input = document.getElementById(inputElementId)
return input.value
}
// Funzione pura per validare un valore
function validate(value, flag, validatorValue) {
if (flag === REQUIRED) {
return value.trim().length > 0
}
if (flag === MIN_LENGTH) {
return value.trim().length > validatorValue
}
return false
}
// Funzione pura per creare un utente
function createUser(username, password) {
// Validare prima di creare
if (!validate(username, REQUIRED)) {
throw new Error('Invalid input: username must not be empty')
}
if (!validate(password, MIN_LENGTH, 5)) {
throw new Error('Invalid input: password must be six characters or longer')
}
// Restituire un nuovo oggetto utente
return {
username: username,
password: password
}
}
// Funzione per salutare un utente (ha side effect: console.log)
function greetUser(user) {
console.log('Hi I am ' + user.username)
}
// Handler principale per il submit del form
function signupHandler(event) {
event.preventDefault()
try {
// Ottenere i valori inseriti utilizzando funzioni pure
const enteredUsername = getUserInput('username')
const enteredPassword = getUserInput('password')
// Creare l'utente (può lanciare un errore se non valido)
const newUser = createUser(enteredUsername, enteredPassword)
// Log e saluto (side effects necessari)
console.log(newUser)
greetUser(newUser)
} catch (error) {
// Gestire errori di validazione
alert(error.message)
}
}
// Inizializzare l'applicazione connettendo il form
connectForm('user-input', signupHandler)

Analisi del Codice Funzionale

Organizzazione:

  • Il codice è organizzato in funzioni con responsabilità specifiche
  • Ogni funzione ha un compito ben definito
  • Le funzioni pure sono separate da quelle con effetti collaterali

Funzioni e responsabilità:

Funzioni pure:

  • validate: riceve parametri, restituisce booleano, nessun side effect
  • createUser: riceve parametri, restituisce oggetto, lancia errore se non valido
  • getUserInput: tecnicamente ha side effect (accede al DOM), ma è isolato

Funzioni con side effects:

  • connectForm: modifica il DOM aggiungendo event listener
  • signupHandler: previene comportamento di default, mostra alert
  • greetUser: usa console.log (side effect)

Vantaggi:

  • Funzioni piccole e focalizzate
  • Facile da testare (funzioni pure)
  • Alta riusabilità
  • Codice più leggibile e organizzato
  • Facile da comprendere il flusso di dati

Limitazioni:

  • Richiede comprensione di funzioni pure e side effects
  • Può essere più verboso per operazioni semplici
  • Alcuni effetti collaterali sono inevitabili (DOM, HTTP, ecc.)

Gestione degli Errori

Nell’approccio funzionale, invece di mostrare alert direttamente nelle funzioni di validazione, si lanciano errori che vengono gestiti nel punto appropriato:

// Funzione pura che lancia errore invece di mostrare alert
function createUser(username, password) {
if (!validate(username, REQUIRED)) {
throw new Error('Invalid input: username must not be empty')
}
// ...
}
// Gestione degli errori nel punto appropriato
try {
const newUser = createUser(username, password)
// Usa newUser
} catch (error) {
alert(error.message) // Side effect isolato
}

Questo mantiene le funzioni pure e isola gli effetti collaterali.


Confronto tra i Paradigmi

Tabella Comparativa

CaratteristicaProceduraleOrientata agli OggettiFunzionale
OrganizzazioneSequenza di passiClassi e oggettiFunzioni
Complessità inizialeBassaMediaMedia-Alta
RiusabilitàBassa (copy-paste)Alta (classi)Alta (funzioni)
TestabilitàMediaAltaMolto Alta
LeggibilitàMediaAltaAlta
Adatto perScript sempliciApplicazioni complesseApplicazioni complesse
OverheadMinimoMedioMinimo
Curva di apprendimentoBassaMediaMedia-Alta

Quando Usare Quale Paradigma

Programmazione Procedurale:

  • Script semplici e diretti
  • Automazioni rapide
  • Prototipi veloci
  • Quando la complessità è bassa
  • Quando non serve riutilizzare molto codice

Programmazione Orientata agli Oggetti:

  • Applicazioni complesse con molte entità
  • Quando si modella il mondo reale
  • Quando serve incapsulare dati e logica insieme
  • Quando serve ereditarietà o polimorfismo
  • Applicazioni con molti sviluppatori (organizzazione chiara)

Programmazione Funzionale:

  • Applicazioni complesse con molte trasformazioni di dati
  • Quando serve alta testabilità
  • Quando si lavora con dati immutabili
  • Quando si vuole minimizzare gli effetti collaterali
  • Applicazioni che richiedono molte operazioni di trasformazione

Combinazione di Paradigmi

È importante notare che non è necessario scegliere un solo paradigma. JavaScript permette di combinare approcci diversi:

// Esempio di codice che combina paradigmi
// Classe (OOP)
class UserService {
// Metodo che usa funzioni pure (Funzionale)
getUserData(userId) {
const rawData = this.fetchUser(userId)
return this.transformUserData(rawData) // Funzione pura
}
// Metodo con side effect
fetchUser(userId) {
// HTTP request
}
// Funzione pura
transformUserData(data) {
return {
name: data.name.toUpperCase(),
email: data.email.toLowerCase()
}
}
}
// Codice procedurale per inizializzare
const userService = new UserService()
const userId = getUserInput('userId-input') // Funzione
const userData = userService.getUserData(userId)
console.log(userData)

Best practice:

  • Usare il paradigma più adatto per ogni parte del codice
  • Non essere dogmatici: la flessibilità è un vantaggio di JavaScript
  • Scegliere in base alle esigenze del progetto

Vantaggi e Svantaggi Dettagliati

Programmazione Procedurale

Vantaggi:

  • Semplicità: facile da comprendere per principianti
  • Direttezza: nessuna astrazione aggiuntiva
  • Velocità di sviluppo: ideale per script rapidi
  • Nessun overhead: codice minimo necessario

Svantaggi:

  • Difficile da scalare: diventa complesso con applicazioni grandi
  • Bassa riusabilità: spesso richiede copy-paste
  • Difficile da testare: logica mescolata e difficile da isolare
  • Manutenzione: modifiche possono richiedere cambiamenti in molti punti

Programmazione Orientata agli Oggetti

Vantaggi:

  • Organizzazione chiara: codice organizzato in entità logiche
  • Riusabilità: classi e metodi possono essere riutilizzati
  • Encapsulation: dati e metodi correlati sono raggruppati
  • Modellazione del mondo reale: facile da comprendere pensando a entità reali
  • Ereditarietà: permette di estendere funzionalità esistenti

Svantaggi:

  • Overhead: può essere verboso per operazioni semplici
  • Complessità: richiede comprensione di classi, oggetti, this, binding
  • Rigidità: può essere difficile modificare strutture di classi complesse
  • Over-engineering: rischio di creare troppe astrazioni per problemi semplici

Programmazione Funzionale

Vantaggi:

  • Testabilità: funzioni pure sono facili da testare
  • Riusabilità: funzioni piccole e focalizzate sono altamente riutilizzabili
  • Leggibilità: codice chiaro e organizzato
  • Prevedibilità: funzioni pure producono risultati prevedibili
  • Debugging: più facile isolare problemi in funzioni piccole

Svantaggi:

  • Curva di apprendimento: richiede comprensione di concetti avanzati
  • Verbosità: può richiedere più codice per operazioni semplici
  • Side effects inevitabili: alcuni effetti collaterali sono necessari (DOM, HTTP)
  • Astrazione: può essere difficile per principianti comprendere il flusso

Esempi Pratici Comparativi

Esempio: Gestione di una Lista di Utenti

Procedurale:

const users = []
function addUser(name, email) {
users.push({ name: name, email: email })
}
function getUserByName(name) {
for (let i = 0; i < users.length; i++) {
if (users[i].name === name) {
return users[i]
}
}
return null
}
addUser('Mario', 'mario@example.com')
addUser('Luigi', 'luigi@example.com')
const user = getUserByName('Mario')
console.log(user)

Orientata agli Oggetti:

class User {
constructor(name, email) {
this.name = name
this.email = email
}
}
class UserManager {
constructor() {
this.users = []
}
addUser(name, email) {
const user = new User(name, email)
this.users.push(user)
}
getUserByName(name) {
return this.users.find(user => user.name === name) || null
}
}
const userManager = new UserManager()
userManager.addUser('Mario', 'mario@example.com')
userManager.addUser('Luigi', 'luigi@example.com')
const user = userManager.getUserByName('Mario')
console.log(user)

Funzionale:

// Funzioni pure
const createUser = (name, email) => ({ name, email })
const addUser = (users, user) => [...users, user]
const getUserByName = (users, name) =>
users.find(user => user.name === name) || null
// Utilizzo
let users = []
users = addUser(users, createUser('Mario', 'mario@example.com'))
users = addUser(users, createUser('Luigi', 'luigi@example.com'))
const user = getUserByName(users, 'Mario')
console.log(user)

Analisi degli Approcci

Procedurale:

  • Array globale modificato direttamente
  • Funzioni che operano sull’array globale
  • Semplice ma può causare problemi di stato condiviso

Orientata agli Oggetti:

  • Stato incapsulato nella classe UserManager
  • Metodi che operano sullo stato interno
  • Chiara separazione delle responsabilità

Funzionale:

  • Funzioni pure che non modificano lo stato esistente
  • Creazione di nuovi array invece di modificare quello esistente
  • Massima prevedibilità e testabilità

Best Practices

Scegliere il Paradigma Giusto

1. Valutare la complessità del progetto

  • Progetti semplici: procedurale può essere sufficiente
  • Progetti complessi: OOP o funzionale sono più adatti

2. Considerare il team

  • Team con esperienza OOP: usare OOP
  • Team con esperienza funzionale: usare funzionale
  • Team misto: combinare approcci

3. Considerare i requisiti

  • Molte entità del mondo reale: OOP
  • Molte trasformazioni di dati: funzionale
  • Script semplici: procedurale

4. Non essere dogmatici

  • Combinare paradigmi quando necessario
  • Usare il paradigma più adatto per ogni parte
  • Evitare over-engineering

Consistenza nel Codice

1. Mantenere coerenza

  • Una volta scelto un paradigma per una parte del codice, mantenerlo
  • Documentare le scelte architetturali
  • Seguire convenzioni del progetto

2. Refactoring graduale

  • Non è necessario riscrivere tutto
  • Refactorizzare quando necessario
  • Migliorare gradualmente la struttura

3. Code review

  • Rivedere il codice con il team
  • Discutere scelte architetturali
  • Apprendere da progetti esistenti

Conclusione

I paradigmi di programmazione sono modi diversi di organizzare e strutturare il codice. Non esiste un paradigma migliore in assoluto: la scelta dipende dal progetto, dal team e dai requisiti specifici.

Punti chiave:

  • Procedurale: ideale per script semplici e diretti
  • Orientata agli Oggetti: ideale per applicazioni complesse con molte entità
  • Funzionale: ideale per applicazioni con molte trasformazioni di dati

Raccomandazioni:

  • Comprendere tutti e tre i paradigmi
  • Scegliere in base alle esigenze del progetto
  • Non essere dogmatici: combinare approcci quando necessario
  • Mantenere coerenza all’interno di ogni parte del codice
  • Apprendere dai progetti esistenti e dalle best practices della community

La versatilità di JavaScript permette di utilizzare qualsiasi paradigma o combinazione di paradigmi, rendendo importante comprendere quando e come utilizzare ciascuno di essi.

Continua la lettura

Leggi il prossimo capitolo: "Web Components"

Continua a leggere