zurück zum Artikel

Idiome in der Softwareentwicklung: Value-Objekte

Rainer Grimm
Rechner

(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.

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 [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.

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

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:

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

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

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 [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.

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.

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

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

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++ [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