Man mano che i progetti software crescono, diventa sempre più importante mantenere il codice organizzato, manutenibile e scalabile. Qui entrano in gioco i design pattern. I design pattern forniscono soluzioni comprovate e riutilizzabili per sfide comuni di progettazione del software, rendendo il tuo codice più efficiente e più facile da gestire.
In questa guida, approfondiremo alcuni dei design pattern più popolari e ti mostreremo come implementarli in Spring Boot. Alla fine, non solo comprenderai concettualmente questi pattern ma sarai anche in grado di applicarli nei tuoi progetti con fiducia.
Indice
Introduzione ai Design Patterns
I modelli di design sono soluzioni riutilizzabili per problemi comuni di design software. Pensali come le migliori pratiche distillate in modelli che possono essere applicati per risolvere sfide specifiche nel tuo codice. Non sono specifici per nessun linguaggio, ma possono essere particolarmente potenti in Java a causa della sua natura orientata agli oggetti.
In questa guida, tratteremo:
-
Modello Singleton: Garantire che una classe abbia solo un’istanza.
-
Modello Factory: Creare oggetti senza specificare la classe esatta.
-
Modello Strategy: Permettere la selezione degli algoritmi a runtime.
-
Modello Observer: Impostare una relazione di pubblicazione-sottoscrizione.
Non tratteremo solo come funzionano questi modelli, ma esploreremo anche come possono essere applicati in Spring Boot per applicazioni del mondo reale.
Come impostare il tuo progetto Spring Boot
Prima di immergerci nei modelli, impostiamo un progetto Spring Boot:
Requisiti
Assicurati di avere:
-
Java 11+
-
Maven
-
Spring Boot CLI (opzionale)
-
Postman o curl (per il testing)
Inizializzazione del Progetto
È possibile creare rapidamente un progetto Spring Boot utilizzando Spring Initializr:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d name=DesignPatternsDemo \
-d javaVersion=11 -o design-patterns-demo.zip
unzip design-patterns-demo.zip
cd design-patterns-demo
Cosa è il Pattern Singleton?
Il pattern Singleton garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale ad essa. Questo pattern è comunemente utilizzato per servizi come il logging, la gestione della configurazione o le connessioni al database.
Come Implementare il Pattern Singleton in Spring Boot
I bean di Spring Boot sono singleton per impostazione predefinita, il che significa che Spring gestisce automaticamente il ciclo di vita di questi bean per garantire che esista solo un’istanza. Tuttavia, è importante capire come funziona il pattern Singleton sotto il cofano, specialmente quando non si utilizzano bean gestiti da Spring o si ha bisogno di maggiore controllo sulla gestione delle istanze.
Attraverso una implementazione manuale del pattern Singleton, vedremo come è possibile controllare la creazione di un’unica istanza all’interno della tua applicazione.
Passaggio 1: Creare una Classe LoggerService
In questo esempio, creeremo un semplice servizio di logging utilizzando il pattern Singleton. L’obiettivo è garantire che tutte le parti dell’applicazione utilizzino la stessa istanza di logging.
public class LoggerService {
// La variabile statica che contiene l'istanza singola
private static LoggerService instance;
// Costruttore privato per impedire l'istanziazione dall'esterno
private LoggerService() {
// Questo costruttore è intenzionalmente vuoto per impedire ad altre classi di creare istanze
}
// Metodo pubblico per fornire accesso all'istanza singola
public static synchronized LoggerService getInstance() {
if (instance == null) {
instance = new LoggerService();
}
return instance;
}
// Metodo di esempio per il logging
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
-
Variabile Statica (
istanza
): Questo contiene l’istanza singola diLoggerService
. -
Costruttore Privato: Il costruttore è contrassegnato come privato per impedire ad altre classi di creare nuove istanze direttamente.
-
Metodo
getInstance()
Sincronizzato: Il metodo è sincronizzato per renderlo thread-safe, garantendo che venga creata solo un’istanza anche se più thread cercano di accedervi contemporaneamente. -
Inizializzazione pigra: L’istanza viene creata solo quando viene richiesta per la prima volta (
inizializzazione pigra
), il che è efficiente in termini di utilizzo della memoria.
Utilizzo nel mondo reale: Questo modello è comunemente utilizzato per risorse condivise, come il logging, le impostazioni di configurazione o la gestione delle connessioni al database, dove si desidera controllare l’accesso e garantire che venga utilizzata solo un’istanza in tutto l’applicativo.
Passo 2: Utilizzare il Singleton in un controller Spring Boot
Ora, vediamo come possiamo utilizzare il nostro Singleton LoggerService
all’interno di un controller Spring Boot. Questo controller esporrà un endpoint che registra un messaggio ogni volta che viene accesso.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LogController {
@GetMapping("/log")
public ResponseEntity<String> logMessage() {
// Accesso all'istanza Singleton di LoggerService
LoggerService logger = LoggerService.getInstance();
logger.log("This is a log message!");
return ResponseEntity.ok("Message logged successfully");
}
}
-
Endpoint GET: Abbiamo creato un endpoint
/log
che, quando viene accesso, registra un messaggio utilizzando ilLoggerService
. -
Utilizzo del Singleton: Invece di creare una nuova istanza di
LoggerService
, chiamiamogetInstance()
per assicurarci di utilizzare la stessa istanza ogni volta. -
Risposta: Dopo il logging, il punto finale restituisce una risposta che indica il successo.
Passaggio 3: Test del Pattern Singleton
Ora, testiamo questo punto finale utilizzando Postman o il tuo browser:
GET http://localhost:8080/log
Output Previsto:
-
Log Console:
[LOG] Questo è un messaggio di log!
-
Risposta HTTP:
Messaggio registrato con successo
Puoi chiamare il punto finale più volte e vedrai che viene utilizzata la stessa istanza di LoggerService
, come indicato dall’output di log coerente.
Casi d’Uso del Pattern Singleton nel Mondo Reale
Ecco quando potresti voler utilizzare il pattern Singleton nelle applicazioni reali:
-
Gestione della Configurazione: Assicurati che la tua applicazione utilizzi un insieme coerente di impostazioni di configurazione, specialmente quando tali impostazioni sono caricate da file o database.
-
Pool di connessioni al database: Controlla l’accesso a un numero limitato di connessioni al database, garantendo che lo stesso pool sia condiviso in tutta l’applicazione.
-
Cache: Mantiene un’unica istanza di cache per evitare dati non coerenti.
-
Servizi di registrazione: Come mostrato in questo esempio, utilizza un singolo servizio di registrazione per centralizzare gli output di log tra i diversi moduli dell’applicazione.
Concetti Chiave
-
Il pattern Singleton è un modo semplice per garantire che venga creata solo un’istanza di una classe.
-
La sicurezza dei thread è cruciale se più thread accedono al Singleton, ecco perché abbiamo utilizzato
synchronized
nel nostro esempio. -
I bean di Spring Boot sono già singleton per impostazione predefinita, ma capire come implementarlo manualmente ti aiuta a ottenere maggiore controllo quando necessario.
Questo copre l’implementazione e l’uso del pattern Singleton. Successivamente, esploreremo il pattern Factory per vedere come può aiutare a razionalizzare la creazione di oggetti.
Cosa è il pattern Factory?
Il pattern Factory ti permette di creare oggetti senza specificare la classe esatta. Questo pattern è utile quando hai diversi tipi di oggetti che devono essere istanziati in base a un input.
Come Implementare una Factory in Spring Boot
Il pattern Factory è incredibilmente utile quando hai bisogno di creare oggetti basati su determinati criteri ma desideri disaccoppiare il processo di creazione degli oggetti dalla logica dell’applicazione principale.
In questa sezione, vedremo come costruire una NotificationFactory
per inviare notifiche tramite Email o SMS. Questo è particolarmente utile se prevedi di aggiungere più tipi di notifiche in futuro, come notifiche push o avvisi in-app, senza modificare il codice esistente.
Passaggio 1: Creare l’Interfaccia Notification
Il primo passo è definire un’interfaccia comune che tutti i tipi di notifica implementeranno. Ciò garantisce che ciascun tipo di notifica (Email, SMS, ecc.) avrà un metodo send()
coerente.
public interface Notification {
void send(String message);
}
-
Scopo: L’interfaccia
Notification
definisce il contratto per l’invio di notifiche. Qualsiasi classe che implementa questa interfaccia deve fornire un’implementazione per il metodosend()
. -
Scalabilità: Utilizzando un’interfaccia, è possibile estendere facilmente l’applicazione in futuro per includere altri tipi di notifiche senza modificare il codice esistente.
Passo 2: Implementare EmailNotification
e SMSNotification
Adesso, implementiamo due classi concrete, una per l’invio di email e un’altra per l’invio di messaggi SMS.
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
public class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
Passo 3: Creare una NotificationFactory
La classe NotificationFactory
è responsabile per la creazione delle istanze di Notification
basate sul tipo specificato. Questo design garantisce che il NotificationController
non debba conoscere i dettagli della creazione degli oggetti.
public class NotificationFactory {
public static Notification createNotification(String type) {
switch (type.toUpperCase()) {
case "EMAIL":
return new EmailNotification();
case "SMS":
return new SMSNotification();
default:
throw new IllegalArgumentException("Unknown notification type: " + type);
}
}
}
Factory Method (createNotification()
):
-
Il metodo di fabbrica prende una stringa (
type
) come input e restituisce un’istanza della classe di notifica corrispondente. -
Switch Statement: Lo statement switch seleziona il tipo di notifica appropriato in base all’input.
-
Gestione degli errori: Se il tipo fornito non è riconosciuto, viene lanciata un’eccezione
IllegalArgumentException
. Questo assicura che i tipi non validi siano individuati precocemente.
Perché utilizzare una Factory?
-
Disaccoppiamento: Il pattern factory separa la creazione degli oggetti dalla logica aziendale. Questo rende il tuo codice più modulare e più facile da mantenere.
-
Estensibilità: Se desideri aggiungere un nuovo tipo di notifica, devi solo aggiornare la factory senza modificare la logica del controller.
Passo 4: Utilizzare la Factory in un Controller Spring Boot
Ora, mettiamo insieme tutto creando un controller Spring Boot che utilizza la NotificationFactory
per inviare notifiche in base alla richiesta dell’utente.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NotificationController {
@GetMapping("/notify")
public ResponseEntity<String> notify(@RequestParam String type, @RequestParam String message) {
try {
// Crea l'oggetto Notification appropriato utilizzando la factory
Notification notification = NotificationFactory.createNotification(type);
notification.send(message);
return ResponseEntity.ok("Notification sent successfully!");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
Endpoint GET (/notify
):
-
Il controller espone un endpoint
/notify
che accetta due parametri di query:type
(sia “EMAIL” che “SMS”) emessage
. -
Utilizza il
NotificationFactory
per creare il tipo di notifica appropriato e invia il messaggio. -
Gestione degli errori: Se viene fornito un tipo di notifica non valido, il controller cattura l’
IllegalArgumentException
e restituisce una risposta400 Bad Request
.
Passaggio 5: Testare il pattern Factory
Testiamo l’endpoint utilizzando Postman o un browser:
-
Invia una notifica via email:
GET http://localhost:8080/notify?type=email&message=Hello%20Email
Output:
Invio Email: Hello Email
-
Invia una Notifica via SMS:
GET http://localhost:8080/notify?type=sms&message=Hello%20SMS
Output:
Invio SMS: Hello SMS
-
Test con un Tipo Non Valido:
GET http://localhost:8080/notify?type=unknown&message=Test
Output:
Richiesta non valida: Tipo di notifica sconosciuto: unknown
Casi d’Uso del Pattern Factory nel Mondo Reale
Il pattern Factory è particolarmente utile in scenari in cui:
-
Creazione Dinamica di Oggetti: Quando devi creare oggetti in base all’input dell’utente, come l’invio di diversi tipi di notifiche, la generazione di report in vari formati o la gestione di diversi metodi di pagamento.
-
Separazione della creazione degli oggetti: Utilizzando una factory, è possibile mantenere la logica principale del business separata dalla creazione degli oggetti, rendendo il codice più mantenibile.
-
Scalabilità: Estendi facilmente la tua applicazione per supportare nuovi tipi di notifiche senza modificare il codice esistente. Basta aggiungere una nuova classe che implementa l’interfaccia
Notification
e aggiornare la factory.
Che cos’è il Pattern Strategy?
Il Pattern Strategy è perfetto quando è necessario passare dinamicamente tra più algoritmi o comportamenti. Consente di definire una famiglia di algoritmi, incapsularli in classi separate e renderli facilmente interscambiabili durante l’esecuzione. Questo è particolarmente utile per selezionare un algoritmo in base a condizioni specifiche, mantenendo il codice pulito, modulare e flessibile.
Caso d’Uso del Mondo Reale: Immagina un sistema di e-commerce che deve supportare diverse opzioni di pagamento, come carte di credito, PayPal o bonifici bancari. Utilizzando il pattern Strategy, puoi facilmente aggiungere o modificare metodi di pagamento senza modificare il codice esistente. Questo approccio garantisce che la tua applicazione rimanga scalabile e manutenibile mentre introduci nuove funzionalità o aggiorni quelle esistenti.
Dimostreremo questo pattern con un esempio di Spring Boot che gestisce i pagamenti utilizzando una strategia di carta di credito o PayPal.
Passo 1: Definire un’Interfaccia PaymentStrategy
Iniziamo creando un’interfaccia comune che tutte le strategie di pagamento implementeranno:
public interface PaymentStrategy {
void pay(double amount);
}
L’interfaccia definisce un contratto per tutti i metodi di pagamento, garantendo coerenza tra le implementazioni.
Passo 2: Implementare le Strategie di Pagamento
Crea classi concrete per i pagamenti con carta di credito e PayPal.
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with Credit Card");
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal");
}
}
Ogni classe implementa il metodo pay()
con il proprio comportamento specifico.
Passo 3: Utilizzare la Strategia in un Controller
Crea un controller per selezionare dinamicamente una strategia di pagamento in base all’input dell’utente:
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PaymentController {
@GetMapping("/pay")
public ResponseEntity<String> processPayment(@RequestParam String method, @RequestParam double amount) {
PaymentStrategy strategy = selectPaymentStrategy(method);
if (strategy == null) {
return ResponseEntity.badRequest().body("Invalid payment method");
}
strategy.pay(amount);
return ResponseEntity.ok("Payment processed successfully!");
}
private PaymentStrategy selectPaymentStrategy(String method) {
switch (method.toUpperCase()) {
case "CREDIT": return new CreditCardPayment();
case "PAYPAL": return new PayPalPayment();
default: return null;
}
}
}
L’endpoint accetta metodo
e importo
come parametri di query e elabora il pagamento utilizzando la strategia appropriata.
Passo 4: Testare l’Endpoint
-
Pagamento con Carta di Credito:
GET http://localhost:8080/paga?metodo=carta_di_credito&importo=100
Output:
Pagato $100.0 con Carta di Credito
-
Pagamento con PayPal:
GET http://localhost:8080/paga?metodo=paypal&importo=50
Output:
Pagato $50.0 tramite PayPal
-
Metodo Non Valido:
GET http://localhost:8080/paga?metodo=bitcoin&importo=25
Output:
Metodo di pagamento non valido
Casi d’Uso per il Pattern Strategia
-
Elaborazione dei pagamenti: Passare dinamicamente tra diversi gateway di pagamento.
-
Algoritmi di ordinamento: Scegli il miglior metodo di ordinamento in base alle dimensioni dei dati.
-
Esportazione di file: Esporta report in vari formati (PDF, Excel, CSV).
Concetti chiave
-
Il pattern Strategy mantiene il tuo codice modulare e segue il principio Aperto/Chiuso.
-
Aggiungere nuove strategie è facile: basta creare una nuova classe che implementa l’interfaccia
PaymentStrategy
. -
È ideale per scenari in cui è necessaria una selezione flessibile degli algoritmi durante l’esecuzione.
Successivamente, esploreremo il pattern Observer, perfetto per gestire architetture basate sugli eventi.
Cos’è il pattern Observer?
Il pattern Observer è ideale quando hai un oggetto (il soggetto) che deve notificare a più altri oggetti (osservatori) i cambiamenti nel suo stato. È perfetto per sistemi basati su eventi in cui gli aggiornamenti devono essere inviati a vari componenti senza creare un accoppiamento stretto tra di loro. Questo pattern ti permette di mantenere un’architettura pulita, specialmente quando diverse parti del tuo sistema devono reagire ai cambiamenti in modo indipendente.
Caso d’uso nel mondo reale: Questo pattern è comunemente usato in sistemi che inviano notifiche o avvisi, come applicazioni di chat o monitor di prezzi delle azioni, dove gli aggiornamenti devono essere inviati agli utenti in tempo reale. Utilizzando il pattern Observer, puoi facilmente aggiungere o rimuovere tipi di notifica senza alterare la logica principale.
Dimostreremo come implementare questo pattern in Spring Boot creando un semplice sistema di notifiche in cui vengono inviate notifiche via email e SMS ogni volta che un utente si registra.
Passo 1: Creare un’Interfaccia Observer
Iniziamo definendo un’interfaccia comune che tutti gli osservatori implementeranno:
public interface Observer {
void update(String event);
}
L’interfaccia stabilisce un contratto in cui tutti gli osservatori devono implementare il metodo update()
, che verrà attivato ogni volta che il soggetto cambia.
Passo 2: Implementare EmailObserver
e SMSObserver
Successivamente, creiamo due implementazioni concrete dell’interfaccia Observer
per gestire le notifiche via email e SMS.
Classe EmailObserver
public class EmailObserver implements Observer {
@Override
public void update(String event) {
System.out.println("Email sent for event: " + event);
}
}
L’EmailObserver
gestisce l’invio di notifiche via email ogni volta che viene notificato di un evento.
Classe SMSObserver
public class SMSObserver implements Observer {
@Override
public void update(String event) {
System.out.println("SMS sent for event: " + event);
}
}
Il SMSObserver
gestisce l’invio di notifiche SMS ogni volta che viene notificato.
Passo 3: Creare una classe UserService
(Il Soggetto)
Ora creeremo una classe UserService
che funge da soggetto, notificando i suoi osservatori registrati ogni volta che un utente si registra.
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
private List<Observer> observers = new ArrayList<>();
// Metodo per registrare gli osservatori
public void registerObserver(Observer observer) {
observers.add(observer);
}
// Metodo per notificare tutti gli osservatori registrati di un evento
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
// Metodo per registrare un nuovo utente e notificare gli osservatori
public void registerUser(String username) {
System.out.println("User registered: " + username);
notifyObservers("User Registration");
}
}
-
Lista Osservatori: Tieni traccia di tutti gli osservatori registrati.
-
registerObserver()
Metodo: Aggiunge nuovi osservatori alla lista. -
notifyObservers()
Metodo: Notifica tutti gli osservatori registrati quando si verifica un evento. -
registerUser()
Metodo: Registra un nuovo utente e attiva le notifiche a tutti gli osservatori.
Passo 4: Utilizzare il Pattern Observer in un Controller
Infine, creeremo un controller Spring Boot per esporre un endpoint per la registrazione dell’utente. Questo controller registrerà sia EmailObserver
che SMSObserver
con il UserService
.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController() {
this.userService = new UserService();
// Registra gli osservatori
userService.registerObserver(new EmailObserver());
userService.registerObserver(new SMSObserver());
}
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestParam String username) {
userService.registerUser(username);
return ResponseEntity.ok("User registered and notifications sent!");
}
}
-
Endpoint (
/register
): Accetta un parametrousername
e registra l’utente, attivando le notifiche a tutti gli osservatori. -
Osservatori: Sia
EmailObserver
cheSMSObserver
sono registrati conUserService
, quindi vengono notificati ogni volta che un utente si registra.
Testare il Pattern Observer
Adesso, testiamo la nostra implementazione usando Postman o un browser:
POST http://localhost:8080/api/register?username=JohnDoe
Output Atteso in Console:
User registered: JohnDoe
Email sent for event: User Registration
SMS sent for event: User Registration
Il sistema registra l’utente e notifica sia gli osservatori Email che SMS, mostrando la flessibilità del pattern Observer.
Applicazioni del Pattern Observer nel Mondo Reale
-
Sistemi di Notifica: Inviare aggiornamenti agli utenti tramite canali diversi (email, SMS, notifiche push) quando si verificano determinati eventi.
-
Architetture Basate su Eventi: Notificare più sottosistemi quando si verificano azioni specifiche, come attività degli utenti o avvisi di sistema.
-
Streaming di Dati: Trasmettere le modifiche ai dati a vari consumatori in tempo reale (ad esempio, prezzi delle azioni in diretta o feed dei social media).
Come Utilizzare l’Iniezione delle Dipendenze di Spring Boot
Finora, abbiamo creato manualmente oggetti per dimostrare i modelli di design. Tuttavia, nelle applicazioni reali di Spring Boot, l’Iniezione delle Dipendenze (DI) è il modo preferito per gestire la creazione degli oggetti. La DI consente a Spring di gestire automaticamente l’instanziazione e il cablaggio delle tue classi, rendendo il tuo codice più modulare, testabile e manutenibile.
Rifattorizziamo il nostro esempio di modello Strategia per sfruttare le potenti capacità di DI di Spring Boot. Questo ci permetterà di passare dinamicamente tra le strategie di pagamento, utilizzando le annotazioni di Spring per gestire le dipendenze.
Modello Strategia Aggiornato Utilizzando la DI di Spring Boot
Nel nostro esempio refattorizzato, sfrutteremo le annotazioni di Spring come @Component
, @Service
e @Autowired
per semplificare il processo di iniezione delle dipendenze.
Passo 1: Annotare le Strategie di Pagamento con @Component
Prima di tutto, contrassegniamo le implementazioni delle nostre strategie con l’annotazione @Component
in modo che Spring possa rilevarle e gestirle automaticamente.
@Component("creditCardPayment")
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with Credit Card");
}
}
@Component("payPalPayment")
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal");
}
}
-
@Component
Annotation: Aggiungendo@Component
, diciamo a Spring di trattare queste classi come bean gestiti da Spring. Il valore di stringa ("creditCardPayment"
e"payPalPayment"
) funge da identificatore del bean. -
Flessibilità: Questa configurazione ci consente di passare tra le strategie usando l’identificatore del bean appropriato.
Passo 2: Refattorizzare il PaymentService
per Utilizzare l’Iniezione delle Dipendenze
Successivamente, modifichiamo il PaymentService
per iniettare una strategia di pagamento specifica utilizzando @Autowired
e @Qualifier
.
@Service
public class PaymentService {
private final PaymentStrategy paymentStrategy;
@Autowired
public PaymentService(@Qualifier("payPalPayment") PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
paymentStrategy.pay(amount);
}
}
-
Annotation
@Service
: MarcaPaymentService
come un bean di servizio gestito da Spring. -
@Autowired
: Spring inietta automaticamente la dipendenza richiesta. -
@Qualifier
: Specifica quale implementazione diPaymentStrategy
iniettare. In questo esempio, stiamo usando"payPalPayment"
. -
Semplicità di configurazione: Cambiando semplicemente il valore di
@Qualifier
, è possibile cambiare la strategia di pagamento senza alterare la logica di business.
Passo 3: Utilizzare il servizio rifattorizzato in un controller
Per vedere i benefici di questo refactoring, aggiorniamo il controller per utilizzare il nostro PaymentService
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class PaymentController {
private final PaymentService paymentService;
@Autowired
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@GetMapping("/pay")
public String makePayment(@RequestParam double amount) {
paymentService.processPayment(amount);
return "Payment processed using the current strategy!";
}
}
-
@Autowired
: Il controller riceve automaticamente ilPaymentService
con la strategia di pagamento iniettata. - Endpoint GET (
/pay
): Quando viene accesso, elabora un pagamento utilizzando la strategia configurata al momento (PayPal in questo esempio).
Testing del Refactored Strategy Pattern con DI
Ora, testiamo la nuova implementazione utilizzando Postman o un browser:
GET http://localhost:8080/api/pay?amount=100
Output Atteso:
Paid $100.0 using PayPal
Se si cambia il qualificatore in PaymentService
in "pagamentoConCartaDiCredito"
, l’output cambierà di conseguenza:
Paid $100.0 with Credit Card
Vantaggi dell’Utilizzo dell’Iniezione delle Dipendenze
-
Accoppiamento Lento: Il servizio e il controller non hanno bisogno di conoscere i dettagli su come viene elaborato un pagamento. Si affidano semplicemente a Spring per iniettare l’implementazione corretta.
-
Modularità: È possibile aggiungere facilmente nuovi metodi di pagamento (ad esempio,
PagamentoTrasferimentoBancario
,PagamentoCrypto
) creando nuove classi annotate con@Component
e regolando il@Qualifier
. - Configurabilità: Sfruttando i Profili di Spring, è possibile cambiare le strategie in base all’ambiente (ad esempio, sviluppo vs. produzione).
Esempio: È possibile utilizzare @Profile
per iniettare automaticamente diverse strategie in base al profilo attivo:
@Component
@Profile("dev")
public class DevPaymentStrategy implements PaymentStrategy { /* ... */ }
@Component
@Profile("prod")
public class ProdPaymentStrategy implements PaymentStrategy { /* ... */ }
Concetti chiave
-
Utilizzando l’Iniezione delle Dipendenze di Spring Boot, è possibile semplificare la creazione degli oggetti e migliorare la flessibilità del codice.
-
Il Pattern Strategia combinato con l’Iniezione delle Dipendenze consente di passare facilmente tra diverse strategie senza modificare la logica di base del business.
-
Utilizzando
@Qualifier
e i Profili di Spring, si ottiene la flessibilità di configurare l’applicazione in base a diversi ambienti o requisiti.
Questo approccio non solo rende il codice più pulito, ma lo prepara anche per configurazioni più avanzate e scalabilità in futuro. Nella prossima sezione, esploreremo le Migliori Pratiche e i Consigli sull’ottimizzazione per portare le tue applicazioni Spring Boot al livello successivo.
Migliori Pratiche e Consigli sull’ottimizzazione
Pratiche Migliori Generali
-
Non sovraccaricare i modelli: Usali solo quando necessario. L’overengineering può rendere il tuo codice più difficile da mantenere.
-
Favorisci la composizione rispetto all’ereditarietà: Modelli come Strategy e Observer sono ottimi esempi di questo principio.
-
Mantieni i tuoi modelli flessibili: Sfrutta le interfacce per mantenere il tuo codice disaccoppiato.
Considerazioni sulle Prestazioni
-
Modello Singleton: Assicura la sicurezza dei thread utilizzando
synchronized
o ilBill Pugh Singleton Design
. -
Modello Factory: Memorizza in cache gli oggetti se sono costosi da creare.
-
Modello Observer: Usa l’elaborazione asincrona se hai molti osservatori per evitare il blocco.
Argomenti Avanzati
-
Utilizzare Reflection con il pattern Factory per il caricamento dinamico delle classi.
-
Sfruttare i Profili Spring per cambiare strategie in base all’ambiente.
-
Aggiungere la Documentazione Swagger per i tuoi endpoint API.
Conclusioni e Concetti Chiave
In questo tutorial, abbiamo esplorato alcuni dei design pattern più potenti – Singleton, Factory, Strategy e Observer – e dimostrato come implementarli in Spring Boot. Riassumiamo brevemente ogni pattern e evidenziamo per cosa è più adatto:
Pattern Singleton:
-
Riassunto: Garantisce che una classe abbia una sola istanza e fornisce un punto di accesso globale ad essa.
-
Ideale Per: Gestire risorse condivise come impostazioni di configurazione, connessioni al database o servizi di logging. È ideale quando si desidera controllare l’accesso a un’istanza condivisa in tutta l’applicazione.
Pattern Factory:
-
Riepilogo: Fornisce un modo per creare oggetti senza specificare la classe esatta da istanziare. Questo modello separa la creazione dell’oggetto dalla logica aziendale.
-
Ideale Per: Scenari in cui è necessario creare diversi tipi di oggetti in base alle condizioni di input, come l’invio di notifiche tramite email, SMS o notifiche push. È ottimo per rendere il codice più modulare ed estendibile.
Modello Strategia:
-
Riepilogo: Consente di definire una famiglia di algoritmi, incapsularne ciascuno e renderli interscambiabili. Questo modello ti aiuta a scegliere un algoritmo a tempo di esecuzione.
-
Ideale Per: Quando è necessario passare dinamicamente tra comportamenti o algoritmi diversi, come il processamento di vari metodi di pagamento in un’applicazione di e-commerce. Mantiene il codice flessibile e aderisce al Principio Aperto/Chiuso.
Pattern Observer:
-
Riepilogo: Definisce una dipendenza uno-a-molti tra gli oggetti in modo che quando uno stato di un oggetto cambia, tutti i suoi dipendenti vengano notificati automaticamente.
-
Migliore Per: Sistemi basati sugli eventi come servizi di notifica, aggiornamenti in tempo reale in app di chat o sistemi che devono reagire ai cambiamenti nei dati. È ideale per disaccoppiare i componenti e rendere il sistema più scalabile.
Cosa Fare Dopo?
Ora che hai imparato questi pattern di progettazione essenziali, prova ad integrarli nei tuoi progetti esistenti per vedere come possono migliorare la struttura del tuo codice e la scalabilità. Ecco alcuni suggerimenti per approfondire ulteriormente:
-
Sperimenta: Prova ad implementare altri pattern di progettazione come Decorator, Proxy, e Builder per ampliare il tuo set di strumenti.
-
Pratica: Utilizza questi pattern per ristrutturare progetti esistenti e migliorarne la manutenibilità.
-
Condividi: Se hai domande o desideri condividere la tua esperienza, non esitare a contattarci!
Spero che questa guida ti abbia aiutato a capire come utilizzare in modo efficace i design pattern in Java. Continua a sperimentare e buon coding!
Source:
https://www.freecodecamp.org/news/how-to-use-design-patterns-in-java-with-spring-boot/