Naarmate softwareprojecten groeien, wordt het steeds belangrijker om je code georganiseerd, onderhoudbaar en schaalbaar te houden. Hier komen ontwerppatronen om de hoek kijken. Ontwerppatronen bieden bewezen, herbruikbare oplossingen voor veelvoorkomende software-ontwerpvraagstukken, waardoor je code efficiënter wordt en gemakkelijker te beheren.

In deze gids duiken we dieper in op enkele van de meest populaire ontwerppatronen en laten we zien hoe je ze kunt implementeren in Spring Boot. Tegen het einde zul je niet alleen deze patronen conceptueel begrijpen, maar ze ook met vertrouwen kunnen toepassen in je eigen projecten.

Inhoudsopgave

Introductie tot Ontwerppatronen

Ontwerppatronen zijn herbruikbare oplossingen voor veelvoorkomende software-ontwerpproblemen. Denk aan hen als best practices gedestilleerd in sjablonen die kunnen worden toegepast om specifieke uitdagingen in uw code op te lossen. Ze zijn niet specifiek voor een taal, maar ze kunnen bijzonder krachtig zijn in Java vanwege de objectgeoriënteerde aard ervan.

In deze handleiding behandelen we:

  • Singleton-patroon: Zorgen dat een klasse slechts één instantie heeft.

  • Factory-patroon: Objecten maken zonder de exacte klasse te specificeren.

  • Strategiepatroon: Toestaan dat algoritmen tijdens runtime worden geselecteerd.

  • Observer-patroon: Opzetten van een publiceer-abonneerrelatie.

We behandelen niet alleen hoe deze patronen werken, maar verkennen ook hoe ze kunnen worden toegepast in Spring Boot voor real-world toepassingen.

Hoe u uw Spring Boot-project instelt

Voordat we ingaan op de patronen, laten we een Spring Boot-project opzetten:

Vereisten

Zorg ervoor dat u heeft:

  • Java 11+

  • Maven

  • Spring Boot CLI (optioneel)

  • Postman of curl (voor testen)

Projectinitialisatie

U kunt snel een Spring Boot-project maken met 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

Wat is het Singleton-patroon?

Het Singleton-patroon zorgt ervoor dat een klasse slechts één instantie heeft en biedt een wereldwijd toegangspunt ernaar. Dit patroon wordt vaak gebruikt voor services zoals logging, configuratiebeheer of databaseverbindingen.

Hoe implementeer je het Singleton-patroon in Spring Boot

Spring Boot-beans zijn standaard singletons, wat betekent dat Spring automatisch het levenscyclus van deze beans beheert om ervoor te zorgen dat slechts één instantie bestaat. Het is echter belangrijk om te begrijpen hoe het Singleton-patroon onder de motorkap werkt, vooral wanneer je geen door Spring beheerde beans gebruikt of meer controle nodig hebt over instantiebeheer.

Laten we een handmatige implementatie van het Singleton-patroon doornemen om te demonstreren hoe je de creatie van een enkele instantie binnen je applicatie kunt beheren.

Stap 1: Maak een LoggerService Klasse

In dit voorbeeld zullen we een eenvoudige logging-service maken met het Singleton-patroon. Het doel is om ervoor te zorgen dat alle delen van de applicatie dezelfde logging-instantie gebruiken.

public class LoggerService {
    // De statische variabele om de enkele instantie vast te houden
    private static LoggerService instance;

    // Private constructor om instantiatie van buitenaf te voorkomen
    private LoggerService() {
        // Deze constructor is opzettelijk leeg om te voorkomen dat andere klassen instanties maken
    }

    // Openbare methode om toegang te bieden tot de enkele instantie
    public static synchronized LoggerService getInstance() {
        if (instance == null) {
            instance = new LoggerService();
        }
        return instance;
    }

