Files
learning-hour-maker/ideas/slice-with-lambda/storyboard/daily-forecasts-demo.md
T
Your Name 77ae05c28c Rename split-loop-with-lambda to slice-with-lambda
The refactoring is now published as Slice on sammancoaching.org:
https://sammancoaching.org/refactorings/slice.html

- git mv ideas/split-loop-with-lambda -> ideas/slice-with-lambda
- rename learning-hour file and update frontmatter name
- update paths in tools/rehearse scripts and report title
- update prose: 'Split Loop with Lambda' -> 'Slice with Lambda'
  in learning hour, exercise READMEs, storyboard, solution comments
- link the published Slice refactoring from the pattern draft
2026-09-29 15:47:30 +00:00

242 lines
7.2 KiB
Markdown

# Demo script — printSalesForecasts
This is what I demo in the Slice 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:
```java
// 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
![](svg/d0-start.svg)
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
![](svg/d1-test.svg)
```java
@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
![](svg/d2-extract-lambda.svg)
**After:**
```java
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
![](svg/d3-slide.svg)
**After:**
```java
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
![](svg/d4-extract.svg)
Select from the loop through its closing brace, Extract Method.
**After:**
```java
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
![](svg/d5-overload-test.svg)
Change Signature on the big method: the lambda becomes a parameter. A no-arg overload
bakes in the real lambda, so callers are unchanged.
```java
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:
```java
@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.