Value Objects
A value object is a small object whose equality is based on state, but not identity. Typical value objects are money, numbers, or strings.
The term Value Object goes back to the seminal book Domain-Driven Design (DDD) by Eric Evans. But, what is a value object? Eric gave the answer in his book:
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.
If this is too formal for you, here is a nice example of the author:
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.
The key term in the formal definition and the example about the Value Object is equality.
Identity
In general, we have two types of equality: reference equality and value equality. For simplicity reasons, I will ignore id-based equality.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- Reference equality: two objects are considered to be equal if they reference the same entity in the memory.
- Value equality: two objects are considered to be equal if all their member have the same value.
Let me switch to Python because Python makes it easy to compare both kinds of equality.
The short example in the Python shell should make the difference between reference equality and value equality clear.
First, I define two lists list1
and list2
with the same elements. When I compare their identity (list1 is list2
), they are different. When I compare their values, they are identical. Python uses for equality (and non-equality) comparison of the memory address of the compared objects. The id
operator (id(liste1)
) returns a decimal representation of its hexadecimal memory address. By assigning list1
to list3,
both lists refer to the same memory location. Consequentially, id(list3)
is identical to id(list1)
, and the call list1
is list3
returns True
.
What does this mean for modern C++20? In C++20, the compiler can generate the equality operator.
Compiler-Generated Equality Operator
For a user-defined type, you have to choose the appropriate equality semantics.
// 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, the compiler can auto-generate the equality operator and use it as a fallback for the inequality operator. The auto-generated equality operator applies value equality. To be more precise, the compiler-generated equality operator performs a lexicographical comparison. Lexicographical comparison means that all base classes are compared left to right and all nonstatic members of the class in their declaration order.
I have to add two important points:
- For strings or vectors, there is a shortcut: the compiler-generated == and != operators compare first their lengths and then their content if necessary.
- The compiler-generated three-way comparison operator (
<=>
) also applies value equality. You can read more about the three-way comparison operator in my previous posts:
Honestly, the example works as expected but does not seem right.
Two dates with identical values should be regarded as equal but not two men. The equality of two men should be based on their identity and not on their name and age.
Value Objects
Let me discuss the details about Value Objects.
Properties
Value Equality
After the last chapter, this should be obvious. The equality of a Value Object should be based on its state and not on its identity.
Immutability
A Value Object should not be mutable. This makes Value Objects ideal candidates for concurrency. Changing a Value Object means creating a new one with the modified attributes. This property, that an operation on an immutable object returns a new object has two excellent properties. For conciseness, I use Python once more.
In Python, a string is immutable:
- You simulate modification, by assigning the new value to the old name: s = s.upper()
.
The originals
and the news
have differentaddresses.
- An operation on the string returns a new string. Consequentially, you can chain string operations. In the function domain, this pattern is called a fluent interface. By the way: arithmetic expressions such as
(5 +5) * 10 - 20
are based on the fluent interface. Each operation returns a temporary, on which you can apply the next operation. Of course, numbers are Value Objects.
Self-Validation
A Value Object should validate its attributes when created. For simplicity, I skipped this step in my previous Date
class.
What are the Pros and Cons of Value Objects:
Pros and Cons
The pros of Value Objects overweight their cons heavily.
Rich Types
You should use rich types instead of built-in types for dealing with primitive values. This has many implications. Let me compare the following two representations of a date:
Date date1(2022, 10, 5); std::string date2 = "2022 10 5";
- You cannot create an invalid date
Date(2022, 15, 5)
, because the constructor’s job is it to validate the input. This does not hold for the string value “2022 15 5"
, because someone mixed up the month and the day. - Your program is easier to read. It is crystal clear from class
Date
documentation, what each component stands for. - You can overload operators on
Date
. E.g.: subtracting two dates returns a time duration. A time duration should also be a Value Object. - You can extend your Value Objects with user-defined literals for a day, a year, and a month. In this case, it is not necessary because we have them with C++20: std::chrono::duration on cppreference.com.
Performance
Value Objects are immutable. Consequentially, they give the optimizer additional guarantees and can be shared between threads without synchronization.
Proliferation of Classes
Only for the sake of arguments: You may end with too many small classes representing Value Objects.
What’s Next?
A Null Object encapsulates a do nothing behavior inside an object. Let me show you in my next post the advantages of Null Objects.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!