---
title: 'Object Responsibility'
source: 'https://academia.sh/en/courses/paradigms/object-responsibility'
course: 'Programming Paradigms'
language: en
updated: '2026-08-23T07:01:20+00:00'
license: 'CC BY-SA 4.0'
---

# Object Responsibility

Comparing an anemic data carrier with an arrangement that keeps behavior with the data: counting how many files the same two rules are written in, a search-based update missing one location when the rule changes, and measuring the inconsistency that shows up in 20 out of 40 shipments.

The previous lesson measured how two objects connect to each other. This lesson's question
is what a single object carries within itself. The consignment class held both the shipments
and the capacity rule; the rule lived in the same place as the data. Another common
arrangement separates them: data carries only fields, and rules are spread across separate
processing units.

Objects that carry fields but contain no rules are called **anemic**. This lesson compares
the two arrangements on the same rules: it counts how many places a rule is written in, and
measures how many results diverge from each other when the rule changes.

## Anemic Arrangement

A shipment carries three fields and knows nothing else.

```js
// anemic/shipment.mjs — data object that carries fields only; no rule lives here
export const shipment = (weightGrams, volumeCm3, contracted = false) =>
  ({ weightGrams, volumeCm3, contracted });
```

There are two rules in the library. The first is **chargeable weight**: the value used for
the fee is the larger of the actual weight and the weight derived from volume. The second is
the **contracted discount**: for a contracted customer, the amount is multiplied by a factor
of 0.9. All three clients apply these rules on their own.

```js
// anemic/billing.mjs — applies the chargeable weight and discount rules itself
export const fee = (s) => {
  const weight = Math.max(s.weightGrams, s.volumeCm3 / 3);
  const rate = s.contracted ? 0.9 : 1;
  return { weight, amount: Math.round(weight * 1.2 * rate) };
};
```

```js
// anemic/quote.mjs — rewrites the same two rules
export const estimate = (s) => {
  const weight = Math.max(s.weightGrams, s.volumeCm3 / 3);
  const rate = s.contracted ? 0.9 : 1;
  return { weight, amount: Math.round(weight * 1.5 * rate) };
};
```

The third client writes the same rule, but has pulled the divisor into a local constant. On
its own, this is an ordinary stylistic choice that is not a defect; it becomes decisive later
in the lesson.

```js
// anemic/plan.mjs — same rule, with the divisor pulled into a local constant
const VOLUME_DIVISOR = 3;

export const load = (s) => {
  const weight = Math.max(s.weightGrams, s.volumeCm3 / VOLUME_DIVISOR);
  return { weight, loadShare: Math.round((weight / 20000) * 100) };
};
```

## Arrangement That Keeps Behavior With the Data

In the second arrangement, the two rules are carried by the shipment itself. The fields are
private; the rule results are exposed to the outside.

```js
// responsible/shipment.mjs — data and the rule that interprets it live in the same unit
export class Shipment {
  #weightGrams; #volumeCm3; #contracted;

  constructor(weightGrams, volumeCm3, contracted = false) {
    this.#weightGrams = weightGrams;
    this.#volumeCm3 = volumeCm3;
    this.#contracted = contracted;
  }

  chargeableWeight() { return Math.max(this.#weightGrams, this.#volumeCm3 / 3); }

  discountRate() { return this.#contracted ? 0.9 : 1; }
}
```

The same three clients no longer compute; they ask.

```js
// responsible/billing.mjs — does not ask for the rule, asks the shipment
export const fee = (s) => {
  const weight = s.chargeableWeight();
  return { weight, amount: Math.round(weight * 1.2 * s.discountRate()) };
};
```

```js
// responsible/quote.mjs — asks the same two questions in the same place
export const estimate = (s) => {
  const weight = s.chargeableWeight();
  return { weight, amount: Math.round(weight * 1.5 * s.discountRate()) };
};
```

