Lesson 03 / 19
Entities
The selection criterion for objects defined by identity: counting how many separate instances twelve shipments reduce to under value equality, measuring how many invoice reads come out correct, working out how many keys point to the same identity after a reweighing correction, and comparing what the two equality definitions answer to the domain's two questions.
Contents
In the previous lesson’s unified version, a shipment was a flat record carrying three fields, and two shipments could only be considered the same if their fields were equal. That is not true in the domain. The same shipment’s weight changes with a reweighing correction, and the shipment is still the same shipment; two separate shipments can have every field equal and still be two shipments.
The object that carries this distinction is called an entity: an object that stays the same even as its fields change, and whose equality is therefore defined not by its fields but by an identity. The definition was given in the Introduction to Object-Oriented Programming course; the work here is not the definition but the selection criterion — which domain concept gets an identity, and what is lost when it does not.
Two Questions
The criterion reduces to two questions. First: is this thing still the same thing after its fields change? Second: do two instances whose fields are all equal have to be tracked separately?
In the shipping-fee library, the answer to both is yes. A customer sends five parcels of the same weight to the same zone on the same day; each of the five gets its own invoice. When the same parcel is weighed, its declared weight is corrected; the invoice keeps being issued to that parcel.
Two Models
The first version defines a shipment by its fields. Equality is a key produced by combining the fields.
mkdir -p value identity
// value/shipment.mjs — shipment without identity: equality is equality of fields export const shipment = (customer, weight, zone) => ({ customer, weight, zone }); export const key = (s) => `${s.customer}|${s.weight}|${s.zone}`; export const equals = (a, b) => key(a) === key(b); export const correctWeight = (s, newWeight) => ({ ...s, weight: newWeight });
The second version gives the shipment a number. Equality looks only at that number; the weight correction changes the object but does not touch its identity.
// identity/shipment.mjs — entity: equality is equality of the shipment number export class Shipment { constructor(shipmentNo, customer, weight, zone) { this.shipmentNo = shipmentNo; this.customer = customer; this.weight = weight; this.zone = zone; } key() { return this.shipmentNo; } sameAs(o) { return o instanceof Shipment && o.shipmentNo === this.shipmentNo; } correctWeight(newWeight) { this.weight = newWeight; return this; } }
Measurement
The measure’s input is twelve shipments and the invoice cut for each one. Five shipments belong to the same customer, the same weight, and the same zone; two belong to a second customer, and two are repeated among a third customer’s heavy parcels.
// records.mjs — 12 shipments in the pricing context and the invoice cut for each export const RECORDS = [ ["G-1041", "ARC", 2.4, "34", "F-9001"], ["G-1042", "ARC", 2.4, "34", "F-9002"], ["G-1043", "ARC", 2.4, "34", "F-9003"], ["G-1044", "ARC", 2.4, "34", "F-9004"], ["G-1045", "ARC", 2.4, "34", "F-9005"], ["G-1046", "ARC", 5, "34", "F-9006"], ["G-1047", "BKM", 0.8, "06", "F-9007"], ["G-1048", "BKM", 0.8, "06", "F-9008"], ["G-1049", "BKM", 12, "65", "F-9009"], ["G-1050", "CTS", 0.8, "06", "F-9010"], ["G-1051", "CTS", 18, "65", "F-9011"], ["G-1052", "CTS", 18, "65", "F-9012"], ];
For each version, the invoice table is built with the shipment’s key in that version. Then the first shipment’s weight is corrected from 2.4 to 3.1 kilograms and the table is read again.
// identity-measure.mjs — how many instances do 12 shipments reduce to, how many invoice reads are correct import { RECORDS } from "./records.mjs"; import * as value from "./value/shipment.mjs"; import { Shipment } from "./identity/shipment.mjs"; function measure(label, build, key, correct) { const objects = RECORDS.map(([no, c, w, z]) => build(no, c, w, z)); const table = new Map(); objects.forEach((o, i) => table.set(key(o), RECORDS[i][4])); const right = objects.filter((o, i) => table.get(key(o)) === RECORDS[i][4]).length; console.log(`${label}: 12 shipments -> ${table.size} separate instances`); console.log(` invoice reads correct ${right}/12`); const old = key(objects[0]); const updated = correct(objects[0], 3.1); table.set(key(updated), table.get(old)); objects[0] = updated; const rightAfter = objects.filter((o, i) => table.get(key(o)) === RECORDS[i][4]).length; const pointer = [...table.keys()].filter((k) => k === old || k === key(updated)).length; console.log(` after the reweighing correction: table entries ${table.size}, ` + `keys pointing to G-1041 ${pointer}`); console.log(` invoice reads correct after the correction ${rightAfter}/12`); } measure("value", (no, c, w, z) => value.shipment(c, w, z), value.key, (s, w) => value.correctWeight(s, w)); measure("identity", (no, c, w, z) => new Shipment(no, c, w, z), (s) => s.key(), (s, w) => s.correctWeight(w)); const [first, second] = [value.shipment("ARC", 2.4, "34"), value.shipment("ARC", 2.4, "34")]; const [s1, s2] = [new Shipment("G-1041", "ARC", 2.4, "34"), new Shipment("G-1042", "ARC", 2.4, "34")]; console.log(`are two separate shipments equal -> value ${value.equals(first, second)}, ` + `identity ${s1.sameAs(s2)}`); const s1b = new Shipment("G-1041", "ARC", 3.1, "34"); console.log(`is the corrected shipment the same -> value ` + `${value.equals(first, value.correctWeight(first, 3.1))}, identity ${s1.sameAs(s1b)}`);
node identity-measure.mjs
value: 12 shipments -> 6 separate instances invoice reads correct 6/12 after the reweighing correction: table entries 7, keys pointing to G-1041 2 invoice reads correct after the correction 6/12 identity: 12 shipments -> 12 separate instances invoice reads correct 12/12 after the reweighing correction: table entries 12, keys pointing to G-1041 1 invoice reads correct after the correction 12/12 are two separate shipments equal -> value true, identity false is the corrected shipment the same -> value false, identity true
The first number gives the size of the loss: under value equality, twelve shipments reduce to six instances. Six shipments collapse onto another shipment’s key, so half of the invoice reads come out wrong — six shipments show the invoice that was not cut for them.
The second number shows the reverse corruption. The weight correction produces a new key in
the value version; the old key cannot be dropped, because other shipments still depend on it.
As a result, the table grows to seven entries, and a single shipment ends up shown under two
keys. In the identity version, the same correction does not change the table’s size: the key
G-1041 stays put, and all twelve reads stay correct.
The last two lines reduce the criterion to a single sentence. The domain’s first question is whether two shipments are the same, and the answer is no; value equality answers yes. The second question is whether the corrected shipment is still the same shipment, and the answer is yes; value equality answers no. Of the two definitions, one answers both of the domain’s questions wrong, and the other answers both right.
Where Identity Comes From
The only requirement identity carries is that it does not change over the object’s lifetime and is not repeated in any other object. If the domain already has a ready-made identity, that one is used: a shipment number, a contract number, a tax ID. If not, one is generated, and the rule for generating it is a decision for the application, not the domain.
The measure breaks when a field from the domain is used as if it were identity. In the pricing context, the combination of customer and zone often produces a single shipment; “often” is not an identity criterion, and the measurement above shows the cost of that assumption. For the same reason, correctable fields such as weight, postal code, and declared value cannot be part of an identity.
Identity’s second consequence is uniqueness: if two instances share the same identity exist in memory at once, which one is correct becomes unclear. This problem was addressed in the Identity Map lesson of the Design Patterns course; the entity decision is that pattern’s precondition, because without an identity to preserve uniqueness, there is no key for the map either.
When Identity Is Not Needed
If the criterion’s two questions both come back no, the object is not an entity. The library has a clear example of this: a zone coefficient. There is no distinction between one 1.35 coefficient and another 1.35 coefficient, and the coefficient has no life story. The same is true of weight, monetary amount, and discount rate.
Giving these concepts identity corrupts the measure in the opposite direction. Once a coefficient is assigned a number, equality comparison looks at that identity, and two values that are equal in the domain no longer come out equal in the code. Identity produces a gain where it belongs; where it does not, it adds a rule to every comparison.
Summary
- An entity is an object that stays the same as its fields change and whose equality is defined by an identity; the selection criterion is two questions — does it stay the same when its fields change, and do two instances with equal fields have to be tracked separately.
- Under value equality, twelve shipments reduced to six instances; 6 of the invoice reads came out wrong.
- The reweighing correction grew the value version’s table to seven entries and left one shipment shown under two keys; the identity version’s table stayed at twelve entries, with all twelve reads correct.
- Value equality answered both of the domain’s questions wrong; identity equality answered both right.
- Identity has to be a name that does not change over the object’s lifetime and is not repeated; correctable fields cannot be part of identity.
- If both questions come back no, identity is not needed; giving identity to concepts such as coefficients, weights, and rates adds an unnecessary rule to every comparison.
Next Step
This lesson’s last section counted the concepts that do not need identity, but still kept
them as bare numbers and strings: weight is a number, zone is a string. That leaves the
code free to add a weight to a volume, compare a kilogram with a gram, and multiply a cent by
a unit of currency. The next lesson turns these concepts into their own types, counts how
many places the equality rule is written by hand, and measures how many cases a unit mix-up
produces a silently wrong amount.
To keep your progress and take notes, Log in
My notes
Log in to take notes.