Files
learning-hour-maker/ideas/split-loop-with-lambda/storyboard/daily-forecasts-demo.md
T
Your Name 558ad54516 Add demo script for printSalesForecasts (Split Loop with Lambda)
Demo on the messier sibling of the exercise method: three unwanted
dependencies in one loop (Scorer incl. static field write, System.out,
clock — clock stripped in 2-min prep). Same six mechanics with SVGs in
the storyboard style, ends green via overload + pure-lambda approval
test. Pointer added to the learning hour's demo section.
2026-09-28 07:42:33 +00:00

7.2 KiB

Demo script — printSalesForecasts

This is what I demo in the Split Loop with Lambda learning hour. I demo on DailyForecasts.printSalesForecasts() — the messier sibling of the method the pairs will refactor in the exercise. Same pattern, but this loop has three unwanted dependencies: Scorer (with a static field for extra credit), System.out, and the clock.

I remove the clock before we start. The rest stays in. Ten minutes, six steps.

Before we start (2 min)

Strip the two header lines:

// var formatter = DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneId.from(ZoneOffset.UTC));
// System.out.println("Forecast at: " + formatter.format(Instant.now()));

I say this out loud while I delete them: the clock is a third dependency, and we do one split at a time.

Then add the test from step 1 and run it once, so the flakiness is a memory before I start typing.


Step 0 — count the concerns

Mutation and value read, a conditional, IO — in twelve lines. I ask: what does this loop need from the outside? Answer: given a name, what is the sales score? That question is the lambda we are about to extract. The rest — printing, the clock — is not the loop's business.


Step 1 — an approval test: red & flaky

@Test
void salesForecasts() {
    var out = new ByteArrayOutputStream();
    var originalOut = System.out;
    System.setOut(new PrintStream(out));
    try {
        new DailyForecasts().printSalesForecasts();
    } finally {
        System.setOut(originalOut);
    }
    Approvals.verify(out.toString());
}

Run it. Red, and every run disagrees with the last. Why: Scorer re-rolls flavour with a Random. Same situation as the exercise method — that is the point of demoing this one.


Step 2 — extract the dependency-using code into a lambda

After:

public void printSalesForecasts() {
    var names = List.of("Steve", "Julie", "Francis");
    Function<String, Integer> nameToSalesScore = name -> {   // ← NEW: the lambda...
        if (Objects.equals(name, "Steve")) {
            Scorer.flavour = IceCream.Strawberry;            // ← ...holds BOTH scorer touches
        } else {
            scorer.updateSelection();
        }
        return scorer.getSalesForecast();
    };
    for (String name : names) {
        System.out.println(name + " score: " + nameToSalesScore.apply(name)); // ← invocation replaced getSalesForecast
    }
}

I say: watch what goes inside — all of it. The static write too. If you leave the Scorer.flavour = ... behind in the loop, the extraction later drags the whole class in with it. (The exercise pairs will meet this as the drift; here I show it before it can happen.)

Checkpoint: compiles; behaviour unchanged; test still red & flaky. Also true and worth saying: this lambda is not pure. The unwanted dependency lives inside it — that is exactly what we want. It is an adapter.


Step 3 — slide the lambda definition to the top of the method

After:

public void printSalesForecasts() {
    Function<String, Integer> nameToSalesScore = name -> {   // ← SLID to the top...
        if (Objects.equals(name, "Steve")) {
            Scorer.flavour = IceCream.Strawberry;
        } else {
            scorer.updateSelection();
        }
        return scorer.getSalesForecast();
    };
    var names = List.of("Steve", "Julie", "Francis");
    for (String name : names) {
        System.out.println(name + " score: " + nameToSalesScore.apply(name)); // ← ...invocation stays here
    }
}

Slide, don't move. Watch the invocation — it stays exactly where the code used to be.

Checkpoint: compiles; test still red & flaky (unchanged behaviour).


Step 4 — extract the loop; the lambda becomes a parameter

Select from the loop through its closing brace, Extract Method.

After:

public void printSalesForecasts() {
    Function<String, Integer> nameToSalesScore = name -> {
        if (Objects.equals(name, "Steve")) {
            Scorer.flavour = IceCream.Strawberry;
        } else {
            scorer.updateSelection();
        }
        return scorer.getSalesForecast();
    };
    var names = List.of("Steve", "Julie", "Francis");
    var scores = collectSalesScores(names, nameToSalesScore);   // ← EXTRACTED
    for (var entry : scores.entrySet()) {
        System.out.println(entry.getKey() + " score: " + entry.getValue());
    }
}

private static LinkedHashMap<String, Integer> collectSalesScores(    // ← NEW — and already static
        List<String> names, Function<String, Integer> nameToSalesScore) {
    var scores = new LinkedHashMap<String, Integer>();
    for (String name : names) {
        scores.put(name, nameToSalesScore.apply(name));
    }
    return scores;
}

(LinkedHashMap because the demo prints names in order; a plain HashMap would shuffle them and confuse everyone. I say that out loud too.)

Checkpoint — the drift test:

  • grep -n scorer src/main/java/codingdojo/DailyForecasts.java — nothing in the new method.
  • Try to make it static (it already is). If the static write had stayed in the loop, this would not compile.
  • System.out is still in printSalesForecasts — that is fine. One split at a time; the printing loop is next time's material.

Step 5 — the overload, and the test goes green

Change Signature on the big method: the lambda becomes a parameter. A no-arg overload bakes in the real lambda, so callers are unchanged.

public void printSalesForecasts() {                              // ← no-arg overload: callers unchanged
    printSalesForecasts(name -> {
        if (Objects.equals(name, "Steve")) {
            Scorer.flavour = IceCream.Strawberry;
        } else {
            scorer.updateSelection();
        }
        return scorer.getSalesForecast();
    });
}

public void printSalesForecasts(                                 // ← the seam: λ is a parameter
        Function<String, Integer> nameToSalesScore) {
    var names = List.of("Steve", "Julie", "Francis");
    var scores = collectSalesScores(names, nameToSalesScore);
    for (var entry : scores.entrySet()) {
        System.out.println(entry.getKey() + " score: " + entry.getValue());
    }
}

The test goes through the seam with a pure lambda:

@Test
void salesForecasts() {
    var out = new ByteArrayOutputStream();
    var originalOut = System.out;
    System.setOut(new PrintStream(out));
    try {
        new DailyForecasts().printSalesForecasts(
                name -> Objects.equals(name, "Steve") ? 9 : 11);
    } finally {
        System.setOut(originalOut);
    }
    Approvals.verify(out.toString());
}

Approved output:

Steve score: 9
Julie score: 11
Francis score: 11

Checkpoint: green — deterministically. Scorer is never touched by the test.


End of demo

Same three moves as the exercise: extract, slide, extract. Scorer is out of the loop, and the lambda made it visible that the whole question is just "given a name, what is the sales score?" System.out and the clock are still in printSalesForecasts — that is the next hour, not this one.