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,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.