Mit zunehmender Größe von Softwareprojekten wird es immer wichtiger, Ihren Code organisiert, wartbar und skalierbar zu halten. Hier kommen Designmuster ins Spiel. Designmuster bieten bewährte, wiederverwendbare Lösungen für häufige Herausforderungen im Softwaredesign und machen Ihren Code effizienter und leichter zu verwalten.
In diesem Leitfaden werden wir tief in einige der beliebtesten Designmuster eintauchen und Ihnen zeigen, wie Sie sie in Spring Boot implementieren können. Am Ende werden Sie diese Muster nicht nur konzeptionell verstehen, sondern sie auch mit Zuversicht in Ihren eigenen Projekten anwenden können.
Inhaltsverzeichnis
Einführung in Entwurfsmuster
Design Patterns sind wiederverwendbare Lösungen für gängige Software-Designprobleme. Denken Sie an sie als bewährte Verfahren, die in Vorlagen destilliert sind und auf spezifische Herausforderungen in Ihrem Code angewendet werden können. Sie sind nicht auf eine bestimmte Sprache beschränkt, können jedoch aufgrund ihrer objektorientierten Natur besonders mächtig in Java sein.
In diesem Leitfaden werden wir behandeln:
-
Singleton-Muster: Sicherstellen, dass eine Klasse nur eine Instanz hat.
-
Factory-Muster: Objekte erstellen, ohne die genaue Klasse anzugeben.
-
Strategie-Muster: Auswahl von Algorithmen zur Laufzeit ermöglichen.
-
Beobachter-Muster: Einrichten einer Publish-Subscribe-Beziehung.
Wir werden nicht nur erläutern, wie diese Muster funktionieren, sondern auch untersuchen, wie sie in Spring Boot für Anwendungen in der realen Welt angewendet werden können.
So richten Sie Ihr Spring Boot-Projekt ein
Bevor wir uns mit den Mustern beschäftigen, richten wir ein Spring Boot-Projekt ein:
Voraussetzungen
Vergewissern Sie sich, dass Sie folgendes haben:
-
Java 11+
-
Maven
-
Spring Boot CLI (optional)
-
Postman oder curl (zum Testen)
Projektinitialisierung
Sie können schnell ein Spring Boot-Projekt mithilfe von Spring Initializr erstellen:
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
Was ist das Singleton-Muster?
Das Singleton-Muster stellt sicher, dass eine Klasse nur eine Instanz hat und einen globalen Zugriffspunkt darauf bereitstellt. Dieses Muster wird häufig für Dienste wie Protokollierung, Konfigurationsverwaltung oder Datenbankverbindungen verwendet.
Wie implementiert man das Singleton-Muster in Spring Boot
Spring Boot-Beans sind standardmäßig Singletons, was bedeutet, dass Spring automatisch den Lebenszyklus dieser Beans verwaltet, um sicherzustellen, dass nur eine Instanz existiert. Es ist jedoch wichtig zu verstehen, wie das Singleton-Muster unter der Oberfläche funktioniert, insbesondere wenn Sie nicht von Spring verwaltete Beans verwenden oder mehr Kontrolle über die Instanzverwaltung benötigen.
Lassen Sie uns eine manuelle Implementierung des Singleton-Musters durchgehen, um zu demonstrieren, wie Sie die Erstellung einer einzelnen Instanz innerhalb Ihrer Anwendung steuern können.
Schritt 1: Erstellen Sie eine LoggerService
-Klasse
In diesem Beispiel erstellen wir einen einfachen Protokollierungsdienst unter Verwendung des Singleton-Musters. Das Ziel ist es, sicherzustellen, dass alle Teile der Anwendung dieselbe Protokollierungsinstanz verwenden.
public class LoggerService {
// Die statische Variable, um die einzelne Instanz zu halten
private static LoggerService instance;
// Privater Konstruktor, um die Instanziierung von außen zu verhindern
private LoggerService() {
// Dieser Konstruktor ist absichtlich leer, um zu verhindern, dass andere Klassen Instanzen erstellen
}
// Öffentliche Methode, um Zugriff auf die einzelne Instanz zu ermöglichen
public static synchronized LoggerService getInstance() {
if (instance == null) {
instance = new LoggerService();
}
return instance;
}
// Beispiel-Logging-Methode
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
-
Statische Variable (
Instanz
): Diese hält die einzelne Instanz vonLoggerService
. -
Privater Konstruktor: Der Konstruktor ist als privat markiert, um zu verhindern, dass andere Klassen direkt neue Instanzen erstellen.
-
Synchronisierte
getInstance()
-Methode: Die Methode ist synchronisiert, um sie thread-sicher zu machen und sicherzustellen, dass nur eine Instanz erstellt wird, auch wenn mehrere Threads gleichzeitig darauf zugreifen. -
Lazy Initialization: Die Instanz wird nur erstellt, wenn sie zum ersten Mal angefordert wird (
lazy initialization
), was in Bezug auf den Speicherverbrauch effizient ist.
Real-World Usage: Dieses Muster wird häufig für gemeinsam genutzte Ressourcen wie Protokollierung, Konfigurationseinstellungen oder das Verwalten von Datenbankverbindungen verwendet, wenn Sie den Zugriff steuern und sicherstellen möchten, dass nur eine Instanz in Ihrer Anwendung verwendet wird.
Schritt 2: Verwenden Sie das Singleton in einem Spring Boot Controller
Jetzt sehen wir, wie wir unseren Singleton LoggerService
innerhalb eines Spring Boot Controllers verwenden können. Dieser Controller wird einen Endpunkt bereitstellen, der eine Nachricht protokolliert, wann immer darauf zugegriffen wird.
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() {
// Zugriff auf die Singleton-Instanz von LoggerService
LoggerService logger = LoggerService.getInstance();
logger.log("This is a log message!");
return ResponseEntity.ok("Message logged successfully");
}
}
-
GET-Endpunkt: Wir haben einen
/log
-Endpunkt erstellt, der beim Zugriff eine Nachricht mithilfe desLoggerService
protokolliert. -
Singleton-Nutzung: Anstatt eine neue Instanz von
LoggerService
zu erstellen, rufen wirgetInstance()
auf, um sicherzustellen, dass wir jedes Mal dieselbe Instanz verwenden. -
Antwort: Nach dem Protokollieren gibt der Endpunkt eine Antwort zurück, die den Erfolg anzeigt.
Schritt 3: Testen des Singleton-Musters
Jetzt testen wir diesen Endpunkt mit Postman oder Ihrem Browser:
GET http://localhost:8080/log
Erwartete Ausgabe:
-
Konsolenausgabe:
[LOG] Dies ist eine Protokollmeldung!
-
HTTP-Antwort:
Nachricht erfolgreich protokolliert
Sie können den Endpunkt mehrmals aufrufen und sehen, dass dieselbe Instanz von LoggerService
verwendet wird, wie durch die konsistente Protokollausgabe angezeigt.
Praktische Anwendungsfälle für das Singleton-Muster
Hier ist, wann Sie das Singleton-Muster in realen Anwendungen verwenden könnten:
-
Konfigurationsverwaltung: Stellen Sie sicher, dass Ihre Anwendung eine konsistente Reihe von Konfigurationseinstellungen verwendet, insbesondere wenn diese Einstellungen aus Dateien oder Datenbanken geladen werden.
-
Datenbank-Verbindungspools: Steuern den Zugriff auf eine begrenzte Anzahl von Datenbankverbindungen, um sicherzustellen, dass derselbe Pool in der Anwendung gemeinsam genutzt wird.
-
Caching: Pflegen eine einzelne Cache-Instanz, um inkonsistente Daten zu vermeiden.
-
Logging-Services: Verwenden Sie, wie in diesem Beispiel gezeigt, einen einzelnen Logging-Service, um Log-Ausgaben über verschiedene Module Ihrer Anwendung zu zentralisieren.
Schlüsselerkenntnisse
-
Das Singleton-Muster ist ein einfacher Weg, um sicherzustellen, dass nur eine Instanz einer Klasse erstellt wird.
-
Die Gewährleistung der Thread-Sicherheit ist entscheidend, wenn mehrere Threads auf das Singleton zugreifen, weshalb wir in unserem Beispiel
synchronized
verwendet haben. - Spring Boot-Beans sind standardmäßig bereits Singletons, aber das Verständnis, wie man es manuell implementiert, hilft Ihnen, mehr Kontrolle zu erlangen, wenn es erforderlich ist.
Dies umfasst die Implementierung und Verwendung des Singleton-Musters. Als Nächstes werden wir das Factory-Muster erkunden, um zu sehen, wie es helfen kann, die Objekterstellung zu optimieren.
Was ist das Factory-Muster?
Das Factory-Muster ermöglicht es Ihnen, Objekte zu erstellen, ohne die genaue Klasse anzugeben. Dieses Muster ist nützlich, wenn Sie verschiedene Arten von Objekten haben, die basierend auf einer Eingabe instanziiert werden müssen.
Wie man eine Factory in Spring Boot implementiert
Das Factory-Muster ist unglaublich nützlich, wenn Sie Objekte basierend auf bestimmten Kriterien erstellen müssen, aber den Objekterstellungsprozess von Ihrer Hauptanwendungslogik entkoppeln möchten.
In diesem Abschnitt werden wir den Aufbau einer NotificationFactory
durchgehen, um Benachrichtigungen per E-Mail oder SMS zu senden. Dies ist besonders nützlich, wenn Sie erwarten, zukünftig weitere Benachrichtigungstypen hinzuzufügen, wie beispielsweise Push-Benachrichtigungen oder In-App-Benachrichtigungen, ohne Ihren bestehenden Code zu ändern.
Schritt 1: Erstellen Sie das Notification
Interface
Der erste Schritt besteht darin, eine gemeinsame Schnittstelle zu definieren, die alle Benachrichtigungstypen implementieren werden. Dies stellt sicher, dass jeder Benachrichtigungstyp (E-Mail, SMS usw.) eine konsistente send()
-Methode hat.
public interface Notification {
void send(String message);
}
-
Zweck: Die
Notification
-Schnittstelle definiert den Vertrag für das Senden von Benachrichtigungen. Jede Klasse, die diese Schnittstelle implementiert, muss eine Implementierung für diesend()
-Methode bereitstellen. -
Skalierbarkeit: Durch die Verwendung einer Schnittstelle können Sie Ihre Anwendung in Zukunft problemlos um andere Arten von Benachrichtigungen erweitern, ohne den bestehenden Code zu ändern.
Schritt 2: Implementieren Sie EmailBenachrichtigung
und SMSBenachrichtigung
Jetzt implementieren wir zwei konkrete Klassen, eine zum Senden von E-Mails und eine andere zum Senden von SMS-Nachrichten.
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);
}
}
Schritt 3: Erstellen Sie eine BenachrichtigungsFabrik
Die Klasse BenachrichtigungsFabrik
ist dafür verantwortlich, Instanzen von Benachrichtigung
basierend auf dem angegebenen Typ zu erstellen. Dieses Design stellt sicher, dass der BenachrichtigungsController
nichts über die Details der Objekterstellung wissen muss.
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-Methode (createBenachrichtigung()
):
-
Die Factory-Methode nimmt einen String (
typ
) als Eingabe und gibt eine Instanz der entsprechenden Benachrichtigungsklasse zurück. -
Switch-Anweisung: Die Switch-Anweisung wählt den entsprechenden Benachrichtigungstyp basierend auf der Eingabe aus.
-
Fehlerbehandlung: Wenn der bereitgestellte Typ nicht erkannt wird, wird eine
IllegalArgumentException
ausgelöst. Dies stellt sicher, dass ungültige Typen frühzeitig erkannt werden.
Warum eine Factory verwenden?
-
Entkopplung: Das Factory-Pattern entkoppelt die Objekterstellung von der Geschäftslogik. Dadurch wird Ihr Code modularer und einfacher zu warten.
-
Erweiterbarkeit: Wenn Sie einen neuen Benachrichtigungstyp hinzufügen möchten, müssen Sie nur die Factory aktualisieren, ohne die Controller-Logik zu ändern.
Schritt 4: Verwenden Sie die Factory in einem Spring Boot Controller
Jetzt setzen wir alles zusammen, indem wir einen Spring Boot Controller erstellen, der die NotificationFactory
verwendet, um Benachrichtigungen basierend auf der Anfrage des Benutzers zu senden.
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 {
// Erstellen Sie das entsprechende Benachrichtigungsobjekt mithilfe der Factory
Notification notification = NotificationFactory.createNotification(type);
notification.send(message);
return ResponseEntity.ok("Notification sent successfully!");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
GET-Endpunkt (/notify
):
-
Der Controller stellt einen
/notify
Endpunkt bereit, der zwei Abfrageparameter akzeptiert:type
(entweder „EMAIL“ oder „SMS“) undmessage
. -
Er verwendet die
NotificationFactory
, um den entsprechenden Benachrichtigungstyp zu erstellen und die Nachricht zu senden. -
Fehlerbehandlung: Wenn ein ungültiger Benachrichtigungstyp bereitgestellt wird, fängt der Controller die
IllegalArgumentException
ab und gibt eine400 Bad Request
Antwort zurück.
Schritt 5: Testen des Factory Patterns
Testen wir den Endpunkt mit Postman oder einem Browser:
-
Eine E-Mail-Benachrichtigung senden:
GET http://localhost:8080/notify?type=email&message=Hello%20Email
Ausgabe:
Sende E-Mail: Hello Email
-
SMS-Benachrichtigung senden:
GET http://localhost:8080/notify?type=sms&message=Hello%20SMS
Ausgabe:
SMS senden: Hallo SMS
-
Test mit ungültigem Typ:
GET http://localhost:8080/notify?type=unknown&message=Test
Ausgabe:
Schlechte Anforderung: Unbekannter Benachrichtigungstyp: unknown
Praxisbeispiele für den Fabrikmuster
Das Fabrikmuster ist besonders nützlich in Szenarien, in denen:
-
Dynamische Objekterstellung: Wenn Sie Objekte basierend auf Benutzereingaben erstellen müssen, wie das Senden verschiedener Arten von Benachrichtigungen, das Generieren von Berichten in verschiedenen Formaten oder das Behandeln unterschiedlicher Zahlungsmethoden.
-
Entkopplung der Objekterstellung: Durch die Verwendung eines Fabrikmusters können Sie Ihre Hauptgeschäftslogik von der Objekterstellung trennen und Ihren Code wartungsfähiger machen.
-
Skalierbarkeit: Erweitern Sie Ihre Anwendung problemlos, um neue Arten von Benachrichtigungen zu unterstützen, ohne den vorhandenen Code zu ändern. Fügen Sie einfach eine neue Klasse hinzu, die das
Benachrichtigungs
-Interface implementiert, und aktualisieren Sie die Fabrik.
Was ist das Strategiemuster?
Das Strategiemuster ist perfekt, wenn Sie dynamisch zwischen mehreren Algorithmen oder Verhaltensweisen wechseln müssen. Es ermöglicht Ihnen, eine Familie von Algorithmen zu definieren, jeden in separaten Klassen zu kapseln und sie leicht austauschbar zur Laufzeit zu machen. Dies ist besonders nützlich, um einen Algorithmus basierend auf spezifischen Bedingungen auszuwählen und Ihren Code sauber, modular und flexibel zu halten.
Praxisbeispiel aus der realen Welt: Stellen Sie sich ein E-Commerce-System vor, das mehrere Zahlungsoptionen unterstützen muss, wie Kreditkarten, PayPal oder Banküberweisungen. Durch die Verwendung des Strategie-Musters können Sie Zahlungsmethoden einfach hinzufügen oder ändern, ohne den bestehenden Code zu ändern. Dieser Ansatz stellt sicher, dass Ihre Anwendung skalierbar und wartbar bleibt, wenn Sie neue Funktionen einführen oder bestehende aktualisieren.
Wir werden dieses Muster anhand eines Spring Boot-Beispiels demonstrieren, das Zahlungen entweder mit einer Kreditkarte oder einer PayPal-Strategie abwickelt.
Schritt 1: Definieren Sie ein PaymentStrategy
-Interface
Wir beginnen damit, eine gemeinsame Schnittstelle zu erstellen, die alle Zahlungsstrategien implementieren werden:
public interface PaymentStrategy {
void pay(double amount);
}
Die Schnittstelle definiert einen Vertrag für alle Zahlungsmethoden und gewährleistet so eine Konsistenz in den Implementierungen.
Schritt 2: Implementieren Sie Zahlungsstrategien
Erstellen Sie konkrete Klassen für Kreditkarten- und PayPal-Zahlungen.
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");
}
}
Jede Klasse implementiert die Methode pay()
mit ihrem spezifischen Verhalten.
Schritt 3: Verwenden Sie die Strategie in einem Controller
Erstellen Sie einen Controller, um dynamisch eine Zahlungsstrategie basierend auf der Benutzereingabe auszuwählen:
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;
}
}
}
Der Endpunkt akzeptiert Methode
und Betrag
als Abfrageparameter und verarbeitet die Zahlung mit der entsprechenden Strategie.
Schritt 4: Testen des Endpunkts
-
Kreditkartenzahlung:
GET http://localhost:8080/zahlen?methode=kreditkarte&betrag=100
Ausgabe:
Bezahlt $100.0 mit Kreditkarte
-
PayPal-Zahlung:
GET http://localhost:8080/zahlen?methode=paypal&betrag=50
Ausgabe:
Bezahlt $50.0 via PayPal
-
Ungültige Methode:
GET http://localhost:8080/zahlen?methode=bitcoin&betrag=25
Ausgabe:
Ungültige Zahlungsmethode
Anwendungsfälle für das Strategie-Muster
-
Zahlungsabwicklung: Dynamisches Umschalten zwischen verschiedenen Zahlungsgateways.
-
Sortieralgorithmen: Wählen Sie die beste Sortiermethode basierend auf der Datenmenge.
-
Dateiexport: Exportieren von Berichten in verschiedenen Formaten (PDF, Excel, CSV).
Schlüsselerkenntnisse
-
Das Strategie-Muster hält Ihren Code modular und folgt dem Open/Closed-Prinzip.
-
Das Hinzufügen neuer Strategien ist einfach – erstellen Sie einfach eine neue Klasse, die das
PaymentStrategy
-Interface implementiert. -
Es ist ideal für Szenarien, in denen Sie eine flexible Algorithmusauswahl zur Laufzeit benötigen.
Als nächstes werden wir das Beobachtermuster erkunden, das perfekt für die Verarbeitung ereignisgesteuerter Architekturen geeignet ist.
Was ist das Beobachtermuster?
Das Beobachter-Muster ist ideal, wenn Sie ein Objekt (das Subjekt) haben, das mehrere andere Objekte (Beobachter) über Änderungen in seinem Zustand benachrichtigen muss. Es eignet sich perfekt für ereignisgesteuerte Systeme, in denen Aktualisierungen an verschiedene Komponenten weitergeleitet werden müssen, ohne eine starke Kopplung zwischen ihnen zu erzeugen. Dieses Muster ermöglicht es Ihnen, eine saubere Architektur aufrechtzuerhalten, insbesondere wenn verschiedene Teile Ihres Systems unabhängig auf Änderungen reagieren müssen.
Praxisbeispiel: Dieses Muster wird häufig in Systemen verwendet, die Benachrichtigungen oder Warnungen senden, wie z. B. Chat-Anwendungen oder Aktienkursverfolger, bei denen Aktualisierungen in Echtzeit an Benutzer weitergeleitet werden müssen. Durch die Verwendung des Beobachtermusters können Sie Benachrichtigungstypen problemlos hinzufügen oder entfernen, ohne die Kernlogik zu ändern.
Wir werden zeigen, wie man dieses Muster in Spring Boot implementiert, indem man ein einfaches Benachrichtigungssystem erstellt, bei dem sowohl E-Mail- als auch SMS-Benachrichtigungen gesendet werden, wenn sich ein Benutzer registriert.
Schritt 1: Erstellen Sie ein Observer
-Interface
Wir beginnen damit, ein gemeinsames Interface zu definieren, das alle Beobachter implementieren werden:
public interface Observer {
void update(String event);
}
Das Interface legt einen Vertrag fest, nach dem alle Beobachter die Methode update()
implementieren müssen, die ausgelöst wird, wenn sich das Subjekt ändert.
Schritt 2: Implementieren Sie EmailObserver
und SMSObserver
Als nächstes erstellen wir zwei konkrete Implementierungen des Observer
-Interfaces, um E-Mail- und SMS-Benachrichtigungen zu behandeln.
EmailObserver
-Klasse
public class EmailObserver implements Observer {
@Override
public void update(String event) {
System.out.println("Email sent for event: " + event);
}
}
Der EmailObserver
kümmert sich um das Senden von E-Mail-Benachrichtigungen, wann immer er über ein Ereignis informiert wird.
Klasse SMSObserver
public class SMSObserver implements Observer {
@Override
public void update(String event) {
System.out.println("SMS sent for event: " + event);
}
}
Der SMSObserver
behandelt das Senden von SMS-Benachrichtigungen, immer wenn er benachrichtigt wird.
Schritt 3: Erstellen einer Klasse UserService
(Das Subjekt)
Jetzt erstellen wir eine Klasse UserService
, die als Subjekt fungiert und ihre registrierten Beobachter benachrichtigt, wann immer sich ein Benutzer registriert.
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
private List<Observer> observers = new ArrayList<>();
// Methode zur Registrierung von Beobachtern
public void registerObserver(Observer observer) {
observers.add(observer);
}
// Methode zur Benachrichtigung aller registrierten Beobachter über ein Ereignis
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
// Methode zur Registrierung eines neuen Benutzers und Benachrichtigung der Beobachter
public void registerUser(String username) {
System.out.println("User registered: " + username);
notifyObservers("User Registration");
}
}
-
Liste der Beobachter: Hält alle registrierten Beobachter fest.
-
registerObserver()
Methode: Fügt neue Beobachter zur Liste hinzu. -
notifyObservers()
Methode: Benachrichtigt alle registrierten Beobachter, wenn ein Ereignis eintritt. -
registerUser()
Methode: Registriert einen neuen Benutzer und löst Benachrichtigungen an alle Beobachter aus.
Schritt 4: Verwenden des Beobachtermusters in einem Controller
Schließlich werden wir einen Spring Boot Controller erstellen, um einen Endpunkt für die Benutzerregistrierung freizugeben. Dieser Controller wird sowohl den EmailObserver
als auch den SMSObserver
mit dem UserService
registrieren.
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();
// Registriere Beobachter
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!");
}
}
-
Endpunkt (
/register
): Akzeptiert einenBenutzernamen
-Parameter und registriert den Benutzer, wodurch Benachrichtigungen an alle Beobachter ausgelöst werden. -
Beobachter: Sowohl
EmailObserver
als auchSMSObserver
sind beimUserService
registriert, sodass sie benachrichtigt werden, wenn ein Benutzer sich registriert.
Testen des Beobachter-Musters
Jetzt testen wir unsere Implementierung mit Postman oder einem Browser:
POST http://localhost:8080/api/register?username=JohnDoe
Erwartete Ausgabe in der Konsole:
User registered: JohnDoe
Email sent for event: User Registration
SMS sent for event: User Registration
Das System registriert den Benutzer und benachrichtigt sowohl die Email- als auch die SMS-Beobachter und zeigt die Flexibilität des Beobachter-Musters.
Praktische Anwendungen des Beobachter-Musters
-
Benachrichtigungssysteme: Senden von Updates an Benutzer über verschiedene Kanäle (E-Mail, SMS, Push-Benachrichtigungen), wenn bestimmte Ereignisse eintreten.
-
Ereignisgesteuerte Architekturen: Benachrichtigung mehrerer Teilsysteme, wenn spezifische Aktionen stattfinden, wie Benutzeraktivitäten oder Systemwarnungen.
-
Datenstrom: Übertragung von Datenänderungen an verschiedene Verbraucher in Echtzeit (zum Beispiel Echtzeit-Aktienkurse oder Social-Media-Feeds).
Wie man die Dependency Injection von Spring Boot verwendet
Bisher haben wir Objekte manuell erstellt, um Designmuster zu demonstrieren. In realen Spring Boot-Anwendungen ist jedoch die Dependency Injection (DI) der bevorzugte Weg, um die Objekterstellung zu verwalten. DI ermöglicht es Spring, die Instanziierung und Verdrahtung Ihrer Klassen automatisch zu handhaben, wodurch Ihr Code modularer, testbarer und wartbarer wird.
Lassen Sie uns unser Beispiel für das Strategiemuster umgestalten, um von den leistungsstarken DI-Fähigkeiten von Spring Boot zu profitieren. Dadurch können wir dynamisch zwischen Zahlungsstrategien wechseln, indem wir Spring-Annotationen verwenden, um Abhängigkeiten zu verwalten.
Aktualisiertes Strategiemuster unter Verwendung von Spring Boots DI
In unserem refaktorisierten Beispiel werden wir Spring-Annotationen wie @Component
, @Service
und @Autowired
nutzen, um den Prozess des Injizierens von Abhängigkeiten zu optimieren.
Schritt 1: Die Zahlungsstrategien mit @Component
annotieren
Zunächst kennzeichnen wir unsere Strategie-Implementierungen mit der @Component
-Annotation, damit Spring sie automatisch erkennen und verwalten kann.
@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: Durch das Hinzufügen von@Component
teilen wir Spring mit, dass diese Klassen als von Spring verwaltete Beans behandelt werden sollen. Der Zeichenfolgenwert ("creditCardPayment"
und"payPalPayment"
) fungiert als Bean-Identifier. -
Flexibilität: Diese Konfiguration ermöglicht es uns, zwischen Strategien zu wechseln, indem wir den entsprechenden Bean-Identifier verwenden.
Schritt 2: Das PaymentService
zur Verwendung der Dependency Injection umstrukturieren
Als Nächstes passen wir den PaymentService
an, um eine spezifische Zahlungsstrategie mithilfe von @Autowired
und @Qualifier
einzufügen.
@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);
}
}
-
@Service
-Annotation: MarkiertPaymentService
als von Spring verwalteten Service-Bean. -
@Autowired
: Spring injiziert automatisch die erforderliche Abhängigkeit. -
@Qualifier
: Legt fest, welche Implementierung vonPaymentStrategy
injiziert werden soll. In diesem Beispiel verwenden wir"payPalPayment"
. -
Konfigurationskomfort: Durch einfaches Ändern des Werts von
@Qualifier
können Sie die Zahlungsstrategie wechseln, ohne die Geschäftslogik zu ändern.
Schritt 3: Verwendung des refaktorierten Service in einem Controller
Um die Vorteile dieser Refaktorisierung zu sehen, aktualisieren wir den Controller, um unseren PaymentService
zu verwenden:
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
: Der Controller empfängt automatisch denPaymentService
mit der eingefügten Zahlungsstrategie. -
GET-Endpunkt (
/zahlen
): Wenn darauf zugegriffen wird, verarbeitet er eine Zahlung mit der aktuell konfigurierten Strategie (PayPal in diesem Beispiel).
Testen des refaktorisierten Strategiemusters mit DI
Jetzt testen wir die neue Implementierung mit Postman oder einem Browser:
GET http://localhost:8080/api/pay?amount=100
Erwartete Ausgabe:
Paid $100.0 using PayPal
Wenn Sie den Qualifier in PaymentService
auf "creditCardPayment"
ändern, ändert sich die Ausgabe entsprechend:
Paid $100.0 with Credit Card
Vorteile der Verwendung von Dependency Injection
-
Lockere Kopplung: Der Service und der Controller müssen nicht wissen, wie eine Zahlung verarbeitet wird. Sie verlassen sich einfach darauf, dass Spring die richtige Implementierung injiziert.
-
Modularität: Sie können problemlos neue Zahlungsmethoden hinzufügen (zum Beispiel
BankTransferPayment
,CryptoPayment
), indem Sie neue Klassen mit@Component
annotieren und den@Qualifier
anpassen. - Konfigurierbarkeit: Durch die Verwendung von Spring-Profilen können Sie Strategien basierend auf der Umgebung wechseln (zum Beispiel Entwicklung vs. Produktion).
Beispiel: Sie können @Profile
verwenden, um automatisch verschiedene Strategien basierend auf dem aktiven Profil einzufügen:
@Component
@Profile("dev")
public class DevPaymentStrategy implements PaymentStrategy { /* ... */ }
@Component
@Profile("prod")
public class ProdPaymentStrategy implements PaymentStrategy { /* ... */ }
Schlüsselerkenntnisse
-
Indem Sie Spring Boots DI verwenden, können Sie die Objekterstellung vereinfachen und die Flexibilität Ihres Codes verbessern.
-
Das Strategiemuster in Verbindung mit DI ermöglicht es Ihnen, zwischen verschiedenen Strategien einfach umzuschalten, ohne Ihre Kerngeschäftslogik zu ändern.
-
Durch die Verwendung von
@Qualifier
und Spring Profiles haben Sie die Flexibilität, Ihre Anwendung basierend auf verschiedenen Umgebungen oder Anforderungen zu konfigurieren.
Dieser Ansatz macht nicht nur Ihren Code sauberer, sondern bereitet ihn auch auf fortgeschrittenere Konfigurationen und Skalierungen in der Zukunft vor. Im nächsten Abschnitt werden wir bewährte Praktiken und Optimierungstipps erkunden, um Ihre Spring Boot-Anwendungen auf das nächste Level zu bringen.
Bewährte Praktiken und Optimierungstipps
Allgemeine bewährte Verfahren
-
Nicht übermäßig Muster verwenden: Verwenden Sie sie nur, wenn es notwendig ist. Überengineering kann Ihren Code schwerer wartbar machen.
-
Komposition der Vererbung vorziehen: Muster wie Strategie und Beobachter sind großartige Beispiele für dieses Prinzip.
-
Halten Sie Ihre Muster flexibel: Nutzen Sie Schnittstellen, um Ihren Code entkoppelt zu halten.
Leistungsüberlegungen
-
Singleton-Muster: Stellen Sie die Thread-Sicherheit durch Verwendung von
synchronized
oder demBill Pugh Singleton Design
sicher. -
Factory-Muster: Cachen Sie Objekte, wenn sie teuer zu erstellen sind.
-
Beobachter-Muster: Verwenden Sie asynchrone Verarbeitung, wenn Sie viele Beobachter haben, um Blockaden zu vermeiden.
Erweiterte Themen
-
Mit Reflection und dem Factory-Muster für das dynamische Laden von Klassen.
-
Nutzung von Spring Profiles zum Umschalten von Strategien basierend auf der Umgebung.
-
Hinzufügen von Swagger-Dokumentation für Ihre API-Endpunkte.
Zusammenfassung und Haupterkenntnisse
In diesem Tutorial haben wir einige der leistungsstärksten Designmuster – Singleton, Factory, Strategy und Observer – erkundet und gezeigt, wie man sie in Spring Boot implementiert. Lassen Sie uns jedes Muster kurz zusammenfassen und hervorheben, wofür es am besten geeignet ist:
Singleton-Muster:
-
Zusammenfassung: Stellt sicher, dass eine Klasse nur eine Instanz hat und bietet einen globalen Zugriffspunkt darauf.
-
Am besten geeignet für: Verwaltung von gemeinsam genutzten Ressourcen wie Konfigurationseinstellungen, Datenbankverbindungen oder Protokolldiensten. Ideal, wenn Sie den Zugriff auf eine gemeinsame Instanz in Ihrer gesamten Anwendung steuern möchten.
Factory-Pattern:
-
Zusammenfassung: Bietet eine Möglichkeit, Objekte zu erstellen, ohne die genaue Klasse anzugeben, die instanziiert werden soll. Dieses Muster entkoppelt die Objekterstellung von der Geschäftslogik.
-
Am besten geeignet für: Szenarien, in denen verschiedene Arten von Objekten basierend auf Eingangsbedingungen erstellt werden müssen, z. B. das Senden von Benachrichtigungen über E-Mail, SMS oder Push-Benachrichtigungen. Es eignet sich hervorragend, um Ihren Code modularer und erweiterbarer zu gestalten.
Strategie-Muster:
-
Zusammenfassung: Ermöglicht es Ihnen, eine Familie von Algorithmen zu definieren, jeden einzukapseln und austauschbar zu machen. Dieses Muster hilft Ihnen dabei, einen Algorithmus zur Laufzeit auszuwählen.
-
Am besten geeignet für: Wenn Sie dynamisch zwischen verschiedenen Verhaltensweisen oder Algorithmen wechseln müssen, z. B. bei der Verarbeitung verschiedener Zahlungsmethoden in einer E-Commerce-Anwendung. Es hält Ihren Code flexibel und entspricht dem Open/Closed-Prinzip.
Beobachtermuster:
-
Zusammenfassung: Definiert eine Abhängigkeit von eins zu vielen zwischen Objekten, sodass wenn sich ein Objekt ändert, automatisch alle seine Abhängigen benachrichtigt werden.
-
Am besten für: Ereignisgesteuerte Systeme wie Benachrichtigungsdienste, Echtzeit-Updates in Chat-Apps oder Systeme, die auf Änderungen in Daten reagieren müssen. Es ist ideal zum Entkoppeln von Komponenten und zur Steigerung der Skalierbarkeit Ihres Systems.
Was kommt als Nächstes?
Jetzt, da Sie diese essenziellen Designmuster gelernt haben, versuchen Sie, sie in Ihre bestehenden Projekte zu integrieren, um zu sehen, wie sie die Struktur und Skalierbarkeit Ihres Codes verbessern können. Hier sind ein paar Vorschläge für weitere Erkundungen:
-
Experimentieren: Versuchen Sie, andere Designmuster wie Dekorierer, Proxy und Erbauer zu implementieren, um Ihr Toolkit zu erweitern.
-
Übung: Verwenden Sie diese Muster, um bestehende Projekte umzustrukturieren und ihre Wartbarkeit zu verbessern.
-
Teilen: Wenn Sie Fragen haben oder Ihre Erfahrungen teilen möchten, können Sie sich gerne melden!
Ich hoffe, dieser Leitfaden hat Ihnen geholfen zu verstehen, wie man Designmuster in Java effektiv einsetzen kann. Bleiben Sie experimentierfreudig und viel Spaß beim Codieren!
Source:
https://www.freecodecamp.org/news/how-to-use-design-patterns-in-java-with-spring-boot/