dynamic-object
dynamic-object copied to clipboard
Equality override
I'm guessing this goes against some clojure principal but this change would allow object to override equals and hashcode by implementing the com.github.rschmitt.dynamicobject.Equality interface and adding default implementations of isEqualTo and getHashCode(). The com.github.rschmitt.dynamicobject.EqualityTest gives and example of when this sort of override would be useful.
This is tricky and tedious in Java for a number of reasons. Equality basically works like this in Java:
-
equals
andhashCode
are defined onjava.lang.Object
. Classes that do not support meaningful equality logic cannot somehow un-inherit those methods. - Equality has never been retroactively extracted into an interface (or genericized, for that matter), so classes that do implement meaningful equality logic have no way to advertise that fact other than documentation.
- Classes that depend on
equals
andhashCode
(such asjava.util.HashMap<T>
) have to simply call those methods directly on each instance ofT
. Equality logic on a given type cannot vary according to the specific context.
Contrast this with how inequality works in Java:
-
compareTo
is declared on theComparator<T>
interface. Types that have no natural ordering can simply abstain from implementingComparator<T>
. - Since
Comparator<T>
does not necessarily have to be implemented byT
, more than one type of comparison logic can be implemented for a given type--for example, seeString.CASE_INSENSITIVE_ORDER
from the standard library, which provides an alternative to the natural (case-sensitive) ordering of strings. - Because library authors are aware of (2), they allow end users to supply the
Comparator<T>
implementation of their choice. For example,new TreeSet<>(String.CASE_INSENSITIVE_ORDER)
yields a case-insensitive set of strings.
When writing POJOs or DTOs by hand, people are often tempted to implement equals
logic in a way that is either domain-specific (ignoring certain fields, or only looking at a particular field) or purpose-specific (equality logic that isn't really generically correct equality logic, but which suits some specific piece of business logic). One of the simplifications of DynamicObject is its opinionated approach to equality: equality is always defined as equality on the underlying maps, unknown fields and all, because this is the most conservative approach to take when you consider the fact that there can only be one implementation of those methods. In other words, you can add all the custom equality methods you want to your Person
type and call them wherever appropriate, but the equals
and hashCode
methods inherited from java.lang.Object
have an enormous amount of surface area (they get called all over the place) and should hew as closely to the standard Java semantics as possible.
Do you have a specific use case that motivated this PR?
I would like to use your library to cut out a large amount of boilerplate code on my domain model objects. Those objects have domain specific equality rules. I'm looking for a hook into the DynamicObjectInstance equals
and hashcode
methods .
I totally get what you're saying, equals
should mean "these two object have exactly the same data" and not "these two things are 'equal' according to some rules of the domain". Developers implement equality in a domain specific way because it's convenient to say person.equals(otherPerson)
and know that the domain equality rules are being applied. Also, as you mentioned, the use of 'java.lang.Object.equals' is so ubiquitous throughout java it's nice to know that "Ok, my domain specific equality rules are going to get applied everywhere if I override equals
and hashcode
". I'm thinking of things like listOfPeople.contains(thePersonImLookingFor)
. Whether this is right or wrong can be debated but if I had to guess I imagine many developers override equals
and hashcode
for domain objects because they're trying to model real world entities and interactions.
This your library and if you want to make it opinionated that's great. I would appreciate hooks into the equals
and hashcode
methods given how ubiquitous their use is across many java libraries. Perhaps the Equality
interface in the PR should be generic so that it more explicitly states what type it is equal to i.e. public interface Person extends DynamicObject<Person>,Equality<Person>
. I'm not looking for your library to be less opinionated but it would be great if it were more flexible in this one respect.
I'm open to other suggestions.
There are many libraries in modern Java development that do boilerplate elimination. My personal favorite is Immutables, and I've also used Lombok extensively and AutoValue a bit. These libraries are implemented in such a way that it is much more straightforward to override equals
and hashCode
(i.e. by actually overriding those methods, instead of having to go through a trap door). They have some other advantages too (a simpler performance model, a build-time metaprogramming strategy, a modular implementation in the case of Immutables), so you should only look at DynamicObject if you need either Clojure integration (either at runtime or in your data model), really strong serialization support out-of-the-box (Immutables will at least give you basic JSON serialization), or some other characteristic feature like metadata. Would one of these alternatives be a better fit?
Lombok is nice but not really what I was looking for. Elimination of boilerplate is only half the reason I was looking at dynamic-object. I've been building my domain model as map backed objects by hand for a while now. Maps are easier to serialize to and from json and/or bson but it's a pain to have to code the getters and builders. I liked how you're generating the map backed implementation at runtime and the serialization is event easier to work with than using something like Gson. The schema validation and metadata features are nice as well.
It's fine to tell me I'm out of luck with these equality rules. I'll figure something else out.
Customizing equals
and hashCode
might not be out of the question. There's already an experimental hook, afterDeserialization
, defined on the DynamicObject
interface that permits the insertion of validation or schema upgrade logic directly into the deserialization routine. It could similarly be possible to declare isEqualTo
and getHashCode
directly on DynamicObject
, and then use dispatch logic similar to afterDeserialization
to route calls to the correct implementation.
I'm not sure if this was what you had in mind but I removed the Equality
interface and instanceof
checks in the DynamiceObjectInstance.equals
and DynamiceObjectInstance.hashCode
functions. Instead I added a mapping for equals
and hashCode
in InvokeDynamicInvocationHandler
that delegates to isEqualTo
and getHashCode
methods (if they exist).