    // Voorbeeld van een logmethode
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}
  • Statische Variabele (instantie): Dit houdt de enkele instantie van LoggerService vast.

  • Private Constructor: De constructor is privé gemarkeerd om te voorkomen dat andere klassen rechtstreeks nieuwe instanties maken.

  • Gesynchroniseerde getInstance() Methode: De methode is gesynchroniseerd om deze thread-safe te maken, zodat slechts één instantie wordt gemaakt, zelfs als meerdere threads er tegelijkertijd toegang toe proberen te krijgen.

  • Lui initialiseren: Het exemplaar wordt alleen gemaakt wanneer het voor het eerst wordt opgevraagd (lui initialiseren), wat efficiënt is qua geheugengebruik.

Praktisch gebruik: Dit patroon wordt vaak gebruikt voor gedeelde resources, zoals logging, configuratie-instellingen of het beheren van databaseverbindingen, waarbij u de toegang wilt controleren en ervoor wilt zorgen dat slechts één exemplaar wordt gebruikt in uw toepassing.

Stap 2: Gebruik de Singleton in een Spring Boot-controller

Latenn we nu zien hoe we onze LoggerService Singleton kunnen gebruiken binnen een Spring Boot-controller. Deze controller zal een eindpunt blootstellen dat een bericht logt telkens wanneer het wordt benaderd.

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() {
        // Toegang tot het Singleton-exemplaar van LoggerService
        LoggerService logger = LoggerService.getInstance();
        logger.log("This is a log message!");
        return ResponseEntity.ok("Message logged successfully");
    }
}
  • GET-eindpunt: We hebben een /log eindpunt gemaakt dat, wanneer benaderd, een bericht logt met behulp van de LoggerService.

  • Singleton-gebruik: In plaats van een nieuw exemplaar van LoggerService te maken, roepen we getInstance() aan om ervoor te zorgen dat we elke keer hetzelfde exemplaar gebruiken.

  • Reactie: Na het loggen, retourneert het eindpunt een reactie die succes aangeeft.

Stap 3: Het Singleton Patroon Testen

Nu, laten we dit eindpunt testen met behulp van Postman of je browser:

GET http://localhost:8080/log

Verwachte Uitvoer:

  • Console log: [LOG] Dit is een logbericht!

  • HTTP Reactie: Bericht succesvol gelogd

Je kunt het eindpunt meerdere keren aanroepen en je zult zien dat dezelfde instantie van LoggerService wordt gebruikt, zoals aangegeven door de consistente loguitvoer.

Gebruiksscenario’s voor het Singleton Patroon in de Echte Wereld

Hier is wanneer je het Singleton-patroon zou willen gebruiken in real-world toepassingen:

  1. Configuratiebeheer: Zorg ervoor dat jouw applicatie een consistente reeks configuratie-instellingen gebruikt, vooral wanneer die instellingen worden geladen vanuit bestanden of databases.

  2. Database Verbinding Pools: Beheer toegang tot een beperkt aantal databaseverbindingen, zorg ervoor dat hetzelfde pool gedeeld wordt over de applicatie.

  3. Caching: Behoud een enkele cache-instantie om inconsistente gegevens te vermijden.

  4. Logdiensten: Zoals getoond in dit voorbeeld, gebruik een enkele logdienst om loguitvoer te centraliseren over verschillende modules van je applicatie.

Belangrijkste punten

  • Het Singleton-patroon is een eenvoudige manier om ervoor te zorgen dat slechts één instantie van een klasse wordt aangemaakt.

  • Threadveiligheid is cruciaal als meerdere threads toegang hebben tot de Singleton, daarom hebben we synchronized gebruikt in ons voorbeeld.

  • Spring Boot-beans zijn standaard al singletons, maar het begrijpen van hoe je het handmatig implementeert helpt je meer controle te krijgen wanneer dat nodig is.

Dit omvat de implementatie en het gebruik van het Singleton-patroon. Vervolgens zullen we het Factory-patroon verkennen om te zien hoe het kan helpen bij het stroomlijnen van objectcreatie.