```js
// responsible/plan.mjs — asks only for the weight
export const load = (s) => {
  const weight = s.chargeableWeight();
  return { weight, loadShare: Math.round((weight / 20000) * 100) };
};
```

## Where the Rule Is Written

The measurement searches for the two rules and counts how many files they appear in; the
same scan also counts how many times the clients access the shipment's fields directly.

```js
// measurement.mjs — counts how many files repeat the two rules and how many times fields are accessed directly
import { readdirSync, readFileSync } from "node:fs";

const RULE = [
  ["chargeable weight", /Math\.max\([^)]*volumeCm3/],
  ["contracted discount", /contracted \? 0\.9/],
];
const FIELD = /\bs\.(weightGrams|volumeCm3|contracted)\b/g;

for (const dir of ["anemic", "responsible"]) {
  const files = readdirSync(dir).sort().map((f) => [f, readFileSync(`${dir}/${f}`, "utf8")]);
  console.log(dir);
  for (const [name, pattern] of RULE) {
    const locations = files.filter(([, k]) => pattern.test(k)).map(([f]) => f);
    console.log(`  ${name.padEnd(20)} files written in = ${locations.length}  ${locations.join(", ")}`);
  }
  const access = files.reduce((t, [, k]) => t + (k.match(FIELD) ?? []).length, 0);
  console.log(`  ${"direct field access".padEnd(20)} = ${access}`);
}
```

```sh
node measurement.mjs
```

```
anemic
  chargeable weight    files written in = 3  billing.mjs, plan.mjs, quote.mjs
  contracted discount  files written in = 2  billing.mjs, quote.mjs
  direct field access  = 8
responsible
  chargeable weight    files written in = 1  shipment.mjs
  contracted discount  files written in = 1  shipment.mjs
  direct field access  = 0
```

The chargeable weight rule is written in three files in the anemic arrangement, and in one
file in the arrangement that keeps behavior with the data. The contracted discount is two
against one. The last line shows why: in the anemic arrangement, the clients access the
shipment's fields directly eight times; in the other arrangement, not once. Whoever reads a
field also has to know how that field is to be interpreted; the multiplication of the rule
is the result of that obligation to know.

## When the Rule Changes

Both arrangements produce the same result. The following checker runs the three clients over
a grid of 40 shipments and counts whether the chargeable weight the three of them find is the
same.

```js
// consistency.mjs — counts whether the three clients find the same chargeable weight for the same shipment
import { shipment } from "./anemic/shipment.mjs";
import { fee as aFee } from "./anemic/billing.mjs";
import { estimate as aEstimate } from "./anemic/quote.mjs";
import { load as aLoad } from "./anemic/plan.mjs";
import { Shipment } from "./responsible/shipment.mjs";
import { fee as sFee } from "./responsible/billing.mjs";
import { estimate as sEstimate } from "./responsible/quote.mjs";
import { load as sLoad } from "./responsible/plan.mjs";

const grid = [];
for (const weight of [400, 900, 2500, 6000, 11000])
  for (const volume of [600, 4500, 18000, 45000])
    for (const contracted of [false, true])
      grid.push([weight, volume, contracted]);

const diverged = (operations, build) => grid.filter((row) => {
  const weights = operations.map((f) => f(build(row)).weight);
  return new Set(weights).size > 1;
}).length;

console.log(`shipment count       = ${grid.length}`);
console.log(`anemic      diverged = ${diverged([aFee, aEstimate, aLoad], (s) => shipment(...s))}`);
console.log(`responsible diverged = ${diverged([sFee, sEstimate, sLoad], (s) => new Shipment(...s))}`);
```

```sh
node consistency.mjs
```

```
shipment count       = 40
anemic      diverged = 0
responsible diverged = 0
```

Now the rule changes: the divisor that converts volume to weight drops from 3 to 4. The
party making the change searches for the rule and updates every place it finds. The
following block does this; `sed -i` is written through a temporary file because its option
behaves differently between the GNU and BSD versions.

