Capitolo 5 · Sviluppo web con Spring Boot: il corso pratico
Hai un controller che riceve le richieste, cerca i pazienti, assegna gli id e tiene pure la lista dei dati, e adesso arriva la domanda scomoda: non sta facendo un po' troppo?
Sì, sta facendo troppo. E la cura, in Spring Boot, ha un nome che a molti fa venire l'orticaria: dependency injection. Detto così sembra una cosa da architetti con la barba lunga.
La realtà? Si riduce a una domanda di tutti i giorni: chi si occupa di costruire gli oggetti di cui hai bisogno, tu o qualcun altro? Mettiamo da parte i termini grossi e partiamo da dove si inceppa il codice.
Prima: il controller deve solo smistare
Nei vecchi progetti era comune trovare controller enormi, che mescolavano query, calcoli e persino invio di email: un anti-pattern noto come God Object, impossibile da provare e fragile a ogni modifica. Per evitarlo, una regola semplice: il controller smista, non decide.
Nel pezzo precedente il controller faceva tre mestieri: gestiva gli indirizzi, applicava la logica (cercare, assegnare gli id) e conservava i dati. Se ti serve il quadro generale sul perché questo sia un problema, lo trovi nella sezione sulla Separation of Concerns di REST API spiegate semplici. Qui ci interessa la versione pratica: spostare la logica in una classe a parte, il service.
Si fa in tre mosse:
- Crei un package
servicese dentro una classePazienteService. - Sposti lì la lista dei pazienti e tutta la logica che stava nel controller.
- Esponi tre metodi con nomi standard, quelli che trovi in quasi tutti i progetti:
getAllPazienti,getPazienteById,addPaziente.
Il controller, a quel punto, non sa più come si cerca un paziente: sa solo a chi chiederlo. Gli resta il compito che è davvero suo, cioè interpretare l'indirizzo e passare il valore giusto al service. Per esempio, la lettura del parametro {id} resta nel controller, mentre il service si aspetta un semplice numero. Ecco il service, con la lista e la stessa logica di prima (la ricerca per id è scritta con uno stream invece del ciclo, ma fa la stessa cosa):
@Service
public class PazienteService {
private final List<Paziente> pazienti = new ArrayList<>(List.of(
new Paziente(1, "Mario", "Rossi", 36),
new Paziente(2, "Giulia", "Bianchi", 29)));
public List<Paziente> getAllPazienti() {
return pazienti;
}
public Paziente getPazienteById(int id) {
return pazienti.stream()
.filter(p -> p.getId() == id)
.findFirst()
.orElse(null);
}
public Paziente addPaziente(Paziente paziente) {
paziente.setId(pazienti.size() + 1);
pazienti.add(paziente);
return paziente;
}
}Per ora ignora quel @Service in cima: ci torniamo fra poco.
Cos'è il dominio, e perché il service gli fa da casa?
Fermiamoci su una parola che finora abbiamo solo sfiorato. Mettiamo da parte la definizione da tesi di laurea: il dominio (in inglese business domain) è semplicemente il problema del mondo reale che il tuo codice cerca di risolvere. È la ragione per cui il software esiste e per cui qualcuno lo paga.
Nel nostro gestionale il dominio non è Spring Boot, né il database, né il JSON, né @RestController, né la porta 8080: quella è infrastruttura, l'impianto idraulico ed elettrico dell'applicazione. Il dominio è il funzionamento del pronto soccorso nel mondo reale: i pazienti che arrivano, i medici di guardia, i codici di priorità, la capienza delle sale visite, le liste d'attesa.
Del dominio fanno parte le sue regole, quelle che valgono a prescindere dalla tecnologia. Qualche esempio, inventato per il nostro ospedale di fantasia:
- un paziente con un codice più grave passa davanti a uno con un codice meno grave, a prescindere da chi è arrivato prima;
- un medico non può essere di turno in due reparti nello stesso momento;
- un letto in rianimazione non può essere assegnato a due persone.
Il database queste regole non le conosce: salva dati su un disco e basta. Non le conosce nemmeno il controller, che riceve richieste HTTP e risponde in JSON. Il codice che le conosce e le applica è il codice di dominio, ed è quello che vogliamo tenere in un posto solo. Nel nostro piccolo progetto quel posto è il service (nei progetti più grandi il dominio vive anche nelle classi del modello, ma per ora il service è la casa giusta). Oggi fa pochissimo, assegna un id, ma domani ospiterà le regole vere. Gli strati, quindi, sono tre:
- Interfaccia (il controller): è la dogana. Riceve la richiesta, controlla che sia scritta bene e risponde con lo status code giusto.
- Dominio (il service): è il motore decisionale. Applica le regole del problema reale.
- Infrastruttura (per ora una lista, poi il repository): scrive e legge i dati.
Perché tanta cura? Prova con questa domanda: se domani l'ospedale abbandonasse il web per un programma a riga di comando, o tu cambiassi framework, le regole del pronto soccorso cambierebbero? No: un caso grave passa avanti sia con Java sia con un foglio di carta. Se il dominio è isolato dal resto, puoi cambiare database o framework senza riscrivere una regola, puoi provare le regole con un test in Java puro, senza avviare un server né collegarti a un database, e puoi parlare con il cliente nella sua lingua: Pazienti, Codici, Ricoveri, non ArrayList e tabelle SQL. Quest'ultima idea ha un nome nella progettazione software, ubiquitous language: lo stesso vocabolario per chi scrive il codice e per chi conosce il mestiere.
In che ordine si costruisce tutto questo?
C'è un ordine che aiuta a non perdersi, e conviene rispettarlo. Immagina una catena di montaggio: prima il modello (Paziente), che è il pezzo comune e che non dipende da nessuno. Poi il service, che usa il modello per fare il suo lavoro. Per ultimo il controller, che usa il service. Ognuno dipende da quello che lo precede, mai da quello che lo segue.
Il problema: chi costruisce il service?
Il controller ora ha bisogno di un PazienteService. La soluzione più ovvia è scrivere dentro ogni metodo new PazienteService():
@PostMapping
public Paziente inserisci(@RequestBody Paziente paziente) {
PazienteService service = new PazienteService();
return service.addPaziente(paziente);
}Funziona, ma ha due difetti.
Il primo: a ogni richiesta nasce un oggetto nuovo, che occupa memoria e poi va buttato.
Il secondo è peggiore. Se è il service a tenere la lista dei pazienti, ogni copia ha la sua lista. Registri un paziente con una POST e quella copia sparisce insieme alla richiesta: la GET successiva ne crea un'altra, con la lista originale. In un pronto soccorso sarebbe un piccolo disastro: i dati, per farla breve, non si accumulano.
L'ho provato davvero, con new in ogni metodo. La POST risponde regolarmente, con l'id assegnato:
{"id":3,"nome":"Luca","cognome":"Verdi","eta":52}Ma subito dopo, la lista contiene ancora solo i due pazienti di partenza:
[{"id":1,"nome":"Mario","cognome":"Rossi","eta":36},{"id":2,"nome":"Giulia","cognome":"Bianchi","eta":29}]E GET /api/pazienti/3 risponde con status 200 e nessun contenuto: Luca non c'è più.
Primo tentativo di rimedio: costruire il service una volta sola, in un campo del controller dichiarato private final (il final significa che, una volta assegnato, non cambia più). Meglio, ma resta una stortura: il controller continua a decidere come si costruisce il suo collaboratore.
Il passo successivo è un'idea semplice: il controller non costruisce niente, chiede il service nel costruttore, e qualcun altro glielo consegna. Ed è qui che entra Spring.
Il magazzino dei bean
Il controller, con il service richiesto nel costruttore, si scrive così:
@RestController
@RequestMapping("api/pazienti")
public class PazienteController {
private final PazienteService service;
public PazienteController(PazienteService service) {
this.service = service;
}
// i metodi chiamano service.getAllPazienti() e compagnia
}Se lanci l'applicazione così, e nel service manca @Service, la prima volta non parte. Il messaggio, copiato dal log, è chiaro: il parametro del costruttore richiede un oggetto di tipo PazienteService, ma Spring non ha trovato nessun bean di quel tipo.
APPLICATION FAILED TO START Description: Parameter 0 of constructor in com.ospedale.prontosoccorso.controllers.PazienteController required a bean of type 'com.ospedale.prontosoccorso.services.PazienteService' that could not be found. Action: Consider defining a bean of type 'com.ospedale.prontosoccorso.services.PazienteService' in your configuration.
Cos'è un bean? Un oggetto che gestisce Spring al posto tuo. Spring tiene tutti i suoi bean in un contenitore (il bean container, o factory): immaginalo come un magazzino in cui ogni oggetto condiviso sta in un solo esemplare, pronto a essere consegnato a chi lo chiede. Costruirlo e consegnarlo è compito suo.
Perché Spring trovi il tuo PazienteService, devi dichiararlo: basta l'annotazione @Service sulla classe. Con quella, il service entra nel magazzino. I bean principali che incontrerai sono tre:
@Controller(o@RestController): gestisce gli indirizzi e le richieste.@Service: contiene la logica applicativa.@Repository: si occupa dell'accesso ai dati.
Un dettaglio che spiega molte cose: anche @RestController è un bean, una variante particolare di @Controller. Ecco perché ci hai messo un'annotazione sopra e Spring l'ha trovato senza che tu lo registrassi da nessuna parte.
E il vantaggio che ci interessa: di ogni bean esiste un solo esemplare condiviso (in gergo, un singleton). Controller e service ricevono sempre lo stesso PazienteService, quindi la lista dei dati è unica e i pazienti registrati non scompaiono.
Due strade per farsi consegnare le cose
1. Il costruttore
È quella di cui abbiamo parlato: la classe dichiara nel costruttore cosa le serve, e Spring glielo passa. È la più esplicita, perché basta guardare il costruttore per sapere di cosa ha bisogno la classe, e non puoi creare l'oggetto dimenticando una dipendenza. Il campo può essere final, quindi non cambia più una volta assegnato, e in un test puoi creare il controller a mano passandogli un service finto, senza avviare Spring.
2. @Autowired
C'è anche una scorciatoia che nei tutorial si vede ovunque:
@Autowired
private PazienteService service;
Cosa dice? "Spring, questo campo lo riempi tu, prendendo l'oggetto giusto dal magazzino." Niente costruttore, niente new: il controller dichiara cosa gli serve e quello arriva. È la dependency injection nella sua forma più compatta: la dipendenza viene iniettata dall'esterno, e non costruita dentro.
Ho provato anche questa variante: avviata con @Autowired sul campo e senza costruttore, l'applicazione parte e, come con il costruttore, la POST di Luca resta visibile nelle letture successive.
Il prezzo della scorciatoia è che il campo non può essere final e che da fuori è più scomodo sostituirlo con un service finto nei test. Due strade, quindi, per lo stesso risultato. Una più esplicita, una più corta: nei progetti che troverai in giro ti capiterà di incontrarle entrambe, ma se devi scegliere tu, parti dal costruttore.
Prima e dopo
A lavoro finito, il controller si riduce a questo:
| Prima | Dopo | |
|---|---|---|
| Lista dei dati | Dentro il controller | Dentro il service (poi nel repository) |
| Logica | Cerca e assegna gli id nel controller | Nel service |
| Chi costruisce il service | Il controller, con new a ogni richiesta | Spring, una volta sola |
| Cosa fa il controller | Tutto | Smista le richieste e chiama il service |
Il controller, a lavoro finito, è questo:
@RestController
@RequestMapping("api/pazienti")
public class PazienteController {
private final PazienteService service;
public PazienteController(PazienteService service) {
this.service = service;
}
@GetMapping
public List<Paziente> elenco() {
return service.getAllPazienti();
}
@GetMapping("{id}")
public Paziente dettaglio(@PathVariable int id) {
return service.getPazienteById(id);
}
@PostMapping
public Paziente inserisci(@RequestBody Paziente paziente) {
return service.addPaziente(paziente);
}
}Per chi usa l'API non cambia niente: gli stessi endpoint, gli stessi JSON, gli stessi status code. Solo che adesso, se domani la lista diventa un database, tocchi il codice che gestisce i dati e il controller non se ne accorge.
Conclusione
La dependency injection, in pratica, è la decisione di non costruire da soli gli oggetti di cui hai bisogno e di farteli consegnare. Il service si occupa della logica, il controller smista, e Spring tiene il magazzino. Tre livelli, ognuno con un mestiere solo: il controller all'ingresso, il service nel mezzo e i dati in fondo. E la parola da portarti a casa è un'altra: il dominio sono le regole del pronto soccorso, tutto il resto, dai controller al JSON, è il tubo attraverso cui viaggiano.
Resta un buco: la lista dei pazienti sta ancora dentro il service, a fare finta di essere un database. Il prossimo passo è darle una casa vera, il repository.

Dario Amato
Sviluppatore software
Informatico con 15 anni di esperienza nello sviluppo software, guidato da una forte curiosità per le nuove frontiere dell'AI e del Web Marketing.
