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
7.2 KiB
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:
// 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.outis still inprintSalesForecasts— 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.