```sh
for f in $(grep -rl 'volumeCm3 / 3' anemic responsible | sort); do
  sed 's|volumeCm3 / 3|volumeCm3 / 4|' "$f" > "$f.new" && mv "$f.new" "$f"
  echo "updated: $f"
done
node consistency.mjs
```

```
updated: anemic/billing.mjs
updated: anemic/quote.mjs
updated: responsible/shipment.mjs
shipment count       = 40
anemic      diverged = 20
responsible diverged = 0
```

The search found two of the three places in the anemic arrangement. The third —
`plan.mjs`, which had pulled the divisor into a local constant — did not get caught by the
search and stayed on the old rule. The result is that in 20 out of 40 shipments, billing
used one weight and the loading plan used another. The same shipment started being referred
to by two different weights, and no error appeared anywhere.

In the arrangement that keeps behavior with the data, the same search found a single place,
because the rule lived in a single place. The number of diverging results stayed at zero.

The real observation here is not that the search missed a place; every search misses a place
sooner or later. The real observation is that in the anemic arrangement, **missing one is
possible**. If the rule lives in one place, there is no second place to miss.

## Tell, Don't Ask

The eight direct accesses in the measurement were the cause of the rule's multiplication.
The principle that reverses this is called **tell, don't ask**: instead of taking an
object's data and deciding on it, having the object make the decision itself. The anemic
client took the `s.volumeCm3` field and divided it; the client working with the responsible
arrangement asked for the result by calling `s.chargeableWeight()`.

The limit of the principle also shows up in the measurement. What can be asked of a shipment
is what the shipment can answer with its own data. Chargeable weight and the discount rate
are like this. Computing an amount according to a tariff is not: that requires a second
object the shipment does not know about. Making the shipment carry the tariff too would not
gather responsibility but inflate it — every rule in the library would pile up in a single
class. The criterion is fixed: a rule is placed next to its data only if it can be answered
with that data alone.

## Where a Field Carrier Belongs

The anemic arrangement is not a flaw everywhere. Records that cross a boundary — a body
arriving from the network, fields read from a database row, an output written to a file — do
not carry behavior and should not; their job is to preserve form, not interpret rules. The
flaw is these records taking the **place** of field objects that have rules: if a record
coming from the boundary spreads through the system as it is, every point that interprets it
has to rewrite the rule, and the measured three-against-one table appears.

In practice, the distinction comes down to a single question: are the fields this object
carries interpreted by a rule? If they are, the rule should stand together with them. If
they are not, a field carrier is the right choice.

## Summary

- An anemic object carries only fields and leaves the rules to separate processors; keeping
  the rule together with the data is the opposite of this.
- The same two rules were written in three and two files in the anemic arrangement; in the
  arrangement that keeps behavior with the data, both were in a single file.
- The source of the rule's multiplication was measured: the anemic clients accessed the
  shipment's fields directly eight times, zero in the other arrangement.
- When the divisor was lowered from 3 to 4, the search found two of the three places in the
  anemic arrangement; the differently written third one stayed on the old rule, and in 20
  out of 40 shipments, billing and the loading plan used different weights, with no error
  appearing.
- The same change touched a single file in the arrangement that keeps behavior with the
  data, and produced 0 divergences.
- A rule is carried by an object only if it can be answered with the object's own data; it
  is correct for records that cross a boundary to remain field carriers.

## Next Step

Throughout this topic, the unit was the **object**: state and the behavior that guarded it
were held together. Encapsulation closed state off from outside writes, abstraction hid the
representation, inheritance and composition determined how behavior was shared, and this
lesson placed the rule next to the data. The shared assumption behind all of them is that
state has an owner, and that owner changes the state over time.

Another model removes state from the unit entirely. Data is immutable; behavior is a mapping
from input to output; side effects are pushed to the boundary of the program. The next topic
builds this model and starts with pure functions: it measures what a function that always
returns the same output for the same input, and leaves no trace outside itself, gains, and
what it makes more costly.
