Idiome in der Softwareentwicklung: Value-Objekte

Das Konzept Value-Objekt stammt aus dem Buch "Domain Driven Design" von Eric Evans. Die Gleichheit beruht auf dem Zustand, aber nicht der Identität des Objekts.

In Pocket speichern vorlesen Druckansicht 37 Kommentare lesen
Rechner

(Bild: Kwangmoozaa/Shutterstock.com)

Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

In der Softwareentwicklung ist ein Value-Objekt ein kleines Objekt, dessen Gleichheit auf seinem Zustand, aber nicht auf seiner Identität basiert. Typische Value-Objekte sind Geld, Zahlen oder Strings.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Der Begriff Value-Objekt geht auf das bahnbrechende Buch Domain-Driven Design (DDD) von Eric Evans zurück. Aber wie sieht ein Value-Objekt aus? Eric gab die Antwort in seinem Buch: "An object that represents a descriptive aspect of the domain with no conceptual identity is called a Value Object. Value Objects are instantiated to represent elements of the design that we care about only for what they are, not who or which they are."

Das klingt recht formal, aber hier ist ein schönes Beispiel des Autors: "When a child is drawing, he cares about the color of the marker he chooses, and he may care about the sharpness of the tip. But if there are two markers of the same color and shape, he probably won’t care which one he uses. If a marker is lost and replaced by another of the same color from a new pack, he can resume his work unconcerned about the switch."

Der Schlüsselbegriff in der formalen Definition und im Beispiel zum Value-Objekt war Gleichheit.

Im Allgemeinen gibt es zwei Arten von Gleichheit: Referenzgleichheit und Wertgleichheit. Der Einfachheit halber werde ich die id-basierte Gleichheit ignorieren.

  • Referenzgleichheit: Zwei Objekte gelten als gleich, wenn sie auf dieselbe Entität im Speicher verweisen.
  • Wertgleichheit: Zwei Objekte gelten als gleich, wenn alle ihre Mitglieder den gleichen Wert haben.

Mit Python ist es ziemlich einfach, beide Arten von Gleichheit gegenüberzustellen:

Das kurze Beispiel in der Python-Shell sollte den Unterschied zwischen Referenzgleichheit und Wertgleichheit deutlich machen.

Zuerst definiere ich zwei Listen list1 und list2 mit denselben Elementen. Wenn ich ihre Identität vergleiche (list1 is list2), sind sie unterschiedlich. Wenn ich ihre Werte vergleiche, sind sie identisch. Python verwendet für den Gleichheits- (und Nicht-Gleichheits-) Vergleich die Speicheradresse der verglichenen Objekte. Der id-Operator (id(liste1)) gibt eine dezimale Darstellung der hexadezimalen Speicheradresse zurück. Durch die Zuweisung von list1 an list3 verweisen beide Listen auf denselben Speicherplatz. Folglich ist id(list3) identisch mit id(list1), und der Aufruf list1 is list3 gibt true zurück.

Wie wirkt sich das auf das moderne C++20 aus? In C++20 kann der Compiler den Gleichheitsoperator erzeugen.

Compiler-generierte Gleichheitsoperator

Für einen benutzerdefinierten Datentyp muss man die passende Gleichheitssemantik wählen.

// equalityReferenceValue.cpp

#include <iostream>

class Date{
 public:
    Date(int y, int m, int d): year(y), month(m), day(d){}
    bool operator==(const Date&) const = default;
 private:
    int year;
    int month;
    int day;
};

class Man{
 public:
    Man(const std::string n, int a): name(n), age(a){}
    bool operator==(const Man&) const = default;
 private:
    std::string name;
    int age;
};

int main() {

    std::cout << std::boolalpha << '\n';

    Date date1(2022, 10, 31);
    Date date2(2022, 10, 31);

    std::cout << "date1 == date2: " << (date1 == date2) << '\n';
    std::cout << "date1 != date2: " << (date1 != date2) << '\n';

    std::cout << '\n';

    Man man1("Rainer Grimm", 56);
    Man man2("Rainer Grimm", 56);

    std::cout << "man1 == man2: " << (man1 == man2) << '\n';
    std::cout << "man1 != man2: " << (man1 != man2) << '\n';

    std::cout << '\n';

}

In C++20 kann der Compiler den Gleichheitsoperator automatisch generieren und ihn als Ersatz für den Ungleichheitsoperator verwenden. Der automatisch erzeugte Gleichheitsoperator wendet Wertgleichheit an. Genauer gesagt führt der vom Compiler erzeugte Gleichheitsoperator einen lexikografischen Vergleich durch. Lexikografischer Vergleich bedeutet, dass alle Basisklassen von links nach rechts und alle nichtstatischen Mitglieder der Klasse in der Reihenfolge ihrer Deklaration verglichen werden.

