# The measured pair: one machine, three requirement changes, six numbers

Two designs of the same trip machine. `a/` puts the rules in the control flow of `Trip.handle`, as a
`switch (state)` with a pattern switch over the event inside each arm. `b/` puts the rules in a list
of `Edge` values and looks them up. Both compile with `-Xlint:all` clean, and both pass all seven
tests in `BaseTest.java`, before and after every change here.

`a/` is not a straw man. It is 4 files against `b/`'s 7, it refuses illegal events with the right
exception, and a duplicate `case` label in one of its arms is a compile error. Under a
forty-five-minute clock it is what a competent engineer writes.

Then three requirements arrive. The first is cheap for both. The second travels along `b/`'s axis.
The third does not.

## Change one, in the interviewer's words

From `corpus/trip-state-machine/curveballs/02-start-without-the-tap/REQUIREMENT-CHANGE.md`:

> Drivers forget to tap "I've arrived". It happens constantly — they pull up, the rider is already
> standing there, they both get on with it, and nobody touches the phone until the trip is over. So:
> a trip may be started directly from `MATCHED`.

`KerbSkipTest.java` asserts it, and passes against `a-kerbskip/` and `b-kerbskip/`.

## Change two, in the interviewer's words

> We are live in two more cities and each regulator wants something different. In London a rider
> cannot cancel once the driver is at the kerb; they call support instead. In São Paulo drivers never
> tap arrive, so a trip has to be startable from `MATCHED` there. One codebase, and a trip is built
> knowing its market.

`MarketsTest.java` asserts it, and passes against `a-markets/` and `b-markets/`.

## Change three, in the interviewer's words

From `corpus/trip-state-machine/curveballs/03-no-show-timeout/REQUIREMENT-CHANGE.md`:

> A new event, `WaitExpired`, raised by a timer when the driver's wait runs out. It takes the trip
> from `DRIVER_ARRIVED` to `CANCELLED`. The new part: it is only legal once the driver has actually
> been waiting for at least five minutes. Measure from the timestamp of the transition that put the
> trip into `DRIVER_ARRIVED`, to now, and take now from the injected clock.

`NoShowTest.java` asserts it, and passes against `a-noshow/` and `b-noshow/`.

## The numbers

Run it yourself:

```
node lessons/B6/contrast/measure.mjs
```

Real output, from exactly these directories:

```
one pair becomes legal  (cheap for both)   a           -> a-kerbskip   diffLines   1  touched 1  new 0  [Trip.java +1/-0]
one pair becomes legal  (cheap for both)   b           -> b-kerbskip   diffLines   3  touched 1  new 0  [TripRules.java +2/-1]
rules vary by market    (on B's axis)      a           -> a-markets    diffLines  10  touched 1  new 1  [Trip.java +9/-1]
rules vary by market    (on B's axis)      b           -> b-markets    diffLines   4  touched 1  new 2  [Trip.java +4/-0]
no-show timeout         (off B's axis)     a           -> a-noshow     diffLines  10  touched 2  new 0  [Trip.java +8/-0, TripEvent.java +2/-0]
no-show timeout         (off B's axis)     b           -> b-noshow     diffLines  15  touched 3  new 2  [Trip.java +7/-3, TripEvent.java +2/-0, TripRules.java +2/-1]

size of a    4 file(s)  100 normalised lines
size of b    7 file(s)  140 normalised lines
```

| Change | A, rules in control flow | B, rules as data |
|---|---|---|
| one `(state, event)` pair becomes legal | **1** line | **3** lines |
| the rules themselves vary by market | **10** lines, and every one inside `handle` | **4** lines, all in a constructor |
| a rule that depends on a third input | **10** lines | **15** lines, plus two new files |

`measureChange` is the same function that scores D4 in a graded attempt, so these are the numbers the
grader would produce. New files cost nothing. Edits to files that already existed are charged,
additions included.

## What each number means

**B's 3 is the corpus's own 3.** `corpus/trip-state-machine/curveballs/02-start-without-the-tap`
records `reference_diff: 3` for this requirement against the real reference solution, and its note
says *"one of them is a comma"*. This contrast reproduces that number independently, on smaller
code. One appended `Edge`, plus the comma the row above it now needs, charged as a removal and a
re-addition because Java forbids a trailing comma in an argument list.

**A's 1 beats it, and pretending otherwise would be the easiest way to lose an interviewer.** One
`case` label in one arm. If your argument for a table is "fewer lines when a rule changes", change
one is where somebody who has written both will stop believing you. The table's 3 is two lines of
punctuation, and the argument has to be about something else.

