Deprecated Behaviour

The inane, sometimes insane, ramblings from the mind of Brenton Alker.

Entity Objects in JavaScript

In my last post I looked at Value Objects and how I might implement them in JavaScript. This time I’m going to look at Entities. Entities are kind of the next rung on the ladder in the domain model. They can be composed of Value Objects.

Entities are not defined by their attributes in the same way as Value Objects, instead they have an enduring “Identity”. The canonical example is a person. A person isn’t defined by their name, their age, their height, or any other attribute. Any of these can change and they are still the same person (barring any deep philisophical discussion). In software systems, this is usually implemented by assigning an arbitrary key that uniquely identifies the entity for its lifetime.

Where this key comes from is irrelevant to this discussion, but it is common for it to be a value that is generated in the persistence layer when the data is saved.

So, from this we could say that an entity is an instance with an identity and a collection of mutable attributes. Using a similar validation style to the one I used for Value Objects, I might write this in JavaScript like:

1
2
3
4
5
6
7
8
9
10
11
12
function Trail(id, fields) {
  if (!id instanceof TrailId) {
      throw new TypeError('Expected: TrailId');
  }
  if (!fields instanceof TrailFields) {
      throw new TypeError('Expected: TrailFields');
  }
  
  this.id = id;
  this.fields = fields;
  return Object.freeze(this);
}

But, I just said that entities are mutable, so, why am I still calling Object.freeze? Because, I want this object – the Entity – to be immutable, its id can’t change but it’s fields are still mutable; I’m taking advantage of the shallow freeze semantics.

Side Note: If you’ve been paying close attention, you may have noticed I’m throwing exceptions here instead of returning them like last time. This is because I consider passing the wrong type to be a developer error, which is (hopefully) an exceptional situation, as opposed to a user entering an invalid value, which is to be expected.

Ignoring the TrailId, which will be a Value Object similar to the previous post. The only interesting part left is the TrailFields object. This will be slightly different to the Value Object and the Entity itself because it’s mutable. So, its validation must be performed in the setters instead of the constructor. Using the common setter/getter pattern, it looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function TrailFields(name, distance, grade) {
  this.name(name);
  this.distance(distance);
  this.grade(grade);
}

TrailFields.prototype.name = function(name) {
  if (name === undefined) {
    return this._name;
  }
  if (!name instanceof TrailName) {
    throw new TypeError('Expected: TrailName');
  }
  this._name = name;
}

TrailFields.prototype.distance = function(distance) {
  if (distance === undefined) {
    return this._distance;
  }
  if (!distance instanceof Distance) {
    throw new TypeError('Expected: Distance');
  }
  this._distance = distance;
}

TrailFields.prototype.grade = function(grade) {
  if (grade === undefined) {
    return this._grade;
  }
  if (!grade instanceof TrailGrade) {
    throw new TypeError('Expected: TrailGrade);
  }
  this._grade = grade;
}

Even though we now have setters, all of the values are still required in the constructor. This makes it impossible* to have a partially instantiated instance running around.

* “Impossible” is an optimistic take on it. If you want to assign invalid values directly to the “internal” fields, this won’t protect you from yourself. This is JavaScript after all.

Another way of modeling the TrailEntity is to have the fields as properties of the entity but leave the id as null until it is persisted, then updating it. The advantage of explicitly defining TrailFields is, once again, less mutation and preventing the creation of incomplete instances.

Before the data is persisted we have only TrailFields. Once it is persisted then we have a TrailEntity. The different types represent the different stages in the lifecycle and the two states are less easily confused.