Implementing an equals method in Java can be quite complicated. Fortunately there are numerous document around the web with useful tips, hints and frameworks to assist you in this process. However, an implementation of the equals method that is technically correct doesn’t have to make any sense functionally. In Domain Driven Design, your domain model implementation is the beating heart of your application. Everything has to make perfect (functional) sense in there. Having good equals methods is of vital importance there.
In this article, I will elaborate on some common pitfalls you can encounter when implementing the equals method, as well as some sensible guidelines.
A lot of IDE’s nowadays allow you to generate technically perfect and compliant implementations of the equals method for any object. You simply choose a number of properties you wish to include in the comparison, indicate some of them as being non-null values and voila. Write a small unit test for the thing, commit the whole shebang and you’re done. Well, not quite. You’ve probably got an implementation even worse than the one provided by Object.
I’ve seen developers generate equals methods in mere seconds. Although managers tend to love this sort of “productivity”, as an architect doing code reviews, I measure productivity differently. I doubt if any developer can properly evaluate the functional value of such an implementation in just seconds.
First, we need to define what equals really means. To me, when to objects are equals, it means they are to such a degree identical to each other, that they can be replaced without side effects. In other words, if two objects are equal, it doesn’t matter which one you pick. In math, equality is very well defined. In software, it is a little harder to achieve that level of definitions. But that doesn’t mean we shouldn’t try.
As the title of this article suggests, I want to look at the equals method in the context of Domain Driven Design (DDD). In DDD, it’s all about making concepts explicit. In our case, it means answering the question: “What does it mean, for two [fill in the blank] to be equal?” Of course, there isn’t really a single generic answer for all objects. However, objects can be classified into a few major groups in DDD: Entities, Value Objects and (Domain)Services. The last of the three is not really of much interest in this context, so let’s focus on the other two.
Value objects are immutable objects of which only the properties are of importance. They carry no concept of identity. A perfect example of a value object in daily life is money. Personally, I don’t care which 10 euro bill I carry, as long as it is a valid 10 euro’s. You can swap it with one owned by a friend and you won’t feel better or worse about it. The two bills are equal.
Because of the immutability in value objects, testing them for equality is rather easy. Generally, you can just generate an equals method using all of the (exposed) properties of the object. In our example, as long as the currency and the amount is the same, we don’t really care which instance of the bill we carry with us.
Therefore: the default choice for the equals method on value objects should be to include all (exposed) properties of that value object in the comparison.
En entity is “an object fundamentally defined not by its attributes, but by a thread of continuity and identity.” In daily life, having the same name as somebody else doesn’t make you the same. This form of mistaken identity can lead to huge problems in an application.
But back to our equals implementation. What does it mean for two entities to be equal? Well, it should mean that they can replace each other without side effects. If they can’t replace each other, they can’t really be equal. This means that, for to entities to be equal, at least their identity should be equal.
But entities have mutable state. To what degree do you want to use that state in the comparison? That really depends on the context of the comparison. If you want to know if the state has been modified between two copies of the instance, you will need an equals method that checks on all mutable properties as well as the identity. If you are only interested in knowing whether you are talking about an object representation of the actual same thing, identity comparison is the only thing you need.
If there is one thing an equals method cannot do, it is to look at the context and intentions of the caller. Since it extremely important to use “intention revealing interfaces”, an equals method on an entity is probably not the right way to go. In a discussion with Eric Evans, he explained that he prefers not to implement equals on entities at all, and instead provide other comparison methods, such as “hasSameIdentityAs”. This method clearly states what it means to be the same. Depending on the context and intention of your comparison, you call another method.
Let’s go back to the statement about equality: when two objects are equal, it means that you can replace one with the other without side effects.
Replacing one entity instance with another is dangerous in most circumstances. If they have the same identifier, they might have different state. The different instances are likely to be used by different threads with different intentions. When using persistence frameworks like JPA, your entity is likely to be attached to a persistence context, meaning that replacing them without side effect is out of the question.
This probably means that this statement is a little too rigid for entities. Mechanisms like the Set rely on the equals method to decide whether you are allowed to add an item or not.
Personally, I like to define two entities as equal when you are talking about the representation of the same actual thing. Hence, when the type and identity of the two are the same. This means my choice of equals method will only take the actual class and identity into consideration.
This gives us a nice problem when identity is provided by the persistence framework at the time an object is persisted. How do you measure equality when one or both instances do not (yet) have an identity? Although you cannot (or should not) try to predict the identity of those instances, there is one thing you can say about it. If two different instances have no identity, there is no way a persistence framework will assign them the same identity. This means that you can revert to the default implementation of equals in that case (practically doing a == comparison). The same goes for comparison of an entity with identity and one without: they will never, ever have the same identity in the future.
Before implementing the equals method, think clearly about the type of object that you are comparing. If it is an immutable value object, you should include all (exposed) properties of the value object. If it is an entity, be very cautious and first define what equality really means. Consider using methods with intention revealing interfaces, such as “hasSameIdentityAs” and “hasSameStateAs”.