29.05.2019
Value-Objekte sind eines der DDD-Entwurfsmuster, die ich auch in jedem nicht-DDD-Projekt einsetzen würde. Denn sie sind eine sehr bedeutende Veränderung im Kleinen mit enormer Wirkung auf die Codequalität.
[
{
"attrs": [],
"content": "Ein Value-Objekt beschreibt einen Aspekt der Dom\u00e4ne ohne eigene Identit\u00e4t. Das bedeutet, es hat keine ID und ist kurzlebiger als Entities. Man k\u00f6nnte statt eines Value-Objekts also auch einen Basis-Datentyp benutzen. Allerdings bringt die Verwendung von Value-Objekten deutliche Vorteile:",
"id": "_5ixf093uy",
"type": "paragraph"
},
{
"attrs": [],
"content": "Die Lesbarkeit des Codes wird erh\u00f6ht",
"id": "_q5o38lamb",
"type": "ul"
},
{
"attrs": [],
"content": "Die Logik zu einem Aspekt ist da, wo sie hingeh\u00f6rt",
"id": "_50oxhvakm",
"type": "ul"
},
{
"attrs": [],
"content": "Der Code wird robuster",
"id": "_9omnopsge",
"type": "ul"
},
{
"attrs": [],
"content": "Werfen wir einen genaueren Blick auf diese Aspekte:",
"id": "_i61mphyx2",
"type": "paragraph"
},
{
"attrs": [],
"content": "Erh\u00f6hte Lesbarkeit#",
"id": "_6gi47dnw3",
"type": "h3"
},
{
"attrs": [],
"content": "Die Lesbarkeit wird durch Value-Objekte erh\u00f6ht, weil die Eigenschaft der Dom\u00e4ne, die sie repr\u00e4sentieren, bereits im Typ kodiert ist. Betrachten wir folgendes, zugegebenerma\u00dfen konstruiertes, Beispiel:",
"id": "_1lh7y5eya",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "customer.billingAddress(\n\t\"Max\",\n \"Mustermann\",\n \"Konsul-Smidt-Str.\", \n \"8g\",\n \"28217\",\n \"Bremen\"\n );",
"id": "_4u29y0px6",
"type": "code"
},
{
"attrs": [],
"content": "Diese Methode hat dann die verwirrende Signatur",
"id": "_r11z0mwmi",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "Customer billingAddress(\n String, String, String, String, String, String\n)",
"id": "_h418jl8c1",
"type": "code"
},
{
"attrs": [],
"content": "bei der man sich auf die Parameter-Namen verlassen muss, und die insbesondere auch den folgenden, falschen Aufruf akzeptiert:",
"id": "_4h2jnvgmw",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "customer.billingAddress(\n\t\"Mustermann\", \n \"Max\", \n \"Konsul-Smidt-Str.\", \n \"8g\",\n \"28217\", \n \"Bremen\")",
"id": "_ljhgt7pb7",
"type": "code"
},
{
"attrs": [],
"content": "Viel klarer hingegen ist diese Signatur:",
"id": "_zrrdq272c",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "Customer setAddress(\n\tFirstName, \n LastName, \n Street, \n HouseNumber, \n PostalCode, \n City)",
"id": "_56furzphr",
"type": "code"
},
{
"attrs": [],
"content": "Hier wird sich bereits der Compiler \u00fcber ung\u00fcltige Eingaben beschweren. Nat\u00fcrlich ist die zu hohe Anzahl der Paramter Teil des Problems, aber ist ja auch nur ein Beispiel. Mit vier String-Parametern w\u00e4re die Situation nicht wirklich besser.",
"id": "_ez6sstue4",
"type": "paragraph"
},
{
"attrs": [],
"content": "Logik da, wo sie hingeh\u00f6rt",
"id": "_6iblpvbeg",
"type": "h3"
},
{
"attrs": [],
"content": "Ein Value-Objekt erlaubt mir, die Fachlogik und die Daten gemeinsam zu halten. Betrachten wir das Beispiel eines Einkaufs per SEPA-Mandat. Dazu k\u00f6nnte die folgende Methode am Warenkorb aufgerufen werden:",
"id": "_zoko0j2w3",
"type": "paragraph"
},
{
"attrs": {
"language": "java"
},
"content": "basket.payPerDirectDebit(\n \t\"DE94 2919 0024 0012 3456 00\", \n \"GENODEF1HB1\"\n);",
"id": "_er0xyn23q",
"type": "code"
},
{
"attrs": [],
"content": "Auch hier gilt nat\u00fcrlich, dass `Basket payPerDirectDebit(IBAN, BIC)` besser zu lesen ist, als `Basket payPerDirectDebit(String, String)`.
Dar\u00fcber hinaus kann so aber auch die Validierungs-Logik f\u00fcr eine g\u00fcltige IBAN in der IBAN-Klasse selbst (statt in irgendeinem Service) abgelegt werden:", "id": "_b16gn42y8", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public IBAN(String value) {\n\tif (!valid(value)) {\n \tthrow new InvalidValueException(value);\n }\n}", "id": "_79pxsnhs3", "type": "code" }, { "attrs": [], "content": "Nat\u00fcrlich kann man auch hier von der reinen Lehre abweichen und in Value-Objekten ung\u00fcltige Werte erlauben. Das ist aber ein Thema f\u00fcr einen anderen Artikel und soll auch in einem anderen Artikel behandelt werden.", "id": "_Ygn3cLI2l", "type": "paragraph" }, { "attrs": [], "content": "Ein weiterer Vorteil der Kapselung der IBAN in einem Value-Objekt: Man begegnet immer wieder Web-Formularen, bei denen man die IBAN nur maschinenlesbar (also nicht in Vierer-Bl\u00f6cken gruppiert) eingeben kann - schon ist die UX ruiniert. Auch die Aufgabe der Formatierung und Normalisierung einer IBAN kann diese Klasse \u00fcbernehmen: Sie akzeptiert Eingaben mit Leerzeichen und bietet die Ausgabeformate", "id": "_3vbHs0K2u", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public String toHumanReadableString()\npublic String toMachineParseableString()", "id": "_v8uj2bmpb", "type": "code" }, { "attrs": [], "content": "an.", "id": "_2kygxj19x", "type": "paragraph" }, { "attrs": [], "content": "Robustheit", "id": "_nrpqk3my6", "type": "h3" }, { "attrs": [], "content": "Die Robustheit folgt eigentlich nur daraus, dass die Logik dort ist, wo sie sein sollte. Nehmen wir an, ein Shop speichert den Warenkorb ganz klassisch in einer relationalen Datenbank und arbeitet nicht mit Value-Objekten. Die Entity f\u00fcr Warenkorb-Positionen k\u00f6nnte dann so aussehen:", "id": "_ant87x9js", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public class LineItem {\n Long lineItemId;\n String skuCode;\n int quantity;\n}", "id": "_oyv85b1j4", "type": "code" }, { "attrs": [], "content": "F\u00fcr den Use Case, die Anzahl einer Warenkorbposition zu \u00e4ndern, g\u00e4be es dann beispielsweise diese Methode:", "id": "_423877k4d", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "Basket updateLineItem(Long lineItemId, int quantity)", "id": "_vjgtynr5l", "type": "code" }, { "attrs": [], "content": "Nun stellt sich die Frage, *wo* die Eingabe validiert wird? Sprich, an welcher Stelle \u00fcberpr\u00fcfe ich, dass die `quantity` gr\u00f6\u00dfer Null ist? Die naheliegende Antwort ist, in der `updateLineItem`-Methode. Was ist aber, wenn im weiteren Projektverlauf aus welchen Gr\u00fcnden auch immer im `LineItemRepository` ebenfalls eine Methode zur \u00c4nderung der Anzahl eingef\u00fchrt wird? Die Wahrscheinlichkeit, dass es dort vergessen wird, ist hoch. Wahrscheinlich kann man deshalb auch erstaunlich vielen Shops immer noch eine negative Anzahl f\u00fcr eine Warenkorb-Position unterjubeln.", "id": "_Ob5DbCErt", "type": "paragraph" }, { "attrs": [], "content": "Wenn die Entity f\u00fcr die Warenkorb-Positionen so auss\u00e4he, w\u00e4re das Problem zentral gel\u00f6st:", "id": "_5A5kWsH93", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public class LineItem {\n LineItemId lineItemId;\n SkuCode skuCode;\n Quantity quantity;\n}", "id": "_fqzkplmj5", "type": "code" }, { "attrs": [], "content": "Das Quantity-Objekt kann im Konstruktor \u00fcberpr\u00fcfen, dass der Wert nicht kleiner als 1 ist. So ist *immer* sichergestellt, dass eine Warenkorb-Position eine g\u00fcltige Anzahl besitzt.", "id": "_Yt2ctAJwx", "type": "paragraph" }, { "attrs": [], "content": "Ein weiterer wichtiger Punkt f\u00fcr Robustheit ist die Vermeidung von `null`-Werten und somit von `NullPointerExceptions`. Damit erspart man sich die `null`-Checks, die an irgendeiner Stelle bestimmt vergessen werden, daf\u00fcr aber an anderen Stellen \u00fcberfl\u00fcssig sind.", "id": "_3QdJZz9rz", "type": "paragraph" }, { "attrs": [], "content": "Dies kann man zum Einen durch `Optional` oder (link:http://www.vavr.io/ text: Vavrs target: _blank) `Option` vermeiden, zum Anderen aber auch dadurch, dass ein Value-Objekt einen leeren Wert haben kann. Die Adresse aus dem Beispiel weiter oben k\u00f6nnte beispielsweise einen Adresszusatz haben, der meistens leer sein wird. Statt hier `null` zu verwenden, kann man lieber einen Empty-Wert definieren:", "id": "_GqPgo1QXP", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "class Address {\n\t...\n StreetAppendix streetAppendix = StreetAppendix.EMPTY;\n ...\n} ", "id": "_cdep1d19c", "type": "code" }, { "attrs": [], "content": "Dieser kann dann ohne `null`-Checks verwendet werden, indem er z.B. zu einem Leerstring serialisiert wird.", "id": "_60n8esl72", "type": "paragraph" } ]
Dar\u00fcber hinaus kann so aber auch die Validierungs-Logik f\u00fcr eine g\u00fcltige IBAN in der IBAN-Klasse selbst (statt in irgendeinem Service) abgelegt werden:", "id": "_b16gn42y8", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public IBAN(String value) {\n\tif (!valid(value)) {\n \tthrow new InvalidValueException(value);\n }\n}", "id": "_79pxsnhs3", "type": "code" }, { "attrs": [], "content": "Nat\u00fcrlich kann man auch hier von der reinen Lehre abweichen und in Value-Objekten ung\u00fcltige Werte erlauben. Das ist aber ein Thema f\u00fcr einen anderen Artikel und soll auch in einem anderen Artikel behandelt werden.", "id": "_Ygn3cLI2l", "type": "paragraph" }, { "attrs": [], "content": "Ein weiterer Vorteil der Kapselung der IBAN in einem Value-Objekt: Man begegnet immer wieder Web-Formularen, bei denen man die IBAN nur maschinenlesbar (also nicht in Vierer-Bl\u00f6cken gruppiert) eingeben kann - schon ist die UX ruiniert. Auch die Aufgabe der Formatierung und Normalisierung einer IBAN kann diese Klasse \u00fcbernehmen: Sie akzeptiert Eingaben mit Leerzeichen und bietet die Ausgabeformate", "id": "_3vbHs0K2u", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public String toHumanReadableString()\npublic String toMachineParseableString()", "id": "_v8uj2bmpb", "type": "code" }, { "attrs": [], "content": "an.", "id": "_2kygxj19x", "type": "paragraph" }, { "attrs": [], "content": "Robustheit", "id": "_nrpqk3my6", "type": "h3" }, { "attrs": [], "content": "Die Robustheit folgt eigentlich nur daraus, dass die Logik dort ist, wo sie sein sollte. Nehmen wir an, ein Shop speichert den Warenkorb ganz klassisch in einer relationalen Datenbank und arbeitet nicht mit Value-Objekten. Die Entity f\u00fcr Warenkorb-Positionen k\u00f6nnte dann so aussehen:", "id": "_ant87x9js", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public class LineItem {\n Long lineItemId;\n String skuCode;\n int quantity;\n}", "id": "_oyv85b1j4", "type": "code" }, { "attrs": [], "content": "F\u00fcr den Use Case, die Anzahl einer Warenkorbposition zu \u00e4ndern, g\u00e4be es dann beispielsweise diese Methode:", "id": "_423877k4d", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "Basket updateLineItem(Long lineItemId, int quantity)", "id": "_vjgtynr5l", "type": "code" }, { "attrs": [], "content": "Nun stellt sich die Frage, *wo* die Eingabe validiert wird? Sprich, an welcher Stelle \u00fcberpr\u00fcfe ich, dass die `quantity` gr\u00f6\u00dfer Null ist? Die naheliegende Antwort ist, in der `updateLineItem`-Methode. Was ist aber, wenn im weiteren Projektverlauf aus welchen Gr\u00fcnden auch immer im `LineItemRepository` ebenfalls eine Methode zur \u00c4nderung der Anzahl eingef\u00fchrt wird? Die Wahrscheinlichkeit, dass es dort vergessen wird, ist hoch. Wahrscheinlich kann man deshalb auch erstaunlich vielen Shops immer noch eine negative Anzahl f\u00fcr eine Warenkorb-Position unterjubeln.", "id": "_Ob5DbCErt", "type": "paragraph" }, { "attrs": [], "content": "Wenn die Entity f\u00fcr die Warenkorb-Positionen so auss\u00e4he, w\u00e4re das Problem zentral gel\u00f6st:", "id": "_5A5kWsH93", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "public class LineItem {\n LineItemId lineItemId;\n SkuCode skuCode;\n Quantity quantity;\n}", "id": "_fqzkplmj5", "type": "code" }, { "attrs": [], "content": "Das Quantity-Objekt kann im Konstruktor \u00fcberpr\u00fcfen, dass der Wert nicht kleiner als 1 ist. So ist *immer* sichergestellt, dass eine Warenkorb-Position eine g\u00fcltige Anzahl besitzt.", "id": "_Yt2ctAJwx", "type": "paragraph" }, { "attrs": [], "content": "Ein weiterer wichtiger Punkt f\u00fcr Robustheit ist die Vermeidung von `null`-Werten und somit von `NullPointerExceptions`. Damit erspart man sich die `null`-Checks, die an irgendeiner Stelle bestimmt vergessen werden, daf\u00fcr aber an anderen Stellen \u00fcberfl\u00fcssig sind.", "id": "_3QdJZz9rz", "type": "paragraph" }, { "attrs": [], "content": "Dies kann man zum Einen durch `Optional` oder (link:http://www.vavr.io/ text: Vavrs target: _blank) `Option` vermeiden, zum Anderen aber auch dadurch, dass ein Value-Objekt einen leeren Wert haben kann. Die Adresse aus dem Beispiel weiter oben k\u00f6nnte beispielsweise einen Adresszusatz haben, der meistens leer sein wird. Statt hier `null` zu verwenden, kann man lieber einen Empty-Wert definieren:", "id": "_GqPgo1QXP", "type": "paragraph" }, { "attrs": { "language": "java" }, "content": "class Address {\n\t...\n StreetAppendix streetAppendix = StreetAppendix.EMPTY;\n ...\n} ", "id": "_cdep1d19c", "type": "code" }, { "attrs": [], "content": "Dieser kann dann ohne `null`-Checks verwendet werden, indem er z.B. zu einem Leerstring serialisiert wird.", "id": "_60n8esl72", "type": "paragraph" } ]