diff --git a/ideas/split-loop-with-lambda/exercise/README.md b/ideas/split-loop-with-lambda/exercise/README.md new file mode 100644 index 0000000..5489d0e --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/README.md @@ -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 longRangeForecast() { + return longRangeForecast(daysForward -> { + scorer.updateSelection(); + return scorer.lookupWeather(daysForward); + }); +} + +public Map longRangeForecast( + LongFunction dayOffsetToWeather) { + var expectedWeather = collectExpectedWeather(dayOffsetToWeather); + // ... reset loop + scoring loop, unchanged ... +} + +private static List collectExpectedWeather( + LongFunction dayOffsetToWeather) { + var expectedWeather = new ArrayList(); + 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. diff --git a/ideas/split-loop-with-lambda/exercise/java/.gitignore b/ideas/split-loop-with-lambda/exercise/java/.gitignore new file mode 100644 index 0000000..a366aaa --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/.gitignore @@ -0,0 +1,5 @@ +target/ +*.received.txt +*.approved.txt +.idea/ +*.iml diff --git a/ideas/split-loop-with-lambda/exercise/java/README.md b/ideas/split-loop-with-lambda/exercise/java/README.md new file mode 100644 index 0000000..1255990 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/README.md @@ -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? diff --git a/ideas/split-loop-with-lambda/exercise/java/pom.xml b/ideas/split-loop-with-lambda/exercise/java/pom.xml new file mode 100644 index 0000000..00662cb --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + codingdojo + icecream-split-loop-lambda-exercise + 1.0.0 + + + 17 + 17 + UTF-8 + + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + com.approvaltests + approvaltests + 18.6.0 + test + + + diff --git a/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/IceCream.java b/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/IceCream.java new file mode 100644 index 0000000..8326bcb --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/IceCream.java @@ -0,0 +1,7 @@ +package codingdojo; + +public enum IceCream { + Strawberry, + Chocolate, + Vanilla; +} diff --git a/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/LongRangeForecasts.java b/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/LongRangeForecasts.java new file mode 100644 index 0000000..8752ab2 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/LongRangeForecasts.java @@ -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 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 longRangeForecast() { + + var result = new HashMap(); + for (IceCream flavour : IceCream.values()) { + result.put(flavour, 0); + } + List 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; + } +} diff --git a/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/Scorer.java b/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/Scorer.java new file mode 100644 index 0000000..b0cef0a --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/src/main/java/codingdojo/Scorer.java @@ -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)]; + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise/java/src/test/java/codingdojo/LongRangeForecastsTest.java b/ideas/split-loop-with-lambda/exercise/java/src/test/java/codingdojo/LongRangeForecastsTest.java new file mode 100644 index 0000000..550c1e5 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/java/src/test/java/codingdojo/LongRangeForecastsTest.java @@ -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 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(); + } +} diff --git a/ideas/split-loop-with-lambda/exercise/rehearse-with-agent.md b/ideas/split-loop-with-lambda/exercise/rehearse-with-agent.md new file mode 100644 index 0000000..6d1f96b --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/rehearse-with-agent.md @@ -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`). +> 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) | diff --git a/ideas/split-loop-with-lambda/exercise/solution/.gitignore b/ideas/split-loop-with-lambda/exercise/solution/.gitignore new file mode 100644 index 0000000..933425f --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/.gitignore @@ -0,0 +1,4 @@ +target/ +*.received.txt +.idea/ +*.iml diff --git a/ideas/split-loop-with-lambda/exercise/solution/README.md b/ideas/split-loop-with-lambda/exercise/solution/README.md new file mode 100644 index 0000000..08464d0 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/README.md @@ -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)` + — 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). diff --git a/ideas/split-loop-with-lambda/exercise/solution/pom.xml b/ideas/split-loop-with-lambda/exercise/solution/pom.xml new file mode 100644 index 0000000..00662cb --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + codingdojo + icecream-split-loop-lambda-exercise + 1.0.0 + + + 17 + 17 + UTF-8 + + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + com.approvaltests + approvaltests + 18.6.0 + test + + + diff --git a/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/IceCream.java b/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/IceCream.java new file mode 100644 index 0000000..8326bcb --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/IceCream.java @@ -0,0 +1,7 @@ +package codingdojo; + +public enum IceCream { + Strawberry, + Chocolate, + Vanilla; +} diff --git a/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/LongRangeForecasts.java b/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/LongRangeForecasts.java new file mode 100644 index 0000000..990b09e --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/LongRangeForecasts.java @@ -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 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 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 longRangeForecast( + LongFunction dayOffsetToWeather) { + var expectedWeather = collectExpectedWeather(dayOffsetToWeather); + + var result = new HashMap(); + 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 collectExpectedWeather( + LongFunction dayOffsetToWeather) { + var expectedWeather = new ArrayList(); + for (Instant interestingDate : DATES) { + long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate); + expectedWeather.add(dayOffsetToWeather.apply(daysForward)); + } + return expectedWeather; + } +} diff --git a/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/Scorer.java b/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/Scorer.java new file mode 100644 index 0000000..b0cef0a --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/src/main/java/codingdojo/Scorer.java @@ -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)]; + } + } +} diff --git a/ideas/split-loop-with-lambda/exercise/solution/src/test/java/codingdojo/LongRangeForecastsTest.java b/ideas/split-loop-with-lambda/exercise/solution/src/test/java/codingdojo/LongRangeForecastsTest.java new file mode 100644 index 0000000..15464a3 --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/src/test/java/codingdojo/LongRangeForecastsTest.java @@ -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 alwaysSunny = daysForward -> true; + var actual = new LongRangeForecasts().longRangeForecast(alwaysSunny); + Approvals.verify(printIceCreamForecast(actual)); + } + + private static String printIceCreamForecast(Map 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(); + } +} diff --git a/ideas/split-loop-with-lambda/exercise/solution/src/test/java/codingdojo/LongRangeForecastsTest.longRangeForecast.approved.txt b/ideas/split-loop-with-lambda/exercise/solution/src/test/java/codingdojo/LongRangeForecastsTest.longRangeForecast.approved.txt new file mode 100644 index 0000000..d51dded --- /dev/null +++ b/ideas/split-loop-with-lambda/exercise/solution/src/test/java/codingdojo/LongRangeForecastsTest.longRangeForecast.approved.txt @@ -0,0 +1 @@ +{Strawberry:12, Chocolate:12, Vanilla:17, } diff --git a/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md b/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md new file mode 100644 index 0000000..241c4bc --- /dev/null +++ b/ideas/split-loop-with-lambda/split-loop-with-lambda-learning-hour.md @@ -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`.) +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 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 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`. +- 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.