Ich muss zwei wichtige Punkte ergänzen:

  • Für Strings und Vektoren gibt es eine Vereinfachung: Die vom Compiler erzeugten Operatoren == und != vergleichen zuerst ihre Länge und dann gegebenenfalls ihren Inhalt.

Ehrlich gesagt, verhält sich das Programm wie erwartet, fühlt sich aber nicht richtig an.

Zwei Datumsangaben mit identischen Werten sollten als gleich angesehen werden, aber nicht zwei Männer. Die Gleichheit von zwei Männern sollte auf ihrer Identität basieren und nicht auf ihrem Namen und Alter.

Nun möchte ich auf die Details zu Value-Objekten eingehen.

Eigenschaften

  • Wert-Gleicheit

Nach dem letzten Kapitel sollte dies offensichtlich sein. Die Gleichheit eines Value-Objekts sollte auf seinem Zustand und nicht auf seiner Identität beruhen.

  • Unveränderlichkeit

Ein Value-Objekt sollte nicht veränderbar sein. Damit sind Value-Objekte ideale Kandidaten für Concurrency. Das Ändern eines Value-Objekts bedeutet, dass ein neues Objekt mit den geänderten Attributen erstellt wird. Diese Eigenschaft, dass eine Operation auf ein unveränderliches Objekt ein neues Objekt zurückgibt, hat zwei interessante Konsequenzen. Mit Python lässt sich dies einfach auf den Punkt bringen.

In Python ist ein String unveränderlich:

  1. Man simuliert eine Änderung, indem man dem neuen Wert den alten Namen zuweist: s = s.upper(). Das ursprüngliche s und das neue s haben unterschiedliche Adressen.
  2. Eine Operation an der Zeichenkette gibt eine neue Zeichenkette zurück. Folglich lassen sich String-Operationen verketten. In der funktionalen Programmierung wird dieses Muster als Fluent Interface bezeichnet. Übrigens: Arithmetische Ausdrücke wie (5 +5) * 10 - 20 basieren auf dem Fluent Interface. Jede Operation gibt ein temporäres Objekt zurück, auf das man die nächste Operation anwenden kann. Natürlich sind Zahlen Value-Objekte.
  • Selbst-Validierung

Ein Value-Objekt sollte seine Attribute beim Erstellen validieren. Der Einfachheit halber habe ich diesen Schritt in meiner vorherigen Date-Klasse übersprungen.

Was sind die Vor- und Nachteile von Value-Objekten?

Vor- und Nachteile

Die Vorteile von Value-Objekten überwiegen deutliche ihre Nachteile.

  • Rich Types

Für den Umgang mit einfachen Werten sollte man reichhaltige Datentypen (rich types) anstelle von built-in Datentypen verwenden. Das hat viele Auswirkungen. Vergleichen wir die folgenden zwei Beispiele für ein Datum:

Date date1(2022, 10, 5);

std::string date2 = "2022 10 5";  
  • Es lässt sich kein ungültiges Datum Date(2022, 15, 5) erstellen, da der Konstruktor die Aufgabe hat, die Eingabe zu überprüfen. Das gilt nicht für den String-Wert "2022 15 5", da sich der Monat und Tag leicht verwechseln lässt.
  • Dein Programm ist einfacher zu lesen. Aus der Dokumentation der Klasse Date geht klar hervor, wie die einzelnen Komponenten zu verstehen sind.
  • Man kann die Operatoren für Date überladen. Zum Beispiel ergibt die Subtraktion zweier Daten eine Zeitdauer. Eine Zeitdauer sollte auch ein Value-Objekt sein.
  • Value-Objekte lassen sich mit benutzerdefinierten Literalen für einen Tag, ein Jahr und einen Monat erweitern. In diesem Fall ist dies nicht nötig, denn wir haben sie mit C++20 erhalten: std::chrono::duration auf cppreference.com.
  • Performanz

Value-Objekte sind unveränderlich. Dadurch bieten sie dem Optimierer zusätzliche Garantien und können ohne Synchronisierung zwischen Threads geteilt werden.

  • Zu viele Klassen

Nur um der Argumente willen: Man könnte am Ende zu viele kleine Klassen haben, die Value-Objekte repräsentieren.

Ein Null-Objekt kapselt ein Tue-Nichts-Verhalten innerhalb eines Objekts. In meinem nächsten Artikel werde ich auf Null-Objekte genauer eingehen.

When you book it before 24/02/2023, you will get a 33% discount. But let me first present my mentoring program.

Do you want to be a mentee? Get the details here: "Design Patterns and Architectural Patterns with C++", and become member of a C++ community. (rme)