Idiome in der Softwareentwicklung: Value-Objekte
![Rechner](https://heise.cloudimg.io/width/700/q75.png-lossy-75.webp-lossy-75.foil1/_www-heise-de_/imgs/18/3/6/9/2/5/4/0/shutterstock_1551975515-bad99eee7a7c63ef.jpeg)
(Bild: Kwangmoozaa/Shutterstock.com)
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 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.
![](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/9/2/5/4/0/ClassIdioms-7ef4ec3577764a73.png)
Der Begriff Value-Objekt geht auf das bahnbrechende Buch Domain-Driven Design [1] (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.
Identität
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:
![](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/9/2/5/4/0/EqualityPythonNew-6bec492500a1b873.png)
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.
- Der Compiler-generierte Drei-Wege-Vergleichsoperator (
<=>
) wendet die Wertgleichheit an. Mehr über den Drei-Wege-Vergleichsoperator finden sich in meinen älteren Artikeln:
Ehrlich gesagt, verhält sich das Programm wie erwartet, fühlt sich aber nicht richtig an.
![](https://heise.cloudimg.io/width/454/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/9/2/5/4/0/equalityReferenceValue-ced2c9f81b23451b.png)
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.
Value-Objekte
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:
![](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/9/2/5/4/0/immutable-0d5385f0f4449e6a.png)
- Man simuliert eine Änderung, indem man dem neuen Wert den alten Namen zuweist:
s = s.upper()
. Das ursprüngliches
und das neues
haben unterschiedliche Adressen. - 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 [5] bezeichnet. Übrigens: Arithmetische Ausdrücke wie (5 +5) * 10 - 20 basieren auf dem Fluent Interface [6]. 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 [7].
- 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.
Wie geht's weiter?
Ein Null-Objekt kapselt ein Tue-Nichts-Verhalten innerhalb eines Objekts. In meinem nächsten Artikel werde ich auf Null-Objekte genauer eingehen.
In eigener Sache: Modernes C++ Mentoring: "Design Patterns and Architectural Patterns with C++"
When you book it before 24/02/2023, you will get a 33% discount. But let me first present my mentoring program.
![](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/6/9/2/5/4/0/RG_E-Cover_M2_DPAC_1080x1080_2022-11-27-31b8bcda7266eb22.png)
Do you want to be a mentee? Get the details here: "Design Patterns and Architectural Patterns with C++ [8]", and become member of a C++ community. (rme [9])
URL dieses Artikels:
https://www.heise.de/-7483633
Links in diesem Artikel:
[1] https://www.oreilly.com/library/view/domain-driven-design-tackling/0321125215/
[2] https://www.heise.de/blog/C-20-Der-Drei-Weg-Vergleichsoperator-4782690.html
[3] https://www.heise.de/blog/C-20-Mehr-Details-zum-Spaceship-Operator-4790117.html
[4] https://www.heise.de/blog/C-20-Optimierte-Vergleiche-mit-dem-Spaceship-Operator-4797164.html
[5] https://en.wikipedia.org/wiki/Fluent_interface
[6] https://en.wikipedia.org/wiki/Fluent_interface
[7] https://en.cppreference.com/w/cpp/chrono/duration
[8] https://www.modernescpp.org/design-patterns-and-architectural-patterns-with-c/
[9] mailto:rme@ix.de
Copyright © 2023 Heise Medien