When it comes to “entity”-like objects there are t...
# codingconventions
m
When it comes to “entity”-like objects there are typically two kinds of implementation for `equals`: • both objects refer to the same entity (
a.id == b.id
) • both entities are completely equal (
a.id == b.id && a.prop1 == b.prop1 && …
) I can’t decide whether I should implement
equals
as comparing merely IDs or comparing entire entities. 1️⃣ Equality by entity IDs • If compare IDs I cover common cases like
if (sender != recipient) sendNotification(…)
and it reads very natural. • It’s also more performant because in most cases you don’t need to compare entire entities. • For
Set<Entity>
typically also the ID is compared, meaning you want to have one entry per user and not one entry per “state” per user. • This can be confusing however if the entity is using
data class
. Most developer would expect by-property equality for data classes. 2️⃣ Equality by entity properties • If I compare entire entities I can do neat things like checking if datasets have changed. If I have a
val old: Collection<User>
and
val new: Collection<User>
and want to check if there have been any updates between the two I can simply use
old != new
. That’s useful for example for Flows and
distinctUntilChanged()
. • Developers may however run into unexpected behavior if for some reason
sender
and
recipient
refer to the same user (by ID) but one of the two instances has slightly different properties (e.g. due to a prior update). Because
sender != recipient
reads so natural, that can be very unexpected. There is a third option where you use either 1️⃣ or 2️⃣ and provide the other one as an additional entity function. That gets tricky however when you’re in a generic context and may not know enough about the type to call such a function, e.g. in a
Flow<Collection<T>>.distinctUntilChanged { old, new -> … }
. I’m curious, how do you usually do that in your codebases?
1️⃣ 1
2️⃣ 5
t
Developers may however run into unexpected behavior if for some reason 
sender
 and 
recipient
 refer to the same user (by ID) but one of the two instances has slightly different properties (e.g. due to a prior update).
My 2 cents (I voted for 2️⃣): I think your point about one of the items having been updated "due to a prior update" illustrates why I actually find
sender != recipient
to be expected. As they are representing the objects at points in time; so when I reason about it, it makes sense that they are not the same (despite having the same ID). More importantly, it more closely matches standard behavior of `data class`es so is more familiar/expected in that regard. Ultimately, go with whatever behavior most closely matches the standard language features. Things that diverge from that behavior deserve their own equality checking functions. If you can avoid overriding
equals
altogether (and let `data class`es do the heavy lifting for you) then that is preferred. If you mess up the equals implementation then you can get subtle bugs within collections. Just another avenue of introducing risk.
m
When you need to compare id, you can always compare by id (
sender.id == recipent.id
). Having full object comparison should be there as it is very useful in many cases, especially in tests (
assertEquals
) or checks if object update should be propagated.