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
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
# 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
|
||||
|
||||

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

|
||||
|
||||
```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
|
||||
|
||||

|
||||
|
||||
**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
|
||||
|
||||

|
||||
|
||||
**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
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user