Dubbele verzending in DDD

1. Overzicht

Dubbele verzending is een technische term om de proces van het kiezen van de methode die moet worden aangeroepen op basis van zowel ontvanger- als argumenttypen.

Veel ontwikkelaars verwarren dubbele verzending vaak met Strategy Pattern.

Java ondersteunt geen dubbele verzending, maar er zijn technieken die we kunnen gebruiken om deze beperking te omzeilen.

In deze tutorial zullen we ons concentreren op het tonen van voorbeelden van dubbele verzending in de context van Domain-driven Design (DDD) en Strategiepatroon.

2. Dubbele verzending

Voordat we dubbele verzending bespreken, laten we eerst enkele basisprincipes bekijken en uitleggen wat Single Dispatch eigenlijk is.

2.1. Enkele verzending

Single dispatch is een manier om de implementatie van een methode te kiezen op basis van het runtime-type van de ontvanger. In Java is dit in wezen hetzelfde als polymorfisme.

Laten we bijvoorbeeld eens kijken naar deze eenvoudige interface voor kortingsbeleid:

openbare interface DiscountPolicy {dubbele korting (bestelling bestellen); }

De Kortingsbeleid interface heeft twee implementaties. De platte, die altijd dezelfde korting oplevert:

public class FlatDiscountPolicy implementeert DiscountPolicy {@Override publieke dubbele korting (Order order) {return 0.01; }}

En de tweede implementatie, die de korting retourneert op basis van de totale kosten van de bestelling:

openbare klasse AmountBasedDiscountPolicy implementeert DiscountPolicy {@Override openbare dubbele korting (orderorder) {if (order.totalCost () .isGreaterThan (Money.of (CurrencyUnit.USD, 500.00))) {return 0.10; } else {return 0; }}}

Laten we voor de behoeften van dit voorbeeld aannemen dat de Bestellen klasse heeft een totale prijs() methode.

Nu is enkelvoudige verzending in Java slechts een zeer bekend polymorf gedrag dat wordt aangetoond in de volgende test:

@DisplayName ("gegeven twee kortingsbeleid", + "bij gebruik van dit beleid," + "dan kiest een enkele verzending de implementatie op basis van het runtime-type") @Test void test () gooit Uitzondering {// gegeven DiscountPolicy flatPolicy = nieuwe FlatDiscountPolicy ( ); DiscountPolicy amountPolicy = nieuw AmountBasedDiscountPolicy (); Bestel orderWorth501Dollars = orderWorthNDollars (501); // wanneer dubbele flatDiscount = flatPolicy.discount (orderWorth501Dollars); double amountDiscount = amountPolicy.discount (orderWorth501Dollars); // dan assertThat (flatDiscount) .isEqualTo (0.01); assertThat (amountDiscount) .isEqualTo (0.1); }

Als dit allemaal vrij eenvoudig lijkt, houd het dan in de gaten. We zullen later hetzelfde voorbeeld gebruiken.

We zijn nu klaar om dubbele verzending te introduceren.

2.2. Dubbele verzending versus overbelasting van de methode

Double dispatch bepaalt de methode die tijdens runtime wordt aangeroepen op basis van zowel het ontvangertype als de argumenttypen.

Java ondersteunt geen dubbele verzending.

Merk op dat dubbele verzending vaak wordt verward met overbelasting van de methode, wat niet hetzelfde is. Overbelasting van de methode kiest de methode die alleen wordt aangeroepen op basis van compilatie-informatie, zoals het declaratietype van de variabele.

In het volgende voorbeeld wordt dit gedrag in detail uitgelegd.

Laten we een nieuwe kortingsinterface introduceren met de naam SpecialDiscountPolicy:

openbare interface SpecialDiscountPolicy verlengt DiscountPolicy {dubbele korting (SpecialOrder-bestelling); }

Speciale bestelling strekt zich gewoon uit Bestellen zonder nieuw toegevoegd gedrag.

Nu, wanneer we een instantie van Speciale bestelling maar verklaar het als normaal Bestellen, dan wordt de speciale kortingsmethode niet gebruikt:

@DisplayName ("gegeven kortingsbeleid accepteert speciale bestellingen," + "wanneer het beleid wordt toegepast op speciale bestelling aangegeven als normale bestelling," + "dan wordt de normale kortingsmethode gebruikt") @Test void test () werpt een uitzondering {// gegeven SpecialDiscountPolicy specialPolicy = nieuwe SpecialDiscountPolicy () {@ Openbare dubbele korting overschrijven (Bestelling) {retour 0.01; } @Override openbare dubbele korting (SpecialOrder-bestelling) {return 0.10; }}; Bestellen specialOrder = nieuwe SpecialOrder (anyOrderLines ()); // wanneer dubbele korting = specialPolicy.discount (specialOrder); // dan assertThat (korting) .isEqualTo (0.01); }

Daarom is overbelasting van de methode geen dubbele verzending.

Zelfs als Java geen dubbele verzending ondersteunt, kunnen we een patroon gebruiken om vergelijkbaar gedrag te bereiken: Bezoeker.

2.3. Bezoekerspatroon

Het bezoekerspatroon stelt ons in staat om nieuw gedrag aan de bestaande klassen toe te voegen zonder deze te wijzigen. Dit is mogelijk dankzij de slimme techniek om dubbele verzending te emuleren.

Laten we het kortingsvoorbeeld even laten, zodat we het bezoekerspatroon kunnen introduceren.

Stel je voor dat we HTML-weergaven willen produceren met verschillende sjablonen voor elke soort bestelling. We zouden dit gedrag rechtstreeks aan de orderklassen kunnen toevoegen, maar het is niet het beste idee omdat het een SRP-overtreding is.

In plaats daarvan gebruiken we het bezoekerspatroon.

Eerst moeten we de Bezoekbaar koppel:

openbare interface Visitable {ongeldig accepteren (V bezoeker); }

We zullen ook een bezoekersinterface gebruiken, in onze casus genaamd Bestel Bezoeker:

openbare interface OrderVisitor {void visit (Order order); ongeldig bezoek (bestelling SpecialOrder); }

Een van de nadelen van het bezoekerspatroon is echter dat bezoekersklassen nodig zijn om op de hoogte te zijn van de bezoeker.

Als klassen niet zijn ontworpen om de bezoeker te ondersteunen, kan het moeilijk zijn (of zelfs onmogelijk als de broncode niet beschikbaar is) om dit patroon toe te passen.

Elk ordertype moet het Bezoekbaar interface en bieden zijn eigen implementatie die schijnbaar identiek is, een ander nadeel.

Merk op dat de toegevoegde methoden aan Bestellen en Speciale bestelling zijn identiek:

openbare klasse Order implementeert Visitable {@Override public void accept (OrderVisitor-bezoeker) {bezoeker.visit (dit); }} public class SpecialOrder verlengt Order {@Override public void accept (OrderVisitor bezoeker) {bezoeker.visit (dit); }}

Het kan verleidelijk zijn om niet opnieuw te implementeren aanvaarden in de subklasse. Als we dat echter niet hebben gedaan, is het OrderVisitor.visit (Bestelling) methode zou natuurlijk altijd wennen vanwege polymorfisme.

Laten we tot slot eens kijken naar de implementatie van Bestel Bezoeker verantwoordelijk voor het maken van HTML-views:

openbare klasse HtmlOrderViewCreator implementeert OrderVisitor {private String html; public String getHtml () {return html; } @Override public void visit (Order order) {html = String.format ("

Totale kosten normale bestelling:% s

", order.totalCost ());} @Override public void visit (SpecialOrder order) {html = String.format ("

totale kosten

", order.totalCost ());}}

Het volgende voorbeeld toont het gebruik van HtmlOrderViewCreator:

@DisplayName ("gegeven verzameling van reguliere en speciale bestellingen," + "wanneer HTML-weergave wordt gemaakt met bezoeker voor elke bestelling," + "dan wordt de speciale weergave gemaakt voor elke bestelling") @Test ongeldig test () gooit Uitzondering {// gegeven lijst anyOrderLines = OrderFixtureUtils.anyOrderLines (); Lijstorders = Arrays.asList (nieuwe Order (anyOrderLines), nieuwe SpecialOrder (anyOrderLines)); HtmlOrderViewCreator htmlOrderViewCreator = nieuwe HtmlOrderViewCreator (); // wanneer orders.get (0) .accept (htmlOrderViewCreator); String regularOrderHtml = htmlOrderViewCreator.getHtml (); orders.get (1) .accept (htmlOrderViewCreator); String specialOrderHtml = htmlOrderViewCreator.getHtml (); // dan assertThat (regularOrderHtml) .containsPattern ("

Totale kosten normale bestelling:. *

"); assertThat (specialOrderHtml) .containsPattern ("

totale prijs: .*

"); }

3. Dubbele verzending in DDD

In eerdere secties hebben we dubbele verzending en het bezoekerspatroon besproken.

We zijn nu eindelijk klaar om te laten zien hoe we deze technieken in DDD kunnen gebruiken.

Laten we teruggaan naar het voorbeeld van bestellingen en kortingsbeleid.

3.1. Kortingsbeleid als strategiepatroon

Eerder introduceerden we de Bestellen klasse en zijn totale prijs() methode die de som van alle orderregelitems berekent:

openbare klasse Order {public Money totalCost () {// ...}}

Er is ook de Kortingsbeleid interface om de korting voor de bestelling te berekenen. Deze interface is geïntroduceerd om verschillende kortingsregels te kunnen gebruiken en deze tijdens runtime te kunnen wijzigen.

Dit ontwerp is veel soepeler dan het simpelweg hardcoderen van alle mogelijke kortingspolissen in Bestellen klassen:

openbare interface DiscountPolicy {dubbele korting (bestelling bestellen); }

We hebben dit tot nu toe niet expliciet genoemd, maar in dit voorbeeld wordt het Strategiepatroon gebruikt. DDD gebruikt dit patroon vaak om te voldoen aan het Ubiquitous Language-principe en een lage koppeling te bereiken. In de DDD-wereld wordt het Strategiepatroon vaak Beleid genoemd.

Laten we eens kijken hoe we de dubbele verzendingstechniek en het kortingsbeleid kunnen combineren.

3.2. Dubbel verzend- en kortingsbeleid

Om het beleidspatroon correct te gebruiken, is het vaak een goed idee om het als argument door te geven. Deze benadering volgt het Tell, Don't Ask-principe dat een betere inkapseling ondersteunt.

Bijvoorbeeld de Bestellen class zou kunnen implementeren totale prijs zo:

openbare klasse Order / * ... * / {// ... openbaar geld totalCost (SpecialDiscountPolicy discountPolicy) {return totalCost (). multipliedBy (1 - discountPolicy.discount (dit), RoundingMode.HALF_UP); } // ...}

Laten we nu aannemen dat we elk type bestelling anders willen verwerken.

Bij het berekenen van de korting voor speciale bestellingen zijn er bijvoorbeeld enkele andere regels die informatie vereisen die uniek is voor de Speciale bestelling klasse. We willen gieten en reflecteren vermijden en tegelijkertijd voor elk de totale kosten kunnen berekenen Bestellen met de correct toegepaste korting.

We weten al dat overbelasting van de methode plaatsvindt tijdens het compileren. De natuurlijke vraag rijst dus: hoe kunnen we de logica van de orderkorting dynamisch naar de juiste methode sturen op basis van het runtime-type van de order?

Het antwoord? We moeten de volgordeklassen enigszins wijzigen.

De wortel Bestellen klasse moet tijdens runtime naar het argument van het kortingsbeleid worden verzonden. De eenvoudigste manier om dit te bereiken, is door een protected applyDiscountPolicy methode:

openbare klasse Order / * ... * / {// ... openbaar geld totalCost (SpecialDiscountPolicy discountPolicy) {return totalCost (). multipliedBy (1 - applyDiscountPolicy (discountPolicy), RoundingMode.HALF_UP); } beschermd dubbel applyDiscountPolicy (SpecialDiscountPolicy discountPolicy) {return discountPolicy.discount (dit); } // ...}

Dankzij dit ontwerp vermijden we dubbele bedrijfslogica in het totale prijs methode in Bestellen subklassen.

Laten we een demo van gebruik laten zien:

@DisplayName ("bij een normale bestelling met items ter waarde van $ 100 in totaal," + "bij toepassing van 10% kortingsbeleid," + "dan zijn de kosten na korting $ 90") @Test ongeldig test () werpt een uitzondering {// gegeven ordervolgorde = nieuw Order (OrderFixtureUtils.orderLineItemsWorthNDollars (100)); SpecialDiscountPolicy discountPolicy = nieuwe SpecialDiscountPolicy () {@Override openbare dubbele korting (bestelling bestelling) {return 0.10; } @Override openbare dubbele korting (SpecialOrder-bestelling) {return 0; }}; // when Money totalCostAfterDiscount = order.totalCost (discountPolicy); // dan assertThat (totalCostAfterDiscount) .isEqualTo (Money.of (CurrencyUnit.USD, 90)); }

Dit voorbeeld gebruikt nog steeds het bezoekerspatroon, maar in een licht gewijzigde versie. Orderklassen zijn zich daarvan bewust SpecialDiscountPolicy (de Bezoeker) heeft enige betekenis en berekent de korting.

Zoals eerder vermeld, willen we verschillende kortingsregels kunnen toepassen op basis van het runtime-type Bestellen. Daarom moeten we het beschermde applyDiscountPolicy methode in elke kindklasse.

Laten we deze methode overschrijven in Speciale bestelling klasse:

openbare klasse SpecialOrder verlengt Order {// ... @Override beschermd dubbel applyDiscountPolicy (SpecialDiscountPolicy discountPolicy) {return discountPolicy.discount (dit); } // ...}

We kunnen nu extra informatie gebruiken over Speciale bestelling in het kortingsbeleid om de juiste korting te berekenen:

@DisplayName ("bepaalde speciale bestelling komt in aanmerking voor extra korting met items ter waarde van $ 100 in totaal" + "wanneer 20% korting wordt toegepast voor bestellingen met extra korting," + "dan zijn de kosten na korting $ 80") @Test ongeldig test () werpt Uitzondering {// gegeven boolean SuitableForExtraDiscount = true; Order order = nieuwe SpecialOrder (OrderFixtureUtils.orderLineItemsWorthNDollars (100), SuitableForExtraDiscount); SpecialDiscountPolicy discountPolicy = nieuwe SpecialDiscountPolicy () {@Override openbare dubbele korting (bestelling bestelling) {return 0; } @Override openbare dubbele korting (SpecialOrder-bestelling) {if (order.isEligibleForExtraDiscount ()) return 0.20; 0,10 teruggeven; }}; // when Money totalCostAfterDiscount = order.totalCost (discountPolicy); // dan assertThat (totalCostAfterDiscount) .isEqualTo (Money.of (CurrencyUnit.USD, 80.00)); }

Bovendien, aangezien we polymorf gedrag in ordeklassen gebruiken, kunnen we de berekeningsmethode van de totale kosten gemakkelijk aanpassen.

4. Conclusie

In dit artikel hebben we geleerd hoe we dubbele verzending kunnen gebruiken en Strategie (ook bekend als Het beleid) patroon in Domain-driven design.

De volledige broncode van alle voorbeelden is beschikbaar op GitHub.