Demo on the messier sibling of the exercise method: three unwanted dependencies in one loop (Scorer incl. static field write, System.out, clock — clock stripped in 2-min prep). Same six mechanics with SVGs in the storyboard style, ends green via overload + pure-lambda approval test. Pointer added to the learning hour's demo section.
242 lines
7.2 KiB
Markdown
242 lines
7.2 KiB
Markdown
# Demo script — printSalesForecasts
|
|
|
|
This is what I demo in the Split Loop 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.
|