But how shall we design these objects? Let's talk about their fundamental properties.

Imagine we're building a weather app.
We recognized that floats weren't the right fit to express temperatures, because it was odd to ask if "a float is cold". Better to extract Temperature.
We call it a value object.

Comparability

If we look at the implementation of #cold?, we spot the same problem again: we're using a float to represent a model domain concept: the cold threshold temperature. That's primitive obsession biting again! We can use Temperature instead.

Comparable requires to define the spaceship operator (aka #<=>) and to return -1, 0 or 1 if other is respectively lesser, equal, or greater than. Now, thanks to Comparable, our Temperature is able to respond to comparability checks like lesser than (#<), or equal to (#==).

We're also capable to check if two temperatures are equal if they carry the same value.

Equality

Now we need to get a list of all the unique temperatures of the last month. We start from raw data which include duplicates:

Immutability

Because we delegate important behaviors to the encapsulated value, we want it to not change. When we implement a hashing strategy we want it to be constant until the end of the program execution. If the value changes, we can't meet this requirement. We should freeze it.

In our case, the value that we hold is a Float, which is already frozen. But if we have other types like strings, hashes, arrays, we should explicitly do that. Preferably, we should define the getter as protected to prevent accidental data modifications.

Conclusion

We designed Temperature to be responsible for specific model domain questions, but also to behave like a primitive.

If you think, our language type system is made of basic "generic primitives", on top of which we build another layer of model domain "specific primitives": the value objects.