Wat is het Factory-patroon?

Het Factory-patroon stelt je in staat om objecten te maken zonder de exacte klasse te specificeren. Dit patroon is handig wanneer je verschillende soorten objecten hebt die moeten worden geïnstantieerd op basis van bepaalde invoer.

Hoe implementeer je een Factory in Spring Boot

Het Factory-patroon is ongelooflijk nuttig wanneer je objecten moet maken op basis van bepaalde criteria, maar de objectcreatie wilt ontkoppelen van de logica van je hoofdtoepassing.

In deze sectie zullen we een NotificationFactory bouwen om meldingen te versturen via e-mail of sms. Dit is vooral handig als je verwacht in de toekomst meer meldingstypen toe te voegen, zoals pushmeldingen of in-appmeldingen, zonder je bestaande code te wijzigen.

Stap 1: Maak de Notification Interface

De eerste stap is het definiëren van een gemeenschappelijke interface die alle typen meldingen zullen implementeren. Dit zorgt ervoor dat elk type melding (e-mail, sms, enzovoort) een consistente send()-methode heeft.

public interface Notification {
    void send(String message);
}
  • Doel: De Notification-interface definieert het contract voor het versturen van meldingen. Elke klasse die deze interface implementeert, moet een implementatie bieden voor de send()-methode.

  • Schaalbaarheid: Door een interface te gebruiken, kunt u in de toekomst eenvoudig uw toepassing uitbreiden om andere soorten meldingen op te nemen zonder bestaande code te wijzigen.

Stap 2: Implementeer EmailNotification en SMSNotification

Latennwe nu twee concrete klassen implementeren, een voor het verzenden van e-mails en een andere voor het verzenden van SMS-berichten.

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);
    }
}

Stap 3: Maak een NotificationFactory

De NotificationFactory klasse is verantwoordelijk voor het maken van instanties van Notification op basis van het opgegeven type. Dit ontwerp zorgt ervoor dat de NotificationController niet op de hoogte hoeft te zijn van de details van objectcreatie.

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 (createNotification()):

  • De factory-methode neemt een string (type) als invoer en retourneert een instantie van de overeenkomstige meldingsklasse.

  • Switch Statement: De switch-instructie selecteert het juiste meldingstype op basis van de invoer.

  • Foutafhandeling: Als het opgegeven type niet wordt herkend, wordt er een IllegalArgumentException opgegooid. Dit zorgt ervoor dat ongeldige types vroegtijdig worden opgemerkt.

Waarom een Factory Gebruiken?

  • Decoupling: Het factory-patroon ontkoppelt de objectcreatie van de bedrijfslogica. Dit maakt je code modulairder en gemakkelijker te onderhouden.

  • Uitbreidbaarheid: Als je een nieuw notificatietype wilt toevoegen, hoef je alleen de factory bij te werken zonder de controllerlogica te wijzigen.

Stap 4: Gebruik de Factory in een Spring Boot Controller

