How do you know if two objects are equal? One breakdown is to classify objects as value objects or reference objects. Value objects compare as equal when their component values are equal. Reference objects compare as equal only when it’s the exact same object. (Two separate reference objects containing identical values are not considered equal.)
This breakdown is not pure – you sometimes see objects that are considered equal if an id field is equal, even if other attributes differ. Or you see value objects that allow some fields to differ. In general, however, your code will be easier to work with if you don’t mix up the styles.
Immutability
Immutability – not changing once the value is set – is very common in value objects. This lets you hand off a value and not worry that some unexpected change can happen. Sometimes this is enforced in the language – for example, structs in Swift are value objects, and cannot be changed once initialized.
I’ve heard a story of early FORTRAN implementations: In FORTRAN, arguments are passed by reference. You could pass a constant to a function that modified its value, and from there out anywhere in the application that referenced that constant got the wrong value. We have to work harder to make this mistake, but mutable value objects are a valiant attempt.
Where it’s not required, immutability is one of those “good ideas” you should evade only consciously and carefully. Eric Evans (Domain-Driven Design) says, “If a VALUE’s implementation is to be mutable, then it must not be shared.”
I don’t specifically test immutability, but I do make sure the design reflects it: fields are only set in the constructor, with no means to change them once set.
The Key Test for Value Objects
There’s one main type of test I use for “value object nature”. I learned it from James Shore.
func test_valueObject { let same1 = new ValueObject(a1, b1, c1, ...) let same2 = new ValueObject(a1, b1, c1, ...) let different1 = new ValueObject(a2, b1, c1, ...) // *** assertEquals(same1, same2) assertNotEquals(same1, different1) assertEquals(same1.hashcode(), same2.hashcode()) }
In Java, you can check that instances of different classes are unequal, but I normally don’t use equals()
that way. In some languages, equality and hash aren’t both required; you could use separate tests if you wanted.
The note (***) is to point out that you could test every unequal value one at a time, with differentN having a difference only in the Nth argument. I often do this, but I’ve never felt the need to test combinations of two or more differences. (But if that check is important for your application or peace of mind, do check it.)
Value Objects – Moving Beyond Data Bags
Value objects are often “small” objects: name, address, zip or postal code, money, ranges, points, pairs. Using these objects can greatly simplify code. Your value objects may start out as “data bags” – simple collections of values, with no behavior.
If you look at its clients, you will typically find that there are patterns of use that can move to the value object class. For example, clients of Point may be calculating distance, when Point has all the information it needs. You can look at that as Feature Envy or Duplication, but either way the design will usually be better if you move that method to Point.
You don’t have to wait, of course – you can be alert for these opportunities while you’re implementing a class rather than waiting to refactor.
Value Objects – Types with Operators
Some value objects grow beyond this: they implement arithmetic-like operations that produce more of the same value object. (It’s a spectrum; a class may start out as a data bag and end up here.)
I look for functions or operations that take and return the ValueObject. Occasionally, there will be functions using other types but that still suggest operations. (For example, a Money class might have operations to add two Moneys, and one to multiply a Money by an Integer.)
If you create a test something like this:
func test_operation() { let v1 = new ValueObject(...) let v2 = new ValueObject(...) let actual = v1.operation(v2) assertEquals(new ValueObject(...), result) }
it will induce you to create something like this:
func add(ValueObject vo) -> ValueObject { let values = some combo of this & vo's values return new ValueObject (values) }
I typically have two tests: one that combines an arbitrary value with an identity object (if it exists), and another that tests combining two arbitrary values.
For example, if you create a method to add(Point)
, Point(0,0)
is an identity since Point(0,0).add(p)
== p
and p.add(Point(0,0)
) == p
.
Depending on the (algebraic) structure, you may have properties you want such as associativity or commutativity. We’ll explore this more deeply another time.
Controlling Creation – Flyweight Pattern
If identically-valued value objects will be numerous, you may want to consider using the Flyweight pattern. Flyweight supports sharing of objects.
To drive this into existence, test for a Factory Method – a class responsible for creating instances of another class. Here, it will manage the set of Flyweight objects, reusing old ones or creating new ones as needed. Most clients that create this type of value object should be designed to go through the factory rather than constructing in place.
This is a performance optimization – shared (immutable) values trade off more expensive creation for less memory pressure.
Conclusion
Value objects determine equality by comparing their contents, not an address or id. They simplify code by providing a home for manipulation of their values. The key test is a test that equality is driven by the values it contains. Even a simple class that starts life as a data bag can grow into an interesting and useful value object.
References
Design Patterns, by Erich Gamma et al. (Flyweight)
Domain-Driven Design, by Eric Evans. (Value Objects)
“ValueObject“, by Martin Fowler. Retrieved 2020-12-26.