Add split-loop-with-lambda learning hour: draft, pre-factored exercise, solution

- Samman learning hour: 5/5/10/25/5 outline; pairs stop at extracted
  method taking the lambda as a parameter
- exercise/java: pre-factored variant of the kata (Q1/Q4 dead branch
  stripped, dates as constant), approval test red/flaky by design
- exercise/solution: collectExpectedWeather(LongFunction<Boolean>) +
  no-arg overload; deterministic approval test (all-sunny lambda)
- rehearse-with-agent.md: rehearsal setup with qwen + java-refactoring-mcp
This commit is contained in:
Your Name
2026-09-27 13:30:00 +00:00
parent 1a063860c9
commit a27e32688a
18 changed files with 737 additions and 0 deletions
@@ -0,0 +1,50 @@
# Exercise folder — Split Loop with Lambda (learning hour)
Contents:
- `java/` — pre-factored exercise repo (prefactored-longrange; runs with `mvn test`)
- `solution/` — reference end state after step 3 (extracted method taking the lambda)
- `rehearse-with-agent.md` — how Willem rehearses this with qwen-code + java-refactoring-mcp
## Reference solution
See `solution/`. End state of the production code (simplified):
```java
public Map<IceCream, Integer> longRangeForecast() {
return longRangeForecast(daysForward -> {
scorer.updateSelection();
return scorer.lookupWeather(daysForward);
});
}
public Map<IceCream, Integer> longRangeForecast(
LongFunction<Boolean> dayOffsetToWeather) {
var expectedWeather = collectExpectedWeather(dayOffsetToWeather);
// ... reset loop + scoring loop, unchanged ...
}
private static List<Boolean> collectExpectedWeather(
LongFunction<Boolean> dayOffsetToWeather) {
var expectedWeather = new ArrayList<Boolean>();
for (Instant interestingDate : DATES) {
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
expectedWeather.add(dayOffsetToWeather.apply(daysForward));
}
return expectedWeather;
}
```
Notes on the end state:
- Key move: `scorer` is used **only inside the lambda**; the extracted method
`collectExpectedWeather` has no dependency on `Scorer` at all — all its
inputs are parameters.
- The public no-arg overload keeps callers unchanged; it injects the
"real" weather lambda. The test calls the overload with a pure
`daysForward -> true` instead.
- The test is pure and glanceable: `alwaysSunny` weather gives
`{Strawberry:12, Chocolate:12, Vanilla:17, }` (approved file included).
- `scorer.updateSelection()` turned out to be deletable (it only mutates
static `Scorer.flavour` state that this method never reads). Leaving it
in the lambda is fine for step 3; deleting it is a debrief aha.
@@ -0,0 +1,5 @@
target/
*.received.txt
*.approved.txt
.idea/
*.iml
@@ -0,0 +1,37 @@
# IceCreamForecasting — Split Loop with Lambda (exercise)
Pre-factored variant of [IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata):
the Q1/Q4 dead branch is stripped and the hardcoded dates are a constant now.
Everything else — including the noise in the scoring loop — is untouched.
## Task
`longRangeForecast()` is hard to test: it calls `scorer`, and `Scorer`
stands for an external weather service you must not call from a unit test.
The rest of the method's inputs are only `DATES` and `ANCHOR_DATE`.
Get it under test:
1. First get any test running that calls `longRangeForecast()` —
it will fail or be flaky, because of the dependency on `Scorer`.
2. Then use the **Split Loop with Lambda** mechanics (6 steps, on the handout)
so that the weather collection becomes an extracted method that takes the
weather-reading as a **lambda parameter**.
3. Stop when you have an extracted method that takes the lambda as a parameter,
and a test that passes **without** touching `Scorer`.
## Constraint
Don't change `Scorer` and don't change the *behaviour* of
`longRangeForecast()`. Compile early, compile often.
## Debrief questions
- At which step did plain **Split Loop** stop working, and why?
(Hint: what type did your introduced variable end up being?)
- There are **two** calls to `scorer` in the weather loop.
What did you do with each one?
- Could you delete either call without changing the forecast?
Try it. Why did that work?
- Where would you take this next — more extraction, or moving the method
to another class where `Scorer` is unknown?
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>codingdojo</groupId>
<artifactId>icecream-split-loop-lambda-exercise</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.approvaltests</groupId>
<artifactId>approvaltests</artifactId>
<version>18.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,7 @@
package codingdojo;
public enum IceCream {
Strawberry,
Chocolate,
Vanilla;
}
@@ -0,0 +1,59 @@
package codingdojo;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
/**
* Pre-factored variant of LongRangeForecasts from
* https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata
*
* Differences from the kata original (deliberate, for the learning hour):
* - The Q1/Q4 branch was removed. It was dead code anyway: the unconditional
* loop right after it overwrites every value it sets. (Splitting loops
* sometimes reveals things like this.)
* - The hardcoded list of interesting dates is now a constant, DATES, and the
* anchor instant is ANCHOR_DATE. (Turning the dates into a parameter is a
* *later* refactoring — not part of this exercise.)
*
* Your task: see README.md
*/
public class LongRangeForecasts {
private static final Instant ANCHOR_DATE = Instant.parse("2023-04-26T14:00:00.000-07:00");
private static final List<Instant> DATES = List.of(
Instant.parse("2023-05-01T14:00:00.000-07:00"),
Instant.parse("2023-05-18T14:00:00.000-07:00"),
Instant.parse("2023-06-05T14:00:00.000-07:00"),
Instant.parse("2023-06-23T14:00:00.000-07:00")
);
private final Scorer scorer = new Scorer();
public Map<IceCream, Integer> longRangeForecast() {
var result = new HashMap<IceCream, Integer>();
for (IceCream flavour : IceCream.values()) {
result.put(flavour, 0);
}
List<Boolean> expectedWeather = new ArrayList<>();
for (Instant interestingDate : DATES) {
scorer.updateSelection();
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
Boolean weather = scorer.lookupWeather(daysForward);
expectedWeather.add(weather);
}
for (IceCream flavour : result.keySet()) {
result.put(flavour, result.get(flavour) + 10);
var sunnyHolidays = expectedWeather.stream().filter(s -> s).count();
if (sunnyHolidays > 2 && flavour == IceCream.Vanilla) {
result.put(flavour, result.get(flavour) + 5);
}
if (sunnyHolidays > 1 && expectedWeather.get(1)) {
result.put(flavour, result.get(flavour) + 2);
}
}
return result;
}
}
@@ -0,0 +1,49 @@
package codingdojo;
import java.util.Random;
/**
* Trimmed version of the kata's Scorer: only the parts LongRangeForecasts uses.
* lookupWeather stands in for an API call to a weather service — in the real
* system this is the "unwanted dependency".
*/
public class Scorer {
public static IceCream flavour = null;
public int getScore() {
boolean sunnyToday = lookupWeather();
if (flavour == null) {
return -1;
}
switch (flavour) {
case Strawberry:
return sunnyToday ? 10 : 5;
case Chocolate:
return 6;
case Vanilla:
return sunnyToday ? 7 : 5;
default:
return -1;
}
}
public boolean lookupWeather() {
return new Random().nextBoolean();
}
public boolean lookupWeather(long daysForward) {
// placeholder implementation - real version would make API call to weather service
return new Random().nextBoolean();
}
public void updateSelection() {
// placeholder implementation - real version would use machine learning to predict sales
var score = getScore();
if (score > 5) {
var randomizer = new Random();
IceCream[] allFlavours = IceCream.values();
flavour = allFlavours[randomizer.nextInt(allFlavours.length)];
}
}
}
@@ -0,0 +1,32 @@
package codingdojo;
import org.approvaltests.Approvals;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
public class LongRangeForecastsTest {
@Test
void longRangeForecast() {
var actual = new LongRangeForecasts().longRangeForecast();
Approvals.verify(printIceCreamForecast(actual));
}
private static String printIceCreamForecast(Map<IceCream, Integer> actual) {
var printedResult = new StringBuilder();
printedResult.append("{");
var sortedKeys = new ArrayList<>(actual.keySet());
Collections.sort(sortedKeys);
for (IceCream key : sortedKeys) {
printedResult.append(key);
printedResult.append(":");
printedResult.append(actual.get(key));
printedResult.append(", ");
}
printedResult.append("}");
return printedResult.toString();
}
}
@@ -0,0 +1,74 @@
# Rehearsing the exercise with a coding agent + java-refactoring-mcp
How Willem can rehearse the exercise (and test whether an agent can do it)
using [java-refactoring-mcp](https://github.com/pfichtner/java-refactoring-mcp)'s
CLI as the semantic refactoring backend. One-time setup, then a repeatable
session script.
## One-time setup
```bash
# 1. Build the tool (needs Java 21 + Maven 3.8+)
git clone https://github.com/pfichtner/java-refactoring-mcp
cd java-refactoring-mcp && mvn package -DskipTests
# → refactor-mcp/target/refactor-mcp-*-fat.jar
# 2. Install the CLI wrapper (see that repo's README, "CLI (optional)"):
# gives you the `java-refactor` command.
# 3. Sanctioned dirs for the MCP server (it refuses paths outside this list)
# — check the repo docs for the exact env var / flag.
# 4. Copy the exercise (NOT the solution folder) to a scratch workspace
cp -r exercise/java ~/scratch/prefactored-longrange
```
## Session script
Run the agent in `~/scratch/prefactored-longrange` with a prompt like:
> You may only change code by invoking `java-refactor` CLI commands
> (`java-refactor --help` lists them). Never edit `.java` files with text
> edits, except lines marked by the CLI as needing manual work.
> After each refactoring, run `mvn -q test` (it may fail — that is expected;
> the dependency on `Scorer` makes the method untestable — but it must
> still *compile*).
>
> Goal: in `LongRangeForecasts.longRangeForecast()`, the loop over `DATES`
> collects weather by calling `scorer` (an unwanted external dependency).
>
> 1. Introduce a lambda for the scorer-dependent code and move its
> definition to the top of the method, leaving the invocation where it is.
> (No tool supports this — do it as a minimal manual edit.)
> 2. Extract the weather-collection loop into a new method
> `collectExpectedWeather` that takes the lambda as a parameter
> (suggested type `LongFunction<Boolean>`).
> 3. Stop. Report the final diff.
Then compare the agent's end state against `exercise/solution/` (or diff
against it directly) and run `mvn test` with the solution's test + approved
file dropped in: it must pass deterministically.
## What this rehearsal tells you
- The mechanics-with-names prompt (introduce lambda / slide statement /
extract method + introduce parameter) is unambiguous enough that an agent
with weak tool-calling (qwen 35B) can follow it — if it can, a human
pair can too.
- Which steps the agent drifts on (usually step 1, where it tends to skip
the lambda and just extract a method that closes over `scorer` — the
exact mistake participants make). Collect these failure modes: they are
gold for the concept slide and the debrief.
- Time estimate for the exercise: run it twice, once tool-only, once with
the manual lambda edit. The gap between the two is the "no tool support"
friction participants will feel.
## Tool coverage cheat sheet
| Mechanic | Tool support |
|---|---|
| introduce lambda | none — manual edit |
| slide statement | none — manual edit |
| extract method | `java-refactor extract` |
| add lambda parameter | `java-refactor introduce-param` / `change-method-signature` |
| move method to another class (step 4, not in scope) | `java-refactor move-method` (call sites not rewritten) |
@@ -0,0 +1,4 @@
target/
*.received.txt
.idea/
*.iml
@@ -0,0 +1,27 @@
# Reference solution — end state after step 3
Derived from [IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata)
(same license as the kata applies to the copied `IceCream.java`).
Run with `mvn test` — the approval test passes deterministically without
touching the weather service.
## What changed vs the exercise start
1. Weather loop extracted to `collectExpectedWeather(LongFunction<Boolean>)`
— a static method whose only "unwanted dependency" is the lambda parameter.
2. Public no-arg `longRangeForecast()` delegates to a new overload that takes
the weather function, keeping existing callers working.
3. Test passes a pure lambda (`daysForward -> true`) — the original test
called the no-arg method and was flaky because of `Scorer`'s `Random`.
## Variations to try (for the debrief or a second round)
- `daysForward -> false` (no sunny holidays): scores drop to `+10` only.
- `daysForward -> daysForward == 5` (exactly one sunny holiday, and it is not
`expectedWeather.get(1)`, so no `+2`): all flavours `10`.
- Delete the `scorer.updateSelection()` call inside the production lambda:
the approved file does not change — the forecast never reads `Scorer.flavour`.
- Variation end state: skip the overload; make `collectExpectedWeather`
package-private and test it directly with pure lambdas. Trade-off:
tests the pure slice but not the composition (scoring loop).
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>codingdojo</groupId>
<artifactId>icecream-split-loop-lambda-exercise</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.approvaltests</groupId>
<artifactId>approvaltests</artifactId>
<version>18.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,7 @@
package codingdojo;
public enum IceCream {
Strawberry,
Chocolate,
Vanilla;
}
@@ -0,0 +1,73 @@
package codingdojo;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.LongFunction;
/**
* Reference end state of the learning hour exercise (step 3 of the
* Split Loop with Lambda mechanics — stop here; step 4, moving the method
* to another class, is future work).
*
* Derived from https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata
*/
public class LongRangeForecasts {
private static final Instant ANCHOR_DATE = Instant.parse("2023-04-26T14:00:00.000-07:00");
private static final List<Instant> DATES = List.of(
Instant.parse("2023-05-01T14:00:00.000-07:00"),
Instant.parse("2023-05-18T14:00:00.000-07:00"),
Instant.parse("2023-06-05T14:00:00.000-07:00"),
Instant.parse("2023-06-23T14:00:00.000-07:00")
);
private final Scorer scorer = new Scorer();
public Map<IceCream, Integer> longRangeForecast() {
return longRangeForecast(daysForward -> {
scorer.updateSelection();
return scorer.lookupWeather(daysForward);
});
}
/**
* The forecast, with the weather source injected as a function.
* Testable: pass any pure function instead of calling the weather service.
*/
public Map<IceCream, Integer> longRangeForecast(
LongFunction<Boolean> dayOffsetToWeather) {
var expectedWeather = collectExpectedWeather(dayOffsetToWeather);
var result = new HashMap<IceCream, Integer>();
for (IceCream flavour : IceCream.values()) {
result.put(flavour, 0);
}
for (IceCream flavour : result.keySet()) {
result.put(flavour, result.get(flavour) + 10);
var sunnyHolidays = expectedWeather.stream().filter(s -> s).count();
if (sunnyHolidays > 2 && flavour == IceCream.Vanilla) {
result.put(flavour, result.get(flavour) + 5);
}
if (sunnyHolidays > 1 && expectedWeather.get(1)) {
result.put(flavour, result.get(flavour) + 2);
}
}
return result;
}
/**
* Extracted method. Note: it uses no fields — all its inputs are
* parameters. The only leftover dependency is the lambda, which can
* be replaced by any pure function in tests.
*/
private static List<Boolean> collectExpectedWeather(
LongFunction<Boolean> dayOffsetToWeather) {
var expectedWeather = new ArrayList<Boolean>();
for (Instant interestingDate : DATES) {
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
expectedWeather.add(dayOffsetToWeather.apply(daysForward));
}
return expectedWeather;
}
}
@@ -0,0 +1,49 @@
package codingdojo;
import java.util.Random;
/**
* Trimmed version of the kata's Scorer: only the parts LongRangeForecasts uses.
* lookupWeather stands in for an API call to a weather service — in the real
* system this is the "unwanted dependency".
*/
public class Scorer {
public static IceCream flavour = null;
public int getScore() {
boolean sunnyToday = lookupWeather();
if (flavour == null) {
return -1;
}
switch (flavour) {
case Strawberry:
return sunnyToday ? 10 : 5;
case Chocolate:
return 6;
case Vanilla:
return sunnyToday ? 7 : 5;
default:
return -1;
}
}
public boolean lookupWeather() {
return new Random().nextBoolean();
}
public boolean lookupWeather(long daysForward) {
// placeholder implementation - real version would make API call to weather service
return new Random().nextBoolean();
}
public void updateSelection() {
// placeholder implementation - real version would use machine learning to predict sales
var score = getScore();
if (score > 5) {
var randomizer = new Random();
IceCream[] allFlavours = IceCream.values();
flavour = allFlavours[randomizer.nextInt(allFlavours.length)];
}
}
}
@@ -0,0 +1,35 @@
package codingdojo;
import org.approvaltests.Approvals;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.function.LongFunction;
public class LongRangeForecastsTest {
@Test
void longRangeForecast() {
// pure weather implementation: every holiday is sunny
LongFunction<Boolean> alwaysSunny = daysForward -> true;
var actual = new LongRangeForecasts().longRangeForecast(alwaysSunny);
Approvals.verify(printIceCreamForecast(actual));
}
private static String printIceCreamForecast(Map<IceCream, Integer> actual) {
var printedResult = new StringBuilder();
printedResult.append("{");
var sortedKeys = new ArrayList<>(actual.keySet());
Collections.sort(sortedKeys);
for (IceCream key : sortedKeys) {
printedResult.append(key);
printedResult.append(":");
printedResult.append(actual.get(key));
printedResult.append(", ");
}
printedResult.append("}");
return printedResult.toString();
}
}
@@ -0,0 +1 @@
{Strawberry:12, Chocolate:12, Vanilla:17, }
@@ -0,0 +1,166 @@
---
theme: refactoring
title: Split Loop with Lambda — when the variable you slide is a function
name: split-loop-with-lambda
kata: IceCreamForecasting-Refactoring-Kata (pre-factored "prefactored-longrange" variant)
difficulty: 4
author: Willem van den Ende
affiliation: QWAN
tags: refactoring java split-loop lambda testability composed-method
---
# Split Loop with Lambda
You know Split Loop: separate the loops that compute from the loops that
collect, using *introduce variable* and *slide statement*. But sometimes the
thing you want to slide is not a value — it is a **function call** that
reaches out to a dependency you do not want (a weather API, machine
learning, a database). Plain Split Loop stalls: the variable you introduce
would have to hold "the answer to a question that is not asked yet". The fix
is to make the laziness explicit: introduce a **lambda**, slide *that*, and
extract a method that takes the function as a parameter.
This is a follow-up session: the team has already done Split Loop (and has
used these mechanics on production code, unnamed). Here we name the pattern,
drill the mechanics on a pre-factored slice of
[IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata),
and end at a testable, pure seam.
## Learning Goals
* Given a loop whose body needs an outside dependency, perform the six
Split-Loop-with-Lambda mechanics (introduce lambda → slide → extract
method with function parameter) on unfamiliar code, without the handout
open.
* Recognize when plain Split Loop is not enough — the variable you need to
slide must be a *function*, not a value — and name the next move.
*(Testability is a consequence, not a goal this hour — it shows up as a
debrief question: "what just became testable, and why?")*
## Session Outline
* 5 min connect: count the concerns (on the kata's original method)
* 5 min concept: the mechanics, six steps with names
* 10 min demo: the weather slice of `LongRangeForecast` (Java)
* 25 min concrete: pairs do the whole pre-factored method
* 5 min conclusion: debrief + "what became testable?" + next steps
Total 50 min + buffer.
## Connect - Count the concerns
Show the original `longRangeForecast` from the kata (the one with the Q1/Q4
branch and the hardcoded dates) on a slide. Ask pairs, 90 seconds: "how many
different things is this method doing? Point at the lines."
Take answers: quarter default, reset loop, date list, weather collection,
scoring loop — five or six, depending how you count. Land the point: *your
production code looks like this too, and today we learn one move that cuts
one concern loose even when Split Loop alone cannot.*
## Concept - The six mechanics
Walk the pattern (handout: `zeroth-draft-of-refactoring-pattern.md`):
1. **Extract the dependency-using code into a lambda** — no tool does this;
it is a small manual edit.
2. **Slide the lambda definition** to the top of the method; leave the
invocation where it is.
3. **Extract the loop** into a method that takes the lambda as a parameter.
(IntelliJ: Extract Method, then Introduce Parameter / Change Signature.
Suggested type for this exercise: `LongFunction<Boolean>`.)
4. Move the method to a class where the unwanted dependency is unknown.
5. Test with a pure lambda instead of the real dependency.
6. Check what new test combinations became easy.
Steps 1–2 are the new move — that is where plain Split Loop stalls, because
the thing being slid is a *function*, not a value. Steps 3–6 are familiar
Split Loop territory. Today we stop after step 3 (with a test, step 5-lite);
step 4 is next time.
If there is time, show one slide: the slice graphic from the
[Slice learning hour](https://www.sammancoaching.org/learning_hours/testable_design/slice.html).
## Demo - The weather slice (10 min)
Do the six mechanics on a whiteboard-projection slice of
`LongRangeForecast` — only the `DATES` loop:
```java
List<Boolean> expectedWeather = new ArrayList<>();
for (Instant interestingDate : DATES) {
scorer.updateSelection();
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
Boolean weather = scorer.lookupWeather(daysForward);
expectedWeather.add(weather);
}
```
Narrate: *everything this loop needs from outside is "given a day offset,
is it sunny?"* Extract that question into a lambda, slide it up, extract the
loop into `collectExpectedWeather(LongFunction<Boolean> dayOffsetToWeather)`.
Note out loud where you paused and what you checked (compile, tests still
compile). Point at the *invocation site* staying behind — that is what
"slide, don't move" means.
## Concrete Practice - Pairs on prefactored-longrange (25 min)
Pairs, Rider or IntelliJ, one driver. Repo: `exercise/java` (a pre-factored
copy of the kata: Q1/Q4 dead branch stripped, dates as a constant — you gave
them the dates-as-parameter refactoring already, so the only "noise" left is
the two loops and the noise inside the scoring loop).
1. (3 min) Run the approval test. It is red and flaky — why? (`Scorer`
calls `Random`; it stands for the weather API.)
2. (17 min) Do the mechanics on the whole method. Careful: the weather loop
calls `scorer` **twice**. Everything the lambda needs has to be in scope
at the invocation site.
3. (5 min) When your extracted method takes the lambda as a parameter, make
the test pass with a pure lambda (e.g. `daysForward -> true`) and check
the approved file against `{Strawberry:12, Chocolate:12, Vanilla:17, }`.
Reference end state: `exercise/solution/`.
Debrief questions on a slide (from `exercise/java/README.md`):
- At which step did plain Split Loop stop working, and why?
- What did you do with each of the two `scorer` calls?
- Could you delete either call without changing the forecast? Try it.
Why did that work? *(updateSelection only mutates Scorer state this
method never reads — the lambda made that visible)*
- What just became testable that was not before?
## Conclusions - Name it, claim it, next move
- Pairs state the pattern in one sentence, using the words *lambda*,
*slide*, *function parameter*. Add their sentences to the pattern draft.
- Slide: "the new tests are actually glanceable and pure" — testability was
the side effect; ask first ("what was a benefit?"), then show the slide.
- Next moves this sets up: move the method to a class without `Scorer`
(step 4), turn `DATES` into a parameter (the refactor Willem did before
this exercise), and the ports-and-adapters view: a lambda is the smallest
Port + Adapter that could work.
- Feedback on the pattern draft: which steps felt named, which felt fuzzy?
## Facilitator notes
- Materials: `exercise/java` (participants), `exercise/solution`
(facilitator + fast pairs), handout = the pattern draft, slides =
original method + debrief questions.
- Fast pairs finish early? Have them do step 4 for real (move
`collectExpectedWeather` to a new class), or the
`daysForward -> daysForward == 5` variation from `solution/README.md`.
- If most pairs stall at step 3 (adding the parameter), interrupt after
~8 min and demo *just* that: Extract Method, then Change Signature to
`LongFunction<Boolean>`.
- The overload extension (test can't reach the pure seam because
`longRangeForecast()` bakes in the real lambda): Inline Variable on the
lambda local (⌘⌥N), Introduce Parameter on the lambda (⌥⌘P), test passes
`daysForward -> true`, then restore a 3-line no-arg overload delegating
with the real lambda. End state: `exercise/solution/`.
- Debrief fork: inject through the public overload (tests composition +
slice) vs. test `collectExpectedWeather` directly, package-private (tests
only the pure slice). No right answer — good conclusion discussion.
- Willem's rehearsal: `exercise/rehearse-with-agent.md` — run the exercise
with qwen + java-refactoring-mcp CLI first; agent failure modes become
concept-slide material.