Nu gaan we alles samenvoegen door een Spring Boot-controller te creëren die de NotificationFactory gebruikt om notificaties te verzenden op basis van het verzoek van de gebruiker.

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 {
            // Maak het juiste Notification-object aan met behulp van de 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 Endpoint (/notify):

  • De controller exposeert een /notify eindpunt dat twee query parameters accepteert: type (ofwel “EMAIL” of “SMS”) en message.

  • Het gebruikt de NotificationFactory om het juiste meldingstype te maken en verzendt het bericht.

  • Foutafhandeling: Als er een ongeldig meldingstype wordt opgegeven, vangt de controller de IllegalArgumentException op en geeft een 400 Bad Request reactie terug.

Stap 5: Het testen van het Fabriekspatroon

Lat de eindpunt testen met Postman of een browser:

  1. Verstuur een E-mailmelding:

     GET http://localhost:8080/notify?type=email&message=Hello%20Email
    

    Output:

     E-mail verzenden: Hallo Email
    
  2. Stuur een sms-melding:

     GET http://localhost:8080/notify?type=sms&message=Hallo%20SMS
    

    Uitvoer:

     SMS verzenden: Hallo SMS
    
  3. Testen met een ongeldig type:

     GET http://localhost:8080/notify?type=unknown&message=Test
    

    Uitvoer:

     Fout verzoek: Onbekend meldingstype: unknown
    

Praktijkgevallen voor het fabriekspatroon

Het fabriekspatroon is bijzonder nuttig in scenario’s waarin:

  1. Dynamische objectcreatie: Wanneer u objecten moet maken op basis van gebruikersinvoer, zoals verschillende soorten meldingen verzenden, rapporten genereren in verschillende formaten of verschillende betaalmethoden verwerken.

  2. Afkoppeling van Objectcreatie: Door een fabriek te gebruiken, kunt u uw belangrijkste bedrijfslogica gescheiden houden van objectcreatie, waardoor uw code beter onderhoudbaar wordt.

  3. Schaalbaarheid: Breid uw toepassing eenvoudig uit om nieuwe soorten meldingen te ondersteunen zonder bestaande code te wijzigen. Voeg eenvoudig een nieuwe klasse toe die de Melding-interface implementeert en werk de fabriek bij.

Wat is het Strategy-patroon?

Het Strategy-patroon is perfect wanneer u dynamisch moet schakelen tussen meerdere algoritmes of gedragingen. Het stelt u in staat om een ​​reeks algoritmes te definiëren, elk binnen afzonderlijke klassen te encapsuleren en ze gemakkelijk uitwisselbaar te maken op runtime. Dit is vooral handig voor het selecteren van een algoritme op basis van specifieke voorwaarden, waardoor uw code schoon, modulair en flexibel blijft.

Gebruik in de echte wereld: Stel je een e-commerce systeem voor dat meerdere betaalopties moet ondersteunen, zoals creditcards, PayPal of bankoverschrijvingen. Door het Strategiepatroon te gebruiken, kun je eenvoudig betaalmethoden toevoegen of aanpassen zonder de bestaande code te wijzigen. Deze aanpak zorgt ervoor dat je toepassing schaalbaar en onderhoudbaar blijft wanneer je nieuwe functies introduceert of bestaande bijwerkt.

We zullen dit patroon demonstreren met een voorbeeld van Spring Boot dat betalingen afhandelt met behulp van een creditcard- of PayPal-strategie.

Stap 1: Definieer een PaymentStrategy Interface

We beginnen met het maken van een gemeenschappelijke interface die alle betaalstrategieën zullen implementeren:

public interface PaymentStrategy {
    void pay(double amount);
}

De interface definieert een contract voor alle betaalmethoden, waardoor consistentie wordt gegarandeerd bij implementaties.

Stap 2: Implementeer Betaalstrategieën

Maak concrete klassen voor creditcard- en PayPal-betalingen.

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");
    }
}

Elke klasse implementeert de betaling() methode met zijn specifieke gedrag.

Stap 3: Gebruik de Strategie in een Controller

Maak een controller om dynamisch een betaalstrategie te selecteren op basis van gebruikersinvoer:

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;
        }
    }
}

De eindpunt accepteert methode en bedrag als queryparameters en verwerkt de betaling met de juiste strategie.

Stap 4: Testen van het Eindpunt

  1. Kredietkaartbetaling:

     GET http://localhost:8080/betalen?methode=credit&bedrag=100
    

    Uitvoer: $100,0 betaald met Creditcard

  2. PayPal-betaling:

     GET http://localhost:8080/betalen?methode=paypal&bedrag=50
    

    Uitvoer: $50,0 betaald via PayPal

  3. Ongeldige methode:

     GET http://localhost:8080/betalen?methode=bitcoin&bedrag=25
    

    Uitvoer: Ongeldige betaalmethode

