The Difference between Entity and Value Object, and How They Relate to Swift's Identifiable and Equatable Protocols
Helge Heß recently posted on Mastodon that he “still find[s] it disturbing how many #SwiftLang devs implement Equatable
as something that breaks the Equatable contract, just to please some API requiring it. Feels like they usually should implement Identifiable
and build on top of that instead.”
An interesting Swift piece he shared is to not slap Hashable
onto a type just to reap the benefits of Set
logic and uniqueness in collections. This usually starts by conforming to Hashable
but then only pick one or a few properties for the hash computation. That’s basically a code smell. Consider to use an identifier that’s hashable instead if you want to “hash by ID” anyway:
struct Foo: Identifiable {
let id: UUID
let otherPropertyYouWantToIgnore: String = ...
// ...
}
// Set<Foo> then becomes:
var foos: [Foo.ID : Foo] = [...]
Never thought of that, so kudos!
Update 2023-01-18: Here’s a Dictionary.init
extension to simplify getting these instead of Set
s.
But I really want to bring attention to the abuse of Equatable
he mentioned.
To me, it looks like users of the Swift language may be conflating the following:
- Value Object and value type (e.g.
struct
andenum
), and - Entity and reference type (e.g.
class
andactor
).
The capitalized ones are concepts – the rest is a language construct. (I encountered the most succinct and sensible definitions of these concepts in Domain-Driven Design, but they are older.)
Entities have some kind of identity over time. In practice, this is often accompanyied by some kind of persistence. You fetch the same SQL database table row 10 times, but it’s supposed to be the same thing, no matter if the type of the resulting object in main memory has a reference type or value type. This applies to e.g. customers and products, or posts and tweets.
Value Objects, on the other hand, are considered to be equal in virtue of their data. If you have two Value Objects in your app’s memory with the same attributes (in Swift parlance: properties), then they are the same thing. This is used for stuff like money/currency, time, addresses, and names.
In Swift, i.e. in code and not in land of concepts, you can get “identity” of two objects by using the Identifiable
protocol, no matter if it’s used on a struct
or class
.
You make different objects of the same type equal via the Equatable
protocol, also no matter if used on a struct
or class
.
We had Value Objects in Objective-C, a language without the value types we know from Swift, by overriding isEqual:
to make the NSObject
subclasses compare their objects not by memory address, but by their properties. In other words, we’d ignore if two references were identical objects in memory or not.
We also had Entities in Objective-C. You could get by with memory address chcks, but if you were serious, you had some database anyway and thus would have access to an Entity ID of sorts. Could be an auto-incrementing SQLite table id, for example.
Conversely, you can have struct
s (a value type) represent Value Objects or Entities.
struct PersonName: Equatable {
let firstName: String
let middleName: String?
let lastName: String
}
struct Person: Identifiable {
let id: Int
let name: PersonName
}
With this definition, it’s tempting to slap Equatable
onto Person
as well because you want to test if personA
and personB
are different. The equality operator ==
feels like a sensible choice, so you’d start with this:
extension Person: Equatable {
static func == (lhs: Person, rhs: Person) -> Bool {
lhs.id == rhs.id
}
}
This correctly omits the name
from the equality test.
It (probably incorrectly) also makes two instances of Person
equatable – for the convenience of comparing two objects – while it’d have sufficed to use this in call sites:
personA.id == personB.id
This does the same thing but does drive home that you’re dealing with Entities that define same-ness via their ID and ignore the attributes.
Conforming to Equatable
arguable improves the ergonomics when you type comparisons:
personA.id == personB.id
// vs
personA == personB
But the second approach also leaves to guesswork what kind of domain concept you’re trying to express. And then you cannot check if the attributes of the Person
instances are also equal.
That’s the delineation of “equal” vs “identical”. Equal in terms of data, identical in terms of it being an Entity.
Let’s get back to the false conflation of “Value Object” and “value type”: once you find yourself using Equatable
(or Hashable
) in a struct
(a value type) and then also limit it to a small subset of properties to express when two instances should be considered identical, you maybe (ab-)using equatability to express identity. Try to use Identifiable
instead.
If you care about modeling a rich domain, consider these rules of thumb:
- Express identity/same-ness of an Entity via the
Identifiable
protocol; - Express same-ness of Value Objects via their data using the
Equatable
protocol; - Instead of (ab-)using
Hashable
to getSet
semantics for cheap uniqueness on a subset of properties, use a hashable identifier instead. ReplaceSet<Foo>
withDictionary<Foo.ID, Foo>
.
By the way, when Swift value objects came along and it became so simple to implement Equatable
by selecting just the identifier, I fell for this exact trap. There’s still code in my apps that abuses the ==
operator when I should’ve compared their identifiers. So thanks to Helge for bringing clarity by talking about the more recent addition of Identifiable
to the Swift language a bit more!