**Change two is what the something else is.** A's rules are code, so "which rules" cannot be a
parameter. Making it one means a `Market` field, a second constructor, and a conditional inside two
arms of the switch. That is 9 added lines and 1 rewritten, all inside the method that decides
legality. B's rules are a value, so a second rule set is a new file, and the only edit is a
constructor that names it. `MarketRules.java` is free because it is new, and it is new because rules
that are data can live somewhere that did not exist yesterday. A has no equivalent move available at
any price.

**Change three is the one this lesson exists for, and B loses it by 5 lines.** The five-minute rule
is a function of the state, the event, and how long the trip has sat at the kerb. `Edge` is
`(TripState, event kind) -> TripState`, so the table cannot express it however it is indexed. B pays
for a guard collaborator: a field, a widened constructor, a rewritten happy path, one row for the
pair, and the new event. A puts an `if` inside the arm that already handles `DRIVER_ARRIVED`, and it
is cheaper.

The corpus records the full-scale version of that same defeat.
`corpus/trip-state-machine/curveballs/03-no-show-timeout/budget.json` measures **28** for this
requirement against a reference whose other two curveballs cost **8** and **3**. Nineteen of the 28
are in one class, `Trip`. Nine of those nineteen are javadoc that had to be corrected. The class had
claimed that a refusal happens *"before the clock has been read"*, and after this change that is
false.

## The alternatives, so the choice is a choice

**Could `b/` have absorbed the five-minute rule as a row?** No, at any price. That is the whole
point, and `03-no-show-timeout/reference-patch/PATCH.md` states the reason in one line: *"The table
is a function of two values, and this rule is a function of three."*

**Could the rule have gone on `Edge` as a fourth component?** `record Edge(from, on, to, guard)` puts
the rule where the rules are, and it costs more rather than less. Every existing row grows a no-op
guard, which is a batch of edits that all say "nothing changed". Worse, `TransitionTable.next` would
have to be handed the clock and the trip's history, which turns a pure lookup into something that
reads the world. The table's purity is what lets a refusal happen before the append, and that is what
means there is no rollback path anywhere.

**Could A's `if` have been the right answer all along?** It measures fewer lines here, and it is the
answer the corpus rejects on grounds that are not about line count. `Trip` holds the sequence, not
one rule a requirement could reach. Now imagine the second time-dependent rule, say a scheduled trip
that may not be matched before its pickup window. In A it is a second `if` in the same method, so the
method that decides legality is also the method that timestamps and records. `b-noshow/`'s
`TripGuard` pays the plumbing once, and that second rule is then a new implementation and no edit.

**Is the smallest diff the right answer?** The corpus has the counter-example.
`corpus/middleware-router/curveballs/02-method-aware-routing/reference-patch/PATCH.md` records an
earlier draft that *"measured smaller"* and was rejected for coupling `Router` to one concrete
`MatchPolicy`. That absorption cost 47 lines, the most expensive in the corpus, and the author took
the larger number knowingly. Use the diff to check a claim. Do not use it to choose a design.

## What this pair does not show

**The compiler helping.** Neither design here notices the new event. A's arms have `default`
branches, and B's `Edge` keys on a `Class`, which is not exhaustiveness-checked. The real reference
has three exhaustive switches over `TripEvent`: a projection, a notification and a narrator. Adding
`WaitExpired` to the sealed interface breaks all three by name before a test runs, as verified in
`01-driver-cancels/reference-patch/PATCH.md`. `lessons/B6/worked/` keeps one of those switches, in
`Main.narrate`, and `NOTES.md` quotes the `javac` message.

**The four-entry-point machine, where A is much worse.** `corpus/vending-machine` has `insertCoin`,
`select`, `refund` and `collect`. Its `TransitionTable` javadoc names the cost of the switch version
directly. The rules end up spread across four methods, and *"each method grows a guard clause that
has to be kept consistent with three others by hand"*. Its maintenance-mode curveball adds a
whole state that refuses every customer operation, and measures 8. Six of those are the price of
admission: 4 lines to declare two new contract methods, and 2 to add two words to the event enum. This trip machine has one entry point, which is the shape most
favourable to A. The comparison above is the table's hardest case, on purpose.

**How the rules read.** `b/TripRules.java` answers "what can a trip do at the kerb?" by grep. In
`a/Trip.java` the same question means reading nested control flow and trusting that no arm disagrees
with another. That is worth something in a design round, and it is not a number.