Gebruiksscenario’s voor het Strategy-patroon

  • Betaling verwerken: Schakel dynamisch tussen verschillende betalingsgateways.

  • Sorteeralgoritmes: Kies de beste sorteermethode op basis van gegevensgrootte.

  • Bestanden exporteren: Exporteer rapporten in diverse formaten (PDF, Excel, CSV).

Belangrijkste punten

  • Het Strategiepatroon houdt uw code modulair en volgt het Open/Closed-principe.

  • Het toevoegen van nieuwe strategieën is eenvoudig—maak gewoon een nieuwe klasse die de PaymentStrategy-interface implementeert.

  • Het is ideaal voor situaties waarin u flexibele algoritmekeuze nodig hebt op runtime.

Hierna zullen we het Observerpatroon verkennen, perfect voor het afhandelen van op gebeurtenissen gebaseerde architecturen.

Wat is het Observerpatroon?

Het Observer-patroon is ideaal wanneer je één object hebt (het onderwerp) dat meerdere andere objecten (waarnemers) moet informeren over veranderingen in zijn toestand. Het is perfect voor op gebeurtenissen gebaseerde systemen waar updates naar verschillende componenten moeten worden gestuurd zonder dat er een sterke koppeling tussen hen ontstaat. Dit patroon stelt je in staat om een schone architectuur te behouden, vooral wanneer verschillende delen van je systeem onafhankelijk op veranderingen moeten reageren.

Gebruik in de echte wereld: Dit patroon wordt veel gebruikt in systemen die meldingen of waarschuwingen versturen, zoals chattoepassingen of aandelenkoersvolgers, waar updates in realtime naar gebruikers moeten worden gestuurd. Door het Observer-patroon te gebruiken, kun je eenvoudig meldingstypen toevoegen of verwijderen zonder de kernlogica te veranderen.

We zullen demonstreren hoe dit patroon te implementeren in Spring Boot door een eenvoudig meldingssysteem te bouwen waarbij zowel e-mail- als sms-meldingen worden verzonden wanneer een gebruiker zich registreert.

Stap 1: Maak een Observer-interface

We beginnen met het definiëren van een gemeenschappelijke interface die alle waarnemers zullen implementeren:

public interface Observer {
    void update(String event);
}

De interface legt een contract vast waarin alle waarnemers de update()-methode moeten implementeren, die wordt geactiveerd telkens wanneer het onderwerp verandert.

Stap 2: Implementeer EmailObserver en SMSObserver

Vervolgens creëren we twee concrete implementaties van de Observer-interface om e-mail- en sms-meldingen te verwerken.

EmailObserver-klasse

public class EmailObserver implements Observer {
    @Override
    public void update(String event) {
        System.out.println("Email sent for event: " + event);
    }
}

De EmailObserver handelt het verzenden van e-mailmeldingen af telkens wanneer het op de hoogte wordt gebracht van een gebeurtenis.

Klasse SMSObserver

public class SMSObserver implements Observer {
    @Override
    public void update(String event) {
        System.out.println("SMS sent for event: " + event);
    }
}

De SMSObserver behandelt het verzenden van SMS-meldingen telkens wanneer het wordt geïnformeerd.

Stap 3: Maak een klasse UserService (Het Onderwerp)

We zullen nu een klasse UserService maken die fungeert als het onderwerp en zijn geregistreerde waarnemers op de hoogte stelt telkens wanneer een gebruiker zich registreert.

import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

@Service
public class UserService {
    private List<Observer> observers = new ArrayList<>();

