Value Objects in PHP: smetti di scrivere codice ossessionato dalle primitive
Guarda quasi qualsiasi codebase PHP e troverai lo stesso schema: email memorizzate come stringhe, denaro memorizzato come float, ID memorizzati come interi, numeri di telefono memorizzati come stringhe. Questo si chiama primitive obsession (ossessione per le primitive) — l'uso eccessivo di tipi primitivi per rappresentare concetti del dominio. È uno degli anti-pattern più comuni e più dannosi nello sviluppo PHP moderno.
I Value Objects sono la cura. Portano sicurezza dei tipi, validazione e chiarezza nel tuo codice — e una volta che inizi a usarli, ti chiederai come hai potuto vivere senza.
Cos'è un Value Object?
Un Value Object (VO, oggetto-valore) è un piccolo oggetto immutabile che rappresenta un concetto del tuo dominio. A differenza di un'Entity, un Value Object non ha identità — è definito interamente dai suoi valori. Due Value Objects con gli stessi valori sono considerati uguali.
Gli esempi classici includono:
- Email — invece di una stringa grezza
- Money — invece di un float più una stringa di valuta
- UserId — invece di un intero
- PhoneNumber — invece di una stringa
- DateRange — invece di due oggetti DateTime
- Address — invece di un array di stringhe
- Password — invece di una semplice stringa
Il problema con le primitive
Considera questo codice tipico:
Ogni valore primitivo porta con sé assunzioni nascoste. Il sistema dei tipi non ti dice nulla su:
- Se il valore è valido
- In quale formato si trova
- Quali operazioni sono consentite
- Quali unità o valuta rappresenta
- Se può essere modificato o riutilizzato in sicurezza
Il risultato? Logica di validazione sparsa ovunque, bug dovuti a formati non corrispondenti e codice difficile da ragionare.
Il tuo primo Value Object: Email
Sostituiamo una stringa primitiva con un Value Object appropriato:
Ora il sistema dei tipi ti protegge. Se hai un oggetto Email, sai che è valido. Niente più controlli di validazione sparsi.
Usare il Value Object
Un esempio più complesso: Money
Il denaro è il caso classico in cui le primitive falliscono catastroficamente. I float perdono precisione, e mescolare valute è un bug silenzioso che aspetta solo di accadere.
Ora guarda quanto diventa pulito il codice chiamante:
Caratteristiche chiave di un buon Value Object
- Immutabile — una volta creato, non cambia mai. Le operazioni restituiscono nuove istanze.
- Auto-validante — un'istanza non può esistere in uno stato non valido.
- Senza identità — l'uguaglianza si basa sui valori, non su un ID del database.
- Senza effetti collaterali — nessuna chiamata al database, nessun logging, nessuno stato globale.
- Piccolo — rappresenta un concetto, non un intero aggregato.
- Sostituibile — l'intero oggetto può essere scambiato, non mutato campo per campo.
Value Object vs. DTO vs. Entity
È importante distinguere questi tre pattern:
- Value Object — immutabile, definito dai valori, contiene comportamento relativo a quei valori (come
add()su Money). - DTO — immutabile o mutabile, trasporta dati tra i livelli, non contiene logica di business.
- Entity — ha un'identità unica che persiste nel tempo, anche quando i suoi attributi cambiano.
Dove brillano i Value Objects
- Modelli di dominio — rappresentazione di email, denaro, ID, date, indirizzi.
- Validazione dell'input — creare un VO dall'input dell'utente garantisce la validità ovunque a valle.
- Regole di business — incapsulamento di logica come la corrispondenza delle valute o i controlli sugli intervalli di date.
- Testing — test più semplici, veloci e mirati senza dipendenze dal database.
- Refactoring — quando i requisiti cambiano, modifichi il VO in un unico posto.
Insidie comuni da evitare
- Aggiungere setter — questo rompe l'immutabilità. Se hai bisogno di un valore diverso, crea una nuova istanza.
- Renderli troppo grandi — un VO dovrebbe rappresentare un concetto, non un'intera radice di aggregato.
- Aggiungere logica di persistenza — mantieni le preoccupazioni del database nei repository, non nei VO.
- Ignorare l'uguaglianza — implementa sempre un metodo
equals(). - Lanciare eccezioni generiche — definisci eccezioni specifiche del dominio per una gestione degli errori più chiara.
Un percorso di refactoring pratico
Non devi riscrivere l'intera applicazione dall'oggi al domani. Inizia da qui:
- Identifica un campo primitivo che causa bug o confusione (email, denaro, ID).
- Crea un Value Object per esso con validazione e metodi utili.
- Sostituisci la primitiva in una classe o un modulo.
- Lascia che il compilatore e i tuoi test guidino il resto.
- Ripeti con il prossimo candidato ovvio.
Conclusione
I Value Objects sono uno dei refactoring a più alto impatto che puoi fare in una codebase PHP. Eliminano l'ossessione per le primitive, centralizzano la validazione, rendono impossibile rappresentare stati non validi e trasformano il tuo codice in un'espressione auto-documentante del tuo dominio. Il sistema dei tipi diventa un alleato invece che una formalità.
Inizia con un singolo Value Object — magari Email o Money. Una volta che vedrai quanta chiarezza porta al tuo codice, non tornerai mai più a passare stringhe grezze in giro.
Smetti di scrivere codice ossessionato dalle primitive. Lascia che i tuoi tipi parlino per il tuo dominio.