1

In the context of an object-oriented language, such as Dart, I have an abstract entity which has a single property called id (which is incrementally uniquely generated to make sure there are no-clashes).

Then, classes which extend Entity can add other properties on top of the Uid, which however are not final (i.e they can be changed after an object is created).

In the example below in addition to the base class Entity, I have implemented an example of an Entity called Frame, which has two attributes which can be modified.

abstract base class Entity {
  final Id id;

  Entity(this.id);
}

final class Frame extends Entity {
  Frame(
    Id id,
    this.color,
    this.position,
  ) : super(id);

  Color color;
  Offset position;
}

If I was to implement an hash function, I could hash the color, position and id like this: color.hashCode^position.hashCode^id.hashCode.

However, if I wanted to implement a selection (as a collection of Entities based on an HashSet), if I was to change the value of a parameter of an object which is inside the selection, its hash would change meaning that i would no longer be able to check if it is in the selection, like this:

 final frame = Frame(
  Id(), // Assume that this is a random unique id
  Colors.red,
  Offset.zero,
);

>> frame.hashCode = 1234

final selection = HashSet<Entity>();
selection.add(frame);

// I change the color of the frame
frame.color = Colors.blue;

// The hash of the frame changes
>> frame.hashCode = 5678

// The selection no longer contains the frame, even though it is the same object
selection.contains(frame); // false

To solve this I thought that I could use the id's hashCode as a hashCode for the entire entity (as it is the only field that does change for sure once an entity is created).

However if I implement such an hashCode, I would have to reduce the equals operator to comparing the Ids (to make sure that the collisions in the hashSet are resolved correctly). This would mean that the equality operator would lose part of its meaning.

I tried looking online for similar problems but I could not find anything (I am aware I might not be using the correct terminology, however).

Is this an anti-pattern or is it a known strategy for dealing with this kind of situation?

3
  • Side note: I have no idea about dart, but in all other languages the statement "if I implement such an hashCode, I would have to reduce the equals operator to comparing the Ids" look invented from thin air. Could you please edit the post to clarify why dart imposes such a strange restriction? (usually one can even return 42 as hash code and still can have meaningful comparison operation for hash-based collections) Commented Aug 18, 2023 at 23:06
  • You are correct. In dart hashCode and equals do not have to be equal, what I was referring to is more of a suggestion than a rule (dart.dev/tools/linter-rules/hash_and_equals).
    – Fabrizio
    Commented Aug 19, 2023 at 9:07
  • The rule (language independent) is: Equal implies hashes are equal, but equal hashes does not imply equal. Although with a good hash it is very unlikely they are not equal.
    – gnasher729
    Commented Aug 19, 2023 at 11:06

1 Answer 1

3

There is no anti-pattern here. What you are observing is that when creating domain models, there are different ways how to model the concept of "identity" or "equality".

  • Value objects have an identity defined by their values exclusively. Two value objects are equal when all their corresponding members are equal. They should not have an ID, and they should be immutable, similar to simple types like numbers. Two frame objects, when modeled as a value type, are representing "the same frame" when they have the same position and color. They should contain no ID in this case.

  • Entities usually have an identity defined by their immutable IDs. Two entities with different IDs can have the same values and still represent different things. In your example, this design allows two "overlapping frames" (with same color and position), but with a different ID.

(There is also the third concept of reference identity, but I think this is not what this question is about.)

Now, whenever we model a new class or object, we should make a clear decision if we want a value object or an entity, and implement hash codes and the standard equality operator accordingly. For entities, this means the standard equality operator should only compare the ID, and the ID can also be used directly for hashing.

In cases where we also have a use case where we need to find entities with matching values (apart from their ID), it is best to implement a separate comparison with a custom name, different from the default equality operator, and the same principle applies for the calculation of hash values. I am not a Dart expert, but a short look into the docs showed me the HashSet constructor allows the user to provide custom equality and hashing operators.

Note it is also possible to create an immutable value type FrameSpec (from color and position, and equality and hashing for these two values), and use it to implement an entity Frame (where equality and hashing uses the ID exclusively).

So, if required, we can provide both concepts of equality, we just make sure they won't get mixed up.

Additional reads:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.