    // Methode om waarnemers te registreren
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    // Methode om alle geregistreerde waarnemers van een gebeurtenis op de hoogte te stellen
    public void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }

    // Methode om een nieuwe gebruiker te registreren en waarnemers op de hoogte te stellen
    public void registerUser(String username) {
        System.out.println("User registered: " + username);
        notifyObservers("User Registration");
    }
}
  • Waarnemerslijst: Houdt bij alle geregistreerde waarnemers bij.

  • registerObserver() Methode: Voegt nieuwe waarnemers toe aan de lijst.

  • notifyObservers() Methode: Stelt alle geregistreerde waarnemers op de hoogte wanneer een gebeurtenis plaatsvindt.

  • registerUser() Methode: Registreert een nieuwe gebruiker en activeert meldingen naar alle waarnemers.

Stap 4: Gebruik het Observer-patroon in een Controller

Tenslotte zullen we een Spring Boot-controller maken om een eindpunt bloot te leggen voor gebruikersregistratie. Deze controller zal zowel de EmailObserver als de SMSObserver registreren bij de 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();
        // Observers registreren
        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!");
    }
}
  • Eindpunt (/register): Accepteert een gebruikersnaam-parameter en registreert de gebruiker, waarbij meldingen naar alle waarnemers worden gestuurd.

  • Waarnemers: Zowel EmailObserver als SMSObserver zijn geregistreerd bij UserService, zodat ze worden geïnformeerd wanneer een gebruiker zich registreert.

Testen van het Observer-patroon

Nu gaan we onze implementatie testen met behulp van Postman of een browser:

POST http://localhost:8080/api/register?username=JohnDoe

Verwachte uitvoer in Console:

User registered: JohnDoe
Email sent for event: User Registration
SMS sent for event: User Registration

Het systeem registreert de gebruiker en informeert zowel de Email- als SMS-waarnemers, waarbij de flexibiliteit van het Observer-patroon wordt gedemonstreerd.

Praktische toepassingen van het Observer-patroon

  1. Meldingssystemen: Updates naar gebruikers sturen via verschillende kanalen (e-mail, SMS, pushmeldingen) wanneer bepaalde gebeurtenissen plaatsvinden.

  2. Gebeurtenisgestuurde architecturen: Het op de hoogte brengen van meerdere subsystemen wanneer specifieke acties plaatsvinden, zoals gebruikersactiviteiten of systeemwaarschuwingen.

  3. Gegevensstreaming: Het uitzenden van gegevenswijzigingen naar diverse gebruikers in realtime (bijvoorbeeld live aandelenkoersen of sociale mediafeeds).

Hoe Spring Boot’s Dependency Injection te gebruiken

Tot nu toe hebben we handmatig objecten gemaakt om ontwerppatronen te demonstreren. In echte Spring Boot-toepassingen is Dependency Injection (DI) echter de voorkeursmethode om objectcreatie te beheren. DI stelt Spring in staat om automatisch de instantiëring en bedrading van uw klassen te beheren, waardoor uw code meer modulair, testbaar en onderhoudbaar wordt.

Laten we ons Strategy-patroonvoorbeeld herschrijven om te profiteren van de krachtige DI-mogelijkheden van Spring Boot. Hiermee kunnen we dynamisch schakelen tussen betalingsstrategieën, met behulp van Spring-annotaties om afhankelijkheden te beheren.

Bijgewerkt Strategy-patroon met gebruik van Spring Boot’s DI

In ons gerefactoreerde voorbeeld zullen we gebruikmaken van Spring’s annotaties zoals @Component, @Service en @Autowired om het proces van het injecteren van afhankelijkheden te stroomlijnen.

Stap 1: Markeer Betaalstrategieën met @Component

Allereerst zullen we onze strategie-implementaties markeren met de @Component-annotatie zodat Spring ze automatisch kan detecteren en beheren.

@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-annotatie: Door @Component toe te voegen, vertellen we Spring om deze klassen te behandelen als door Spring beheerde beans. De stringwaarde ("creditCardPayment" en "payPalPayment") fungeert als de bean-identificatie.

  • Flexibiliteit: Met deze opstelling kunnen we schakelen tussen strategieën door de juiste bean-identificatie te gebruiken.

Stap 2: Refactor de PaymentService om Afhankelijkheidsinjectie te Gebruiken

Vervolgens zullen we de PaymentService aanpassen om een specifieke betaalstrategie in te voegen met behulp van @Autowired en @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);
    }
}
  • @Service-annotatie: Markeert PaymentService als een door Spring beheerde service bean.

  • @Autowired: Spring injecteert automatisch de vereiste afhankelijkheid.

  • @Qualifier: Specificeert welke implementatie van PaymentStrategy moet worden geïnjecteerd. In dit voorbeeld gebruiken we "payPalPayment".

  • Gemak van configuratie: Door eenvoudigweg de waarde van @Qualifier te wijzigen, kunt u de betaalstrategie wijzigen zonder enige bedrijfslogica te veranderen.

Stap 3: Het geherstructureerde Service gebruiken in een Controller

Om de voordelen van deze herstructurering te zien, laten we de controller bijwerken om onze PaymentService te gebruiken:

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: De controller ontvangt automatisch de PaymentService met de geïnjecteerde betaalstrategie.

  • GET-eindpunt (/betalen): Wanneer toegankelijk, verwerkt het een betaling met behulp van de momenteel geconfigureerde strategie (PayPal in dit voorbeeld).

Het testen van het opnieuw ontworpen strategiepatroon met DI

Nu gaan we de nieuwe implementatie testen met behulp van Postman of een browser:

GET http://localhost:8080/api/pay?amount=100

Verwachte uitvoer:

Paid $100.0 using PayPal

Als je de kwalificatie in PaymentService wijzigt naar "creditCardPayment", zal de uitvoer dienovereenkomstig veranderen:

Paid $100.0 with Credit Card

Voordelen van het gebruik van Dependency Injection

  • Losse koppeling: De service en de controller hoeven niet te weten hoe een betaling wordt verwerkt. Ze vertrouwen eenvoudigweg op Spring om de juiste implementatie in te voegen.

  • Modulariteit: U kunt eenvoudig nieuwe betaalmethoden toevoegen (bijvoorbeeld BankTransferPayment, CryptoPayment) door nieuwe klassen aan te maken die zijn geannoteerd met @Component en de @Qualifier aan te passen.

  • Configureerbaarheid: Door gebruik te maken van Spring-profielen, kunt u strategieën wijzigen op basis van de omgeving (bijvoorbeeld ontwikkeling vs. productie).

Voorbeeld: U kunt @Profiel gebruiken om automatisch verschillende strategieën in te voegen op basis van het actieve profiel:

@Component
@Profile("dev")
public class DevPaymentStrategy implements PaymentStrategy { /* ... */ }

@Component
@Profile("prod")
public class ProdPaymentStrategy implements PaymentStrategy { /* ... */ }

Belangrijkste punten

  • Door het gebruik van Spring Boot’s DI kunt u objectcreatie vereenvoudigen en de flexibiliteit van uw code verbeteren.

  • Het Strategiepatroon gecombineerd met DI stelt u in staat om eenvoudig te schakelen tussen verschillende strategieën zonder uw kernbedrijfslogica te wijzigen.

  • Het gebruik van @Kwalificeer en Spring Profiles geeft u de flexibiliteit om uw toepassing te configureren op basis van verschillende omgevingen of vereisten.

Deze aanpak maakt niet alleen uw code schoner, maar bereidt deze ook voor op meer geavanceerde configuraties en schaalvergroting in de toekomst. In de volgende sectie zullen we Best Practices en Optimalisatietips verkennen om uw Spring Boot-toepassingen naar een hoger niveau te tillen.

Best Practices en Optimalisatietips

Algemene Beste Praktijken

  • Overschrijd patronen niet: Gebruik ze alleen wanneer nodig. Overengineering kan je code moeilijker te onderhouden maken.

  • Geef de voorkeur aan compositie boven overerving: Patronen zoals Strategie en Observer zijn goede voorbeelden van dit principe.

  • Houd je patronen flexibel: Maak gebruik van interfaces om je code losgekoppeld te houden.

Overwegingen voor Prestaties

  • Singleton Patroon: Zorg voor thread-veiligheid door synchronized te gebruiken of het Bill Pugh Singleton Design.

  • Factory Patroon: Caché objecten als ze duur zijn om te maken.

  • Observer Patroon: Gebruik asynchrone verwerking als je veel waarnemers hebt om blokkering te voorkomen.

Geavanceerde Onderwerpen

  • Gebruik Reflectie met het Factory-patroon voor dynamisch laden van klassen.

  • Benut Spring Profielen om strategieën te wisselen op basis van de omgeving.

  • Voeg Swagger Documentatie toe voor je API-eindpunten.

Conclusie en Belangrijkste Leerpunten

In deze tutorial hebben we enkele van de krachtigste ontwerppatronen verkend—Singleton, Factory, Strategy en Observer—en laten zien hoe je ze kunt implementeren in Spring Boot. Laten we elk patroon kort samenvatten en benadrukken waarvoor het het beste geschikt is:

Singleton Patroon:

  • Samenvatting: Zorgt ervoor dat een klasse slechts één instantie heeft en biedt een wereldwijd toegangspunt daarvoor.

  • Het Beste Voor: Beheren van gedeelde bronnen zoals configuratie-instellingen, databaseverbindingen of logservices. Het is ideaal wanneer je de toegang tot een gedeelde instantie in je hele applicatie wilt beheersen.

Factory Pattern:

  • Samenvatting: Biedt een manier om objecten te maken zonder de exacte klasse te specificeren die moet worden geïnstantieerd. Dit patroon ontkoppelt objectcreatie van de bedrijfslogica.

  • Beste Voor: Scenario’s waarin u verschillende soorten objecten moet maken op basis van invoercondities, zoals het verzenden van meldingen via e-mail, sms of pushmeldingen. Het is geweldig om uw code modulair en uitbreidbaar te maken.

Strategiepatroon:

  • Samenvatting: Hiermee kunt u een reeks algoritmen definiëren, elk daarvan encapsuleren en ze uitwisselbaar maken. Dit patroon helpt u een algoritme op runtime te kiezen.

  • Beste Voor: Wanneer u dynamisch wilt schakelen tussen verschillende gedragingen of algoritmen, zoals het verwerken van verschillende betalingsmethoden in een e-commerce toepassing. Het houdt uw code flexibel en voldoet aan het Open/Closed-Principe.

Observer Patroon:

  • Samenvatting: Definieert een één-op-veel afhankelijkheid tussen objecten zodat wanneer één object van staat verandert, al zijn afhankelijken automatisch op de hoogte worden gesteld.

  • Beste voor: Gebeurtenisgestuurde systemen zoals meldingsdiensten, real-time updates in chat-apps, of systemen die moeten reageren op veranderingen in gegevens. Het is ideaal voor het ontkoppelen van componenten en het maken van je systeem schaalbaarder.

Wat nu?

Nu je deze essentiële ontwerppatronen hebt geleerd, probeer ze te integreren in je bestaande projecten om te zien hoe ze de structuur en schaalbaarheid van je code kunnen verbeteren. Hier zijn een paar suggesties voor verder onderzoek:

  • Experimenteer: Probeer andere ontwerppatronen te implementeren zoals Decorator, Proxy en Builder om je toolkit uit te breiden.

  • Praktijk: Gebruik deze patronen om bestaande projecten te refactoren en hun onderhoudbaarheid te verbeteren.

  • Deel: Als je vragen hebt of je ervaring wilt delen, voel je vrij om contact op te nemen!

Ik hoop dat deze gids je heeft geholpen te begrijpen hoe je effectief ontwerppatronen kunt gebruiken in Java. Blijf experimenteren en veel codeplezier!