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
@@ -0,0 +1,126 @@
|
||||
# Example 3 — Buyers: extract lambda in its purest form
|
||||
|
||||
Third rep of the Slice with Lambda mechanics, in C#. Example 1
|
||||
(`longRangeForecast`) had a drift trap and a dead call; the demo
|
||||
(`printSalesForecasts`) had three dependencies and grew into a role interface.
|
||||
This one has **one loop and one lambda-able expression, and nothing else** —
|
||||
the move in its purest form. Diagrams use the same
|
||||
[vocabulary as the main storyboard](storyboard.md).
|
||||
|
||||
Kata theme: the sellers exist (`DailyForecasts` — Steve, Julie, Francis), the
|
||||
buyers don't. `Buyers` computes a buyer's shopping-list total; `PriceList`
|
||||
stands for the external pricing service. Exercise repo: `../exercise3/csharp`,
|
||||
end state: `../exercise3/solution`.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — one loop, one unwanted dependency
|
||||
|
||||
**Goal:** the test is red or flaky — why? (`PriceList` adds random surge
|
||||
pricing; it stands for the external pricing service.)
|
||||
|
||||

|
||||
|
||||
**Before:**
|
||||
|
||||
```csharp
|
||||
public int ShoppingListTotal(string buyerName)
|
||||
{
|
||||
var buyer = ByName(buyerName);
|
||||
var total = 0;
|
||||
foreach (var flavour in buyer.Favourites)
|
||||
{
|
||||
total += buyer.ScoopsOf(flavour) * priceList.PriceFor(flavour); // ← unwanted dependency
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** run the test. Flaky. What does the loop need from outside?
|
||||
*Given a flavour, what does it cost?* That question is the lambda.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — extract the pricing call into a lambda, and slide it
|
||||
|
||||
**Goal:** the move, with nothing else in the way. Extract, then slide — the
|
||||
invocation stays in the loop.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```csharp
|
||||
public int ShoppingListTotal(string buyerName)
|
||||
{
|
||||
Func<IceCream, int> priceFor = flavour => priceList.PriceFor(flavour); // ← SLID to the top...
|
||||
var buyer = ByName(buyerName);
|
||||
var total = 0;
|
||||
foreach (var flavour in buyer.Favourites)
|
||||
{
|
||||
total += buyer.ScoopsOf(flavour) * priceFor(flavour); // ← ...invocation stays here
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** compiles; test still flaky (the lambda still calls
|
||||
`PriceList` — that is fine). Unlike the scoring-loop example, `priceList` is a
|
||||
*field*, so the lambda slides all the way to the top. Slide, don't move.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — extract the loop; the lambda becomes a parameter
|
||||
|
||||
**Goal:** select the loop, Extract Method. The loop touches no fields — the
|
||||
lambda is the only thing crossing the boundary.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```csharp
|
||||
public int ShoppingListTotal(string buyerName) // ← no-arg overload: callers unchanged
|
||||
{
|
||||
return ShoppingListTotal(ByName(buyerName), priceList.PriceFor); // ← the "real" pricing, as a method group
|
||||
}
|
||||
|
||||
public int ShoppingListTotal(Buyer buyer, Func<IceCream, int> priceFor) // ← the seam: pricing is a parameter
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var flavour in buyer.Favourites)
|
||||
{
|
||||
total += buyer.ScoopsOf(flavour) * priceFor(flavour);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
(The extracted loop method touches no fields; it sits in the same class as
|
||||
`ByName` for convenience. Move it to a class without `PriceList` any time —
|
||||
that is step 4 of the full pattern, unblocked.)
|
||||
|
||||
**The test — the whole point:**
|
||||
|
||||
```csharp
|
||||
[TestMethod]
|
||||
public void ShoppingListTotalUsesThePricingWePassIn()
|
||||
{
|
||||
// pure pricing function: everything costs 2.
|
||||
// Anke: 2 flavours x 2 scoops x 2 = 8 — deterministic, glanceable.
|
||||
var buyers = new Buyers();
|
||||
var anke = new Buyer("Anke", new List<IceCream> { IceCream.Strawberry, IceCream.Vanilla });
|
||||
Assert.AreEqual(8, buyers.ShoppingListTotal(anke, _ => 2));
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** green — deterministically. `PriceList` is never touched.
|
||||
|
||||
- `grep PriceFor src/` — the extracted method has no hits.
|
||||
- C# note: the extracted method stays an instance method here because the
|
||||
class also holds `ByName`; if you want a `static`, extract the loop only and
|
||||
keep the lookup in the composed method.
|
||||
|
||||
**Debrief in one line:** the loop now reads "for each favourite flavour, add
|
||||
scoops times price" — and the pricing is a question anyone can answer with one
|
||||
line of test code.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,353 @@
|
||||
# Slice with Lambda — storyboard / handout
|
||||
|
||||
Step-by-step mechanics of the **Slice with Lambda** refactoring (formerly called "Split Loop with Lambda"), one diagram per
|
||||
step, each step followed by the exact code (full before/after, highlights as `←`
|
||||
comments). Written to be sliced into slides later; until then it works as a handout.
|
||||
|
||||
Sources: `../zeroth-draft-of-refactoring-pattern.md` (the mechanics),
|
||||
`../slice-with-lambda-learning-hour.md` (where we stop: after step 3 + the test),
|
||||
`../exercise/rehearsal-findings.md` (the drift anti-pattern and the "both calls!"
|
||||
caution). Java only. Pristine start: `../exercise/java`, end state: `../exercise/solution`.
|
||||
C# demo examples live at `../exercise/csharp/` (MetaphorFree.cs, WeatherStation.cs, IceCreamShop.cs); the slides are at `~/qwan/slides-rjs/decks/refactoring/slice-with-lambda/slides.md`, served at http://localhost:9001/#/refactoring/slice-with-lambda.
|
||||
|
||||
## How to read the diagrams
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Where we start: one method, three loops, one unwanted dependency
|
||||
|
||||
**Goal:** see the problem before touching anything. `longRangeForecast()` resets, looks
|
||||
up weather, and scores — and the weather part calls `scorer`, which stands for an
|
||||
external weather service (it uses `Random`, so the approval test is red *and* flaky).
|
||||
|
||||

|
||||
|
||||
**Before:**
|
||||
|
||||
```java
|
||||
public Map<IceCream, Integer> longRangeForecast() {
|
||||
var result = new HashMap<IceCream, Integer>();
|
||||
for (IceCream flavour : IceCream.values()) {
|
||||
result.put(flavour, 0);
|
||||
}
|
||||
List<Boolean> expectedWeather = new ArrayList<>();
|
||||
for (Instant interestingDate : DATES) {
|
||||
scorer.updateSelection(); // ← unwanted dependency (void)
|
||||
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
|
||||
Boolean weather = scorer.lookupWeather(daysForward); // ← unwanted dependency (value)
|
||||
expectedWeather.add(weather);
|
||||
}
|
||||
for (IceCream flavour : result.keySet()) {
|
||||
result.put(flavour, result.get(flavour) + 10);
|
||||
var sunnyHolidays = expectedWeather.stream().filter(s -> s).count();
|
||||
if (sunnyHolidays > 2 && flavour == IceCream.Vanilla) {
|
||||
result.put(flavour, result.get(flavour) + 5);
|
||||
}
|
||||
if (sunnyHolidays > 1 && expectedWeather.get(1)) {
|
||||
result.put(flavour, result.get(flavour) + 2);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:**
|
||||
|
||||
- Run the approval test. It fails or flakes. Why? (`Scorer` calls `Random` — it *is*
|
||||
the unwanted dependency.)
|
||||
- Plain **Split Loop** can identify the three loops — but it cannot cut the weather
|
||||
loop loose, because the thing you would introduce and slide is not a value, it is a
|
||||
*call on an object you don't want*. That is where this pattern starts.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Extract the dependency-using code into a lambda
|
||||
|
||||
**Goal:** make the laziness explicit. Instead of asking the weather service now, we
|
||||
inject *a function that can answer the weather question when asked*. Both `scorer`
|
||||
calls go inside the lambda — the void one too (see the anti-pattern slide for what
|
||||
happens if you leave it behind). This step is a small manual edit: no refactoring tool
|
||||
does it.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```java
|
||||
public Map<IceCream, Integer> longRangeForecast() {
|
||||
var result = new HashMap<IceCream, Integer>();
|
||||
for (IceCream flavour : IceCream.values()) {
|
||||
result.put(flavour, 0);
|
||||
}
|
||||
List<Boolean> expectedWeather = new ArrayList<>();
|
||||
LongFunction<Boolean> dayOffsetToWeather = daysForward -> { // ← NEW: the lambda...
|
||||
scorer.updateSelection(); // ← ...holds BOTH scorer calls
|
||||
return scorer.lookupWeather(daysForward);
|
||||
};
|
||||
for (Instant interestingDate : DATES) {
|
||||
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
|
||||
expectedWeather.add(dayOffsetToWeather.apply(daysForward)); // ← invocation replaced lookupWeather
|
||||
}
|
||||
for (IceCream flavour : result.keySet()) {
|
||||
// ... scoring loop unchanged ...
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
(IDE will offer to add `import java.util.function.LongFunction;` — take it.)
|
||||
|
||||
**Checkpoint:**
|
||||
|
||||
- Compiles; behaviour unchanged; test still flaky — the lambda still calls `Scorer`
|
||||
for now, that is fine.
|
||||
- The lambda's parameter (`daysForward`) is *only* in scope inside the lambda. The
|
||||
loop still computes its own `daysForward` and hands it to the invocation.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Slide the lambda definition to the top of the method
|
||||
|
||||
**Goal:** *slide, don't move.* The **definition** of the lambda slides to the top of
|
||||
the method; the **invocation** (`.apply(...)`) stays exactly where the code used to
|
||||
be. This is the Split Loop "slide statement" move — except what slides is a
|
||||
function, not a value.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```java
|
||||
public Map<IceCream, Integer> longRangeForecast() {
|
||||
LongFunction<Boolean> dayOffsetToWeather = daysForward -> { // ← SLID to the top...
|
||||
scorer.updateSelection();
|
||||
return scorer.lookupWeather(daysForward);
|
||||
};
|
||||
var result = new HashMap<IceCream, Integer>();
|
||||
for (IceCream flavour : IceCream.values()) {
|
||||
result.put(flavour, 0);
|
||||
}
|
||||
List<Boolean> expectedWeather = new ArrayList<>();
|
||||
for (Instant interestingDate : DATES) {
|
||||
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
|
||||
expectedWeather.add(dayOffsetToWeather.apply(daysForward)); // ← ...invocation stays here
|
||||
}
|
||||
for (IceCream flavour : result.keySet()) {
|
||||
// ... scoring loop unchanged ...
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:**
|
||||
|
||||
- Compiles; behaviour unchanged; test still flaky.
|
||||
- Read the method out loud: everything it needs from outside the weather loop is
|
||||
now "given a day offset, is it sunny?" — the loop body itself no longer mentions
|
||||
`Scorer`.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Extract the weather loop; the lambda becomes a parameter
|
||||
|
||||
**Goal:** select from the `expectedWeather` declaration through the end of the
|
||||
weather loop and Extract Method. The IDE infers the parameter type
|
||||
(`LongFunction<Boolean>`) from the lambda variable. The extracted method has no
|
||||
field dependencies left — `DATES` and `ANCHOR_DATE` are constants — so it can be
|
||||
`static` right away.
|
||||
|
||||

|
||||
|
||||
**Before:** end state of step 2 (above).
|
||||
|
||||
**After:**
|
||||
|
||||
```java
|
||||
public Map<IceCream, Integer> longRangeForecast() {
|
||||
LongFunction<Boolean> dayOffsetToWeather = daysForward -> {
|
||||
scorer.updateSelection();
|
||||
return scorer.lookupWeather(daysForward);
|
||||
};
|
||||
var result = new HashMap<IceCream, Integer>();
|
||||
for (IceCream flavour : IceCream.values()) {
|
||||
result.put(flavour, 0);
|
||||
}
|
||||
List<Boolean> expectedWeather = collectExpectedWeather(dayOffsetToWeather); // ← EXTRACTED
|
||||
for (IceCream flavour : result.keySet()) {
|
||||
result.put(flavour, result.get(flavour) + 10);
|
||||
var sunnyHolidays = expectedWeather.stream().filter(s -> s).count();
|
||||
if (sunnyHolidays > 2 && flavour == IceCream.Vanilla) {
|
||||
result.put(flavour, result.get(flavour) + 5);
|
||||
}
|
||||
if (sunnyHolidays > 1 && expectedWeather.get(1)) {
|
||||
result.put(flavour, result.get(flavour) + 2);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<Boolean> collectExpectedWeather( // ← NEW — and already static
|
||||
LongFunction<Boolean> dayOffsetToWeather) {
|
||||
var expectedWeather = new ArrayList<Boolean>();
|
||||
for (Instant interestingDate : DATES) {
|
||||
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
|
||||
expectedWeather.add(dayOffsetToWeather.apply(daysForward));
|
||||
}
|
||||
return expectedWeather;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint — the drift test.** This is where the refactoring can silently fail:
|
||||
|
||||
- `grep -n scorer src/main/java/codingdojo/LongRangeForecasts.java` — the new method
|
||||
must contain **no** hits.
|
||||
- Try to make `collectExpectedWeather` `static` (it already is here). If any field
|
||||
reference remained, it would not compile.
|
||||
- If `scorer.updateSelection()` is still in the loop instead of inside the lambda,
|
||||
the dependency travelled *with* the extraction and the split is cosmetic. Put it
|
||||
in the lambda and start this step again.
|
||||
|
||||
### Inset — the overload: let the λ come from outside
|
||||
|
||||
**Goal:** right now the lambda is a local variable — the test still cannot reach the
|
||||
seam. Change Signature on the big method: *add* the lambda as a parameter and *drop*
|
||||
the local. Then give callers a no-arg overload that bakes in the "real" lambda.
|
||||
|
||||

|
||||
|
||||
**After:**
|
||||
|
||||
```java
|
||||
public Map<IceCream, Integer> longRangeForecast() { // ← no-arg overload: callers unchanged
|
||||
return longRangeForecast(daysForward -> {
|
||||
scorer.updateSelection();
|
||||
return scorer.lookupWeather(daysForward);
|
||||
});
|
||||
}
|
||||
|
||||
public Map<IceCream, Integer> longRangeForecast( // ← the seam: λ is a parameter
|
||||
LongFunction<Boolean> dayOffsetToWeather) {
|
||||
var expectedWeather = collectExpectedWeather(dayOffsetToWeather);
|
||||
// ... reset loop + scoring loop, unchanged ...
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Checkpoint:** compiles; existing callers (and the old flaky test, if you keep it)
|
||||
still work through the no-arg overload.
|
||||
|
||||
---
|
||||
|
||||
## Step 5-lite — test through the seam with a pure lambda
|
||||
|
||||
**Goal:** replace the weather service with a pure function in the test. Deterministic,
|
||||
glanceable, and `Scorer` is never touched.
|
||||
|
||||

|
||||
|
||||
**The test (end of document version — this is the whole thing):**
|
||||
|
||||
```java
|
||||
package codingdojo;
|
||||
|
||||
import org.approvaltests.Approvals;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.function.LongFunction;
|
||||
|
||||
public class LongRangeForecastsTest {
|
||||
|
||||
@Test
|
||||
void longRangeForecast() {
|
||||
// pure weather implementation: every holiday is sunny
|
||||
LongFunction<Boolean> alwaysSunny = daysForward -> true;
|
||||
var actual = new LongRangeForecasts().longRangeForecast(alwaysSunny);
|
||||
Approvals.verify(printIceCreamForecast(actual));
|
||||
}
|
||||
|
||||
private static String printIceCreamForecast(Map<IceCream, Integer> actual) {
|
||||
var printedResult = new StringBuilder();
|
||||
printedResult.append("{");
|
||||
var sortedKeys = new ArrayList<>(actual.keySet());
|
||||
Collections.sort(sortedKeys);
|
||||
for (IceCream key : sortedKeys) {
|
||||
printedResult.append(key);
|
||||
printedResult.append(":");
|
||||
printedResult.append(actual.get(key));
|
||||
printedResult.append(", ");
|
||||
}
|
||||
printedResult.append("}");
|
||||
return printedResult.toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Approved output:
|
||||
|
||||
```
|
||||
{Strawberry:12, Chocolate:12, Vanilla:17, }
|
||||
```
|
||||
|
||||
**Checkpoint:** `mvn test` is green — deterministically. Nothing in the test path
|
||||
touches `Scorer`. *What just became testable that was not before?*
|
||||
|
||||
---
|
||||
|
||||
## Debrief
|
||||
|
||||
### The dead call the lambda exposed
|
||||
|
||||

|
||||
|
||||
`scorer.updateSelection()` re-rolls `Scorer.flavour` — state that nothing in this
|
||||
method ever reads (its only real consumer is the unrelated `salesForecast()`; it is
|
||||
copy-paste residue in the long-range loop). Delete it from the lambda: the approved
|
||||
file does not change. The refactoring didn't just hide the dependency — it exposed
|
||||
that one of the two calls was dead all along.
|
||||
|
||||
### Anti-pattern — the drift: scorer left behind in the extracted method
|
||||
|
||||

|
||||
|
||||
If you lambda-ize only the value-producing call and leave `updateSelection()` as
|
||||
"loop machinery", the extraction carries the `scorer` field into the new method:
|
||||
|
||||
```java
|
||||
// DRIFTED — the method still depends on the field; step 4 (move it
|
||||
// to another class) would be blocked, and the split is cosmetic.
|
||||
private List<Boolean> collectExpectedWeather(
|
||||
LongFunction<Boolean> dayOffsetToWeather) {
|
||||
var expectedWeather = new ArrayList<Boolean>();
|
||||
for (Instant interestingDate : DATES) {
|
||||
scorer.updateSelection(); // ← LEFT BEHIND
|
||||
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
|
||||
expectedWeather.add(dayOffsetToWeather.apply(daysForward));
|
||||
}
|
||||
return expectedWeather;
|
||||
}
|
||||
```
|
||||
|
||||
Worse: the test still passes (the stray call only mutates state nothing reads), so
|
||||
**green ≠ drift-free**. Catch it mechanically: `grep scorer` on the extracted method
|
||||
must return nothing, or try to make it `static` — it won't compile with a field
|
||||
reference.
|
||||
|
||||
---
|
||||
|
||||
## Next moves (not storyboarded yet)
|
||||
|
||||
These are steps 4 and 6 of the full pattern — the learning hour stops before them:
|
||||
|
||||
- **Step 4:** move `collectExpectedWeather` to a class where `Scorer` is unknown.
|
||||
(It is already `static` and scorer-free — the move is now unblocked.)
|
||||
- **Step 6:** more pure-lambda combinations are now one line each:
|
||||
`daysForward -> false` (no sunny holidays → all flavours 10),
|
||||
`daysForward -> daysForward == 5` (exactly one sunny holiday, not at index 1 → all 10).
|
||||
- The design view: the lambda is the smallest **Port** (function signature) +
|
||||
**Adapter** (the extracted code with the unwanted dependency) that could work —
|
||||
see the Role Interface discussion in the pattern draft.
|
||||
@@ -0,0 +1,47 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 0 — one method, three loops, one unwanted dependency</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">The weather loop reaches outside the method to Scorer (which uses Random) — that is why the test is red & flaky.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="340" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
|
||||
|
||||
<!-- reset loop -->
|
||||
<rect x="80" y="112" width="300" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="134" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (IceCream flavour : IceCream.values())</text>
|
||||
<text x="94" y="154" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> result.put(flavour, 0);</text>
|
||||
|
||||
<!-- weather loop -->
|
||||
<rect x="80" y="184" width="300" height="112" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="206" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant interestingDate : DATES)</text>
|
||||
<text x="94" y="228" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">scorer.updateSelection();</text>
|
||||
<text x="94" y="248" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">daysForward = DAYS.between(ANCHOR, date)</text>
|
||||
<text x="94" y="268" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">weather = scorer.lookupWeather(daysForward);</text>
|
||||
|
||||
<!-- scoring loop -->
|
||||
<rect x="80" y="312" width="300" height="104" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="334" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (flavour : keySet) result += 10</text>
|
||||
<text x="94" y="356" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunnyHolidays > 2 && Vanilla) += 5</text>
|
||||
<text x="94" y="376" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunnyHolidays > 1 && weather[1]) += 2</text>
|
||||
<text x="94" y="402" font-size="10" fill="#64748b">scoring loop</text>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="580" y="200" width="150" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">stands for the weather</text>
|
||||
<text x="655" y="266" font-size="11" fill="#7f1d1d" text-anchor="middle">service — uses Random</text>
|
||||
|
||||
<line x1="384" y1="228" x2="572" y2="222" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="384" y1="268" x2="572" y2="254" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
|
||||
<!-- test -->
|
||||
<rect x="580" y="330" width="150" height="60" rx="8" fill="#ffffff" stroke="#94a3b8" stroke-dasharray="4 3"/>
|
||||
<text x="655" y="354" font-size="12" fill="#334155" text-anchor="middle">approval test</text>
|
||||
<text x="655" y="372" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">red & flaky ✗</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 1 — extract the dependency-using code into a lambda</text>
|
||||
<text x="24" y="56" font-size="13" fill="#dc2626" font-weight="bold">Both scorer calls go inside the lambda — the void one too. The invocation stays in the loop.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="340" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
|
||||
|
||||
<!-- reset loop -->
|
||||
<rect x="80" y="112" width="300" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="134" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (IceCream flavour : IceCream.values())</text>
|
||||
<text x="94" y="154" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> result.put(flavour, 0);</text>
|
||||
|
||||
<!-- weather loop -->
|
||||
<rect x="80" y="184" width="300" height="144" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant interestingDate : DATES)</text>
|
||||
|
||||
<!-- lambda (inside loop, where the code used to be) -->
|
||||
<rect x="94" y="214" width="256" height="74" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="106" y="234" font-size="12" font-weight="bold" fill="#9333ea">λ dayOffsetToWeather</text>
|
||||
<text x="106" y="254" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
|
||||
<text x="106" y="272" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.lookupWeather(days);</text>
|
||||
|
||||
<!-- invocation -->
|
||||
<circle cx="90" cy="308" r="4" fill="#9333ea"/>
|
||||
<text x="102" y="312" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8">expectedWeather.add(λ.apply(daysForward));</text>
|
||||
|
||||
<!-- scoring loop -->
|
||||
<rect x="80" y="344" width="300" height="80" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="368" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (flavour : keySet) result += 10</text>
|
||||
<text x="94" y="388" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">+5 / +2 ifs, unchanged</text>
|
||||
<text x="94" y="410" font-size="10" fill="#64748b">scoring loop</text>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="580" y="200" width="150" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">still called —</text>
|
||||
<text x="655" y="266" font-size="11" fill="#7f1d1d" text-anchor="middle">but only via the λ now</text>
|
||||
|
||||
<line x1="354" y1="244" x2="572" y2="228" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="354" y1="272" x2="572" y2="256" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 2 — slide the lambda definition to the top of the method</text>
|
||||
<text x="24" y="56" font-size="13" fill="#9333ea" font-weight="bold">Slide, don't move: the definition slides up, the invocation stays exactly where it was.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="340" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
|
||||
|
||||
<!-- lambda at top -->
|
||||
<rect x="94" y="104" width="256" height="74" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="106" y="124" font-size="12" font-weight="bold" fill="#9333ea">λ dayOffsetToWeather</text>
|
||||
<text x="106" y="144" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
|
||||
<text x="106" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.lookupWeather(days);</text>
|
||||
|
||||
<!-- reset loop -->
|
||||
<rect x="80" y="196" width="300" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="218" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (IceCream flavour : IceCream.values())</text>
|
||||
<text x="94" y="238" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> result.put(flavour, 0);</text>
|
||||
|
||||
<!-- weather loop -->
|
||||
<rect x="80" y="268" width="300" height="100" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="290" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant interestingDate : DATES)</text>
|
||||
|
||||
<!-- ghost of old position -->
|
||||
<rect x="94" y="300" width="256" height="28" rx="6" fill="none" stroke="#9333ea" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="106" y="318" font-size="10" fill="#9333ea" font-style="italic">λ used to be defined here</text>
|
||||
|
||||
<circle cx="90" cy="352" r="4" fill="#9333ea"/>
|
||||
<text x="102" y="356" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8">expectedWeather.add(λ.apply(daysForward));</text>
|
||||
|
||||
<!-- scoring loop -->
|
||||
<rect x="80" y="384" width="300" height="40" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="409" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scoring loop: += 10 / +5 / +2</text>
|
||||
|
||||
<!-- slide arrow -->
|
||||
<line x1="219" y1="298" x2="219" y2="184" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
|
||||
<text x="232" y="192" font-size="12" font-weight="bold" fill="#9333ea">slide</text>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="580" y="200" width="150" height="80" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">still called —</text>
|
||||
<text x="655" y="266" font-size="11" fill="#7f1d1d" text-anchor="middle">but only via the λ now</text>
|
||||
|
||||
<line x1="354" y1="130" x2="572" y2="226" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="354" y1="160" x2="572" y2="250" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,67 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 3 — extract the weather loop; the lambda becomes a parameter</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">Extract Method on the expectedWeather block. Checkpoint: grep scorer → no hits; try static → compiles.</text>
|
||||
|
||||
<!-- left: original method -->
|
||||
<rect x="50" y="74" width="300" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="66" y="96" font-size="14" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
|
||||
|
||||
<!-- lambda at top -->
|
||||
<rect x="64" y="104" width="272" height="68" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="78" y="126" font-size="12" font-weight="bold" fill="#9333ea">λ dayOffsetToWeather</text>
|
||||
<text x="78" y="146" font-size="10" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
|
||||
<text x="78" y="162" font-size="10" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.lookupWeather(daysForward);</text>
|
||||
|
||||
<!-- reset loop -->
|
||||
<rect x="64" y="188" width="272" height="44" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="78" y="214" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">reset loop: result.put(flavour, 0)</text>
|
||||
|
||||
<!-- call site -->
|
||||
<rect x="64" y="248" width="272" height="48" rx="8" fill="#ffffff" stroke="#475569" stroke-width="1.5"/>
|
||||
<text x="78" y="268" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">expectedWeather =</text>
|
||||
<text x="78" y="286" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">collectExpectedWeather(<tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
|
||||
|
||||
<!-- scoring loop -->
|
||||
<rect x="64" y="312" width="272" height="84" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="78" y="334" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (flavour : keySet) result += 10</text>
|
||||
<text x="78" y="354" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunny > 2 && Vanilla) += 5</text>
|
||||
<text x="78" y="374" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (sunny > 1 && weather[1]) += 2</text>
|
||||
|
||||
<!-- purple arrow: lambda becomes parameter -->
|
||||
<line x1="340" y1="272" x2="452" y2="146" stroke="#9333ea" stroke-width="1.5" marker-end="url(#arrP)"/>
|
||||
<text x="396" y="214" font-size="10" font-weight="bold" fill="#9333ea" text-anchor="middle" transform="rotate(-48 396 214)">λ becomes a parameter</text>
|
||||
|
||||
<!-- right: extracted method -->
|
||||
<rect x="440" y="74" width="290" height="280" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="456" y="96" font-size="13" font-weight="bold" fill="#0f172a">collectExpectedWeather</text>
|
||||
<text x="456" y="112" font-size="11" fill="#475569">static — no fields, all inputs are parameters</text>
|
||||
|
||||
<!-- parameter pill -->
|
||||
<rect x="456" y="122" width="258" height="32" rx="16" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="585" y="142" font-size="11" font-weight="bold" fill="#9333ea" text-anchor="middle">λ LongFunction<Boolean> dayOffsetToWeather</text>
|
||||
|
||||
<!-- weather loop -->
|
||||
<rect x="456" y="166" width="258" height="110" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="470" y="190" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant d : DATES)</text>
|
||||
<text x="470" y="212" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">daysForward = between(ANCHOR, d)</text>
|
||||
<text x="470" y="234" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">expected.add(<tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(daysForward))</text>
|
||||
<text x="470" y="260" font-size="10" fill="#64748b">no scorer reference in here</text>
|
||||
|
||||
<text x="456" y="308" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">return expectedWeather;</text>
|
||||
|
||||
<!-- scorer, out of reach -->
|
||||
<rect x="560" y="386" width="160" height="50" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 3"/>
|
||||
<text x="640" y="408" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="640" y="426" font-size="10" fill="#7f1d1d" text-anchor="middle">out of reach from in here</text>
|
||||
<line x1="640" y1="384" x2="640" y2="360" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<text x="650" y="376" font-size="10" font-weight="bold" fill="#dc2626">✗ cannot reach in</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,53 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#475569"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Inset — the overload: let the λ come from outside</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">Add the λ as a parameter (Change Signature), drop the local. A no-arg overload bakes in the real λ — callers unchanged.</text>
|
||||
|
||||
<!-- left: no-arg overload -->
|
||||
<rect x="50" y="90" width="320" height="250" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="66" y="114" font-size="13" font-weight="bold" fill="#0f172a">longRangeForecast()</text>
|
||||
<text x="66" y="130" font-size="10" fill="#475569">no-arg overload — existing callers keep working</text>
|
||||
|
||||
<rect x="66" y="144" width="288" height="92" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="80" y="166" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">daysForward -> {</text>
|
||||
<text x="80" y="186" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626"> scorer.updateSelection();</text>
|
||||
<text x="80" y="206" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626"> return scorer.lookupWeather(days);</text>
|
||||
<text x="80" y="226" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">}</text>
|
||||
|
||||
<text x="66" y="278" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">return longRangeForecast(<tspan fill="#9333ea" font-weight="bold">λ</tspan>);</text>
|
||||
<text x="66" y="316" font-size="10" fill="#64748b">the "real" weather lambda lives here</text>
|
||||
|
||||
<!-- delegate arrow -->
|
||||
<line x1="374" y1="150" x2="424" y2="150" stroke="#475569" stroke-width="1.5" marker-end="url(#arrG)"/>
|
||||
<text x="376" y="140" font-size="10" fill="#475569">delegates</text>
|
||||
|
||||
<!-- right: the seam -->
|
||||
<rect x="430" y="90" width="300" height="250" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="446" y="114" font-size="12" font-weight="bold" fill="#0f172a">longRangeForecast(<tspan fill="#9333ea">λ</tspan>)</text>
|
||||
<text x="446" y="132" font-size="10" fill="#475569">the seam: weather is a parameter now</text>
|
||||
|
||||
<rect x="446" y="150" width="268" height="40" rx="8" fill="#ffffff" stroke="#475569" stroke-width="1"/>
|
||||
<text x="460" y="166" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">expectedWeather =</text>
|
||||
<text x="460" y="182" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a"> collectExpectedWeather(<tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
|
||||
|
||||
<rect x="446" y="206" width="268" height="40" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="460" y="230" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">reset loop: result.put(flavour, 0)</text>
|
||||
|
||||
<rect x="446" y="262" width="268" height="56" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="460" y="284" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scoring loop: += 10 / +5 / +2</text>
|
||||
<text x="460" y="304" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return result;</text>
|
||||
|
||||
<!-- scorer still reachable from the no-arg overload only -->
|
||||
<rect x="80" y="380" width="180" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="170" y="402" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="170" y="422" font-size="10" fill="#7f1d1d" text-anchor="middle">only the no-arg overload knows it</text>
|
||||
<line x1="170" y1="378" x2="170" y2="346" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,51 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrGr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 5-lite — test through the seam with a pure lambda</text>
|
||||
<text x="24" y="56" font-size="13" fill="#16a34a" font-weight="bold">The test passes alwaysSunny. Scorer is never touched: no Random, deterministic, glanceable.</text>
|
||||
|
||||
<!-- test -->
|
||||
<rect x="50" y="160" width="230" height="130" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="66" y="186" font-size="13" font-weight="bold" fill="#14532d">LongRangeForecastsTest</text>
|
||||
<text x="66" y="210" font-size="10" fill="#475569">LongFunction<Boolean></text>
|
||||
<text x="66" y="232" font-size="12" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#16a34a">alwaysSunny = days -> true</text>
|
||||
<text x="66" y="258" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">longRangeForecast(alwaysSunny)</text>
|
||||
|
||||
<!-- middle: overload -->
|
||||
<rect x="340" y="150" width="180" height="170" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="354" y="176" font-size="11" font-weight="bold" fill="#0f172a">longRangeForecast(<tspan fill="#9333ea">λ</tspan>)</text>
|
||||
<text x="354" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">collectExpected-</text>
|
||||
<text x="354" y="220" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">Weather(<tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
|
||||
<text x="354" y="248" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">reset loop</text>
|
||||
<text x="354" y="274" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scoring loop</text>
|
||||
<text x="354" y="296" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">+= 10 / +5 / +2</text>
|
||||
|
||||
<!-- right: extracted method -->
|
||||
<rect x="560" y="150" width="170" height="170" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="574" y="176" font-size="10" font-weight="bold" fill="#0f172a">collectExpectedWeather(λ)</text>
|
||||
<text x="574" y="204" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for d : DATES</text>
|
||||
<text x="574" y="226" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">expected.add(<tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(d))</text>
|
||||
<text x="574" y="252" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return expectedWeather</text>
|
||||
<text x="574" y="290" font-size="10" fill="#16a34a" font-weight="bold">pure ✓</text>
|
||||
|
||||
<!-- green arrows -->
|
||||
<line x1="284" y1="225" x2="334" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
|
||||
<text x="286" y="214" font-size="10" fill="#16a34a">alwaysSunny</text>
|
||||
<line x1="524" y1="225" x2="554" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
|
||||
|
||||
<!-- scorer, never called -->
|
||||
<rect x="560" y="370" width="170" height="56" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.8"/>
|
||||
<text x="645" y="392" font-size="12" font-weight="bold" fill="#94a3b8" text-anchor="middle">Scorer</text>
|
||||
<text x="645" y="412" font-size="10" fill="#64748b" text-anchor="middle">never called by the test ✗</text>
|
||||
<line x1="645" y1="368" x2="645" y2="326" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<text x="655" y="350" font-size="11" font-weight="bold" fill="#dc2626">✗</text>
|
||||
|
||||
<text x="50" y="430" font-size="12" fill="#475569">Approved: {Strawberry:12, Chocolate:12, Vanilla:17, } — every run, not just a lucky roll.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Debrief — the lambda made dead code visible</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">updateSelection() re-rolls Scorer.flavour — state nothing in this method ever reads. Delete it; the approved file is unchanged.</text>
|
||||
|
||||
<!-- lambda box -->
|
||||
<rect x="200" y="110" width="360" height="124" rx="10" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="216" y="136" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">daysForward -> {</text>
|
||||
<text x="216" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">scorer.updateSelection();</text>
|
||||
<line x1="216" y1="157" x2="408" y2="157" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="420" y="162" font-size="10" font-weight="bold" fill="#dc2626">✗ delete me</text>
|
||||
<text x="216" y="188" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return scorer.lookupWeather(daysForward);</text>
|
||||
<text x="216" y="212" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#9333ea">}</text>
|
||||
|
||||
<!-- result -->
|
||||
<text x="200" y="290" font-size="13" font-weight="bold" fill="#16a34a">✓ approved file unchanged:</text>
|
||||
<text x="200" y="316" font-size="13" font-family="Menlo, Consolas, monospace" fill="#166534">{Strawberry:12, Chocolate:12, Vanilla:17, }</text>
|
||||
|
||||
<!-- explanation -->
|
||||
<rect x="200" y="350" width="360" height="72" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="216" y="374" font-size="11" fill="#334155">Why: Scorer.flavour's only real consumer is the unrelated</text>
|
||||
<text x="216" y="392" font-size="11" fill="#334155">salesForecast(). In the long-range loop it was copy-paste</text>
|
||||
<text x="216" y="410" font-size="11" fill="#334155">residue — visible only once the lambda made it explicit.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Anti-pattern — the drift: scorer left behind in the extracted method</text>
|
||||
<text x="24" y="56" font-size="13" fill="#dc2626" font-weight="bold">Lambda-ize only the value-producing call, and the dependency travels with the extraction.</text>
|
||||
<text x="24" y="74" font-size="13" fill="#dc2626" font-weight="bold">Happened in 2 of 6 agent rehearsal runs — behaviour passed, design goal silently failed.</text>
|
||||
|
||||
<!-- drifted extracted method -->
|
||||
<rect x="250" y="110" width="300" height="190" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="266" y="134" font-size="12" font-weight="bold" fill="#0f172a">collectExpectedWeather(<tspan fill="#9333ea">λ</tspan>)</text>
|
||||
<text x="516" y="136" font-size="22" font-weight="bold" fill="#dc2626">✗</text>
|
||||
|
||||
<rect x="266" y="148" width="268" height="110" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="280" y="172" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (Instant d : DATES)</text>
|
||||
<text x="280" y="196" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">scorer.updateSelection(); ✗</text>
|
||||
<text x="280" y="218" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">daysForward = between(ANCHOR, d)</text>
|
||||
<text x="280" y="240" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">expected.add(<tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(daysForward))</text>
|
||||
|
||||
<text x="266" y="284" font-size="10" fill="#dc2626">not static anymore — it uses the field</text>
|
||||
|
||||
<!-- scorer, still wired in -->
|
||||
<rect x="600" y="140" width="130" height="56" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="665" y="162" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="665" y="182" font-size="10" fill="#7f1d1d" text-anchor="middle">still wired in</text>
|
||||
<line x1="552" y1="192" x2="594" y2="172" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrR)"/>
|
||||
|
||||
<!-- what it breaks -->
|
||||
<text x="250" y="330" font-size="12" fill="#475569">Behaviour looks fine (the test can still pass) — but the split is cosmetic:</text>
|
||||
<text x="250" y="350" font-size="12" fill="#475569">step 4, moving the method to a class without Scorer, is now blocked.</text>
|
||||
|
||||
<!-- catch it -->
|
||||
<rect x="250" y="374" width="310" height="72" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="266" y="398" font-size="11" font-weight="bold" fill="#92400e">Catch it mechanically:</text>
|
||||
<text x="266" y="418" font-size="11" fill="#92400e">• grep scorer on the extracted method → no hits</text>
|
||||
<text x="266" y="436" font-size="11" fill="#92400e">• try static → won't compile with a field ref</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,38 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 0 — one loop, one unwanted dependency</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">PriceList stands for the external pricing service (random surge pricing) — that is why the test is red & flaky.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="400" height="260" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">ShoppingListTotal(buyerName)</text>
|
||||
|
||||
<text x="80" y="126" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">buyer = ByName(buyerName)</text>
|
||||
|
||||
<!-- loop -->
|
||||
<rect x="80" y="140" width="360" height="120" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="166" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">foreach (var flavour : buyer.Favourites)</text>
|
||||
<text x="94" y="196" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total += buyer.ScoopsOf(flavour) *</text>
|
||||
<text x="94" y="216" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626"> priceList.PriceFor(flavour);</text>
|
||||
<text x="94" y="244" font-size="10" fill="#64748b">the pricing loop — one expression, nothing else</text>
|
||||
|
||||
<text x="80" y="300" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return total;</text>
|
||||
|
||||
<!-- PriceList -->
|
||||
<rect x="580" y="150" width="150" height="90" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="180" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">PriceList</text>
|
||||
<text x="655" y="200" font-size="11" fill="#7f1d1d" text-anchor="middle">stands for the pricing</text>
|
||||
<text x="655" y="216" font-size="11" fill="#7f1d1d" text-anchor="middle">service — Random surge</text>
|
||||
|
||||
<line x1="444" y1="206" x2="572" y2="192" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
|
||||
<!-- test -->
|
||||
<rect x="580" y="290" width="150" height="60" rx="8" fill="#ffffff" stroke="#94a3b8" stroke-dasharray="4 3"/>
|
||||
<text x="655" y="314" font-size="12" fill="#334155" text-anchor="middle">test: Anke = 10?</text>
|
||||
<text x="655" y="332" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">red & flaky ✗</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,50 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 1 — extract the pricing call into a lambda, and slide it</text>
|
||||
<text x="24" y="56" font-size="13" fill="#9333ea" font-weight="bold">One expression, nothing else in the way. priceList is a field, so the lambda slides all the way to the top.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="400" height="330" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">ShoppingListTotal(buyerName)</text>
|
||||
|
||||
<!-- lambda at top -->
|
||||
<rect x="80" y="110" width="360" height="82" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="94" y="136" font-size="12" font-weight="bold" fill="#9333ea">λ priceFor</text>
|
||||
<text x="94" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">Func<IceCream,int> =</text>
|
||||
<text x="94" y="180" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626"> flavour => priceList.PriceFor(flavour);</text>
|
||||
|
||||
<text x="80" y="200" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">buyer = ByName(buyerName)</text>
|
||||
|
||||
<!-- loop -->
|
||||
<rect x="80" y="214" width="360" height="120" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="240" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">foreach (var flavour : buyer.Favourites)</text>
|
||||
<text x="94" y="270" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total += buyer.ScoopsOf(flavour) *</text>
|
||||
<text x="94" y="290" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8"> priceFor(flavour);</text>
|
||||
<text x="94" y="318" font-size="10" fill="#64748b">invocation stays exactly where the code was</text>
|
||||
|
||||
<!-- ghost -->
|
||||
<rect x="94" y="352" width="330" height="26" rx="6" fill="none" stroke="#9333ea" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="106" y="370" font-size="10" fill="#9333ea" font-style="italic">λ used to be defined right here</text>
|
||||
|
||||
<text x="80" y="398" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return total;</text>
|
||||
|
||||
<!-- slide arrow -->
|
||||
<line x1="260" y1="350" x2="260" y2="196" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
|
||||
<text x="270" y="205" font-size="12" font-weight="bold" fill="#9333ea">slide</text>
|
||||
|
||||
<!-- PriceList -->
|
||||
<rect x="580" y="130" width="150" height="90" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="160" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">PriceList</text>
|
||||
<text x="655" y="180" font-size="11" fill="#7f1d1d" text-anchor="middle">still called —</text>
|
||||
<text x="655" y="196" font-size="11" fill="#7f1d1d" text-anchor="middle">but only via the λ now</text>
|
||||
|
||||
<line x1="444" y1="142" x2="572" y2="162" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,54 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrGr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 2 — extract the loop; the lambda becomes a parameter</text>
|
||||
<text x="24" y="56" font-size="13" fill="#16a34a" font-weight="bold">The test passes its own pricing (_ => 2). PriceList is never touched: deterministic, glanceable.</text>
|
||||
|
||||
<!-- test -->
|
||||
<rect x="50" y="150" width="230" height="140" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="66" y="176" font-size="13" font-weight="bold" fill="#14532d">BuyersTests</text>
|
||||
<text x="66" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#334155">var anke = new Buyer(...)</text>
|
||||
<text x="66" y="228" font-size="12" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#16a34a">priceFor: _ => 2</text>
|
||||
<text x="66" y="252" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">ShoppingListTotal(anke, λ)</text>
|
||||
<text x="66" y="276" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">Assert.AreEqual(8, ...)</text>
|
||||
|
||||
<!-- middle: overload -->
|
||||
<rect x="330" y="150" width="200" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="344" y="176" font-size="11" font-weight="bold" fill="#0f172a">ShoppingListTotal(buyer, <tspan fill="#9333ea">λ</tspan>)</text>
|
||||
<text x="344" y="196" font-size="10" fill="#475569">no-arg overload passes</text>
|
||||
<text x="344" y="210" font-size="10" fill="#475569">priceList.PriceFor as method group</text>
|
||||
<text x="344" y="238" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total = 0</text>
|
||||
<text x="344" y="262" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">foreach flavour:</text>
|
||||
<text x="344" y="286" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">total += scoops *</text>
|
||||
<text x="344" y="306" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"><tspan fill="#9333ea" font-weight="bold">λ</tspan>(flavour)</text>
|
||||
|
||||
<!-- right: the extracted loop, pure -->
|
||||
<rect x="570" y="150" width="170" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="584" y="176" font-size="10" font-weight="bold" fill="#0f172a">the pricing loop</text>
|
||||
<text x="584" y="196" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">inputs: buyer, λ</text>
|
||||
<text x="584" y="220" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">sum of scoops *</text>
|
||||
<text x="584" y="238" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"><tspan fill="#9333ea" font-weight="bold">λ</tspan>(flavour)</text>
|
||||
<text x="584" y="270" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return total</text>
|
||||
<text x="584" y="304" font-size="10" fill="#16a34a" font-weight="bold">pure ✓</text>
|
||||
|
||||
<!-- green arrows -->
|
||||
<line x1="284" y1="225" x2="324" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
|
||||
<text x="286" y="214" font-size="10" fill="#16a34a">λ</text>
|
||||
<line x1="534" y1="225" x2="564" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
|
||||
|
||||
<!-- PriceList, never called -->
|
||||
<rect x="570" y="370" width="170" height="56" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.8"/>
|
||||
<text x="655" y="392" font-size="12" font-weight="bold" fill="#94a3b8" text-anchor="middle">PriceList</text>
|
||||
<text x="655" y="412" font-size="10" fill="#64748b" text-anchor="middle">never called by the test ✗</text>
|
||||
<line x1="655" y1="368" x2="655" y2="336" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<text x="665" y="356" font-size="11" font-weight="bold" fill="#dc2626">✗</text>
|
||||
|
||||
<text x="50" y="430" font-size="12" fill="#475569">2 flavours x 2 scoops x 2 = 8 — every run, not just a lucky roll.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrO" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Demo — printSalesForecasts: one loop, three unwanted dependencies</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">The clock is already out (prep). Count the rest: a static side-channel write, a random re-roll, a value read, and IO.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="380" height="250" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">printSalesForecasts()</text>
|
||||
|
||||
<!-- loop -->
|
||||
<rect x="80" y="112" width="340" height="196" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="136" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (String name : names)</text>
|
||||
<text x="94" y="158" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">if (name.equals("Steve"))</text>
|
||||
<text x="94" y="180" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626"> Scorer.flavour = Strawberry;</text>
|
||||
<text x="94" y="202" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">else</text>
|
||||
<text x="94" y="224" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626"> scorer.updateSelection();</text>
|
||||
<text x="94" y="246" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">score = scorer.getSalesForecast();</text>
|
||||
<text x="94" y="274" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#d97706">System.out.println(name + " score: " + score);</text>
|
||||
<text x="94" y="298" font-size="10" fill="#64748b">the weather/sales loop</text>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="580" y="110" width="150" height="90" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="140" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="160" font-size="11" fill="#7f1d1d" text-anchor="middle">Random + a static</text>
|
||||
<text x="655" y="176" font-size="11" fill="#7f1d1d" text-anchor="middle">field (flavour)</text>
|
||||
|
||||
<line x1="424" y1="180" x2="572" y2="140" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="424" y1="224" x2="572" y2="160" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="424" y1="246" x2="572" y2="182" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
|
||||
<!-- System.out -->
|
||||
<rect x="580" y="250" width="150" height="60" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="655" y="274" font-size="12" font-weight="bold" fill="#d97706" text-anchor="middle">System.out</text>
|
||||
<text x="655" y="292" font-size="10" fill="#92400e" text-anchor="middle">IO — next time's split</text>
|
||||
<line x1="424" y1="274" x2="572" y2="282" stroke="#d97706" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrO)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#475569"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 1 — an approval test: red & flaky</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">Capture System.out, Approvals.verify it. Every run disagrees — Scorer re-rolls with a Random.</text>
|
||||
|
||||
<!-- test -->
|
||||
<rect x="50" y="140" width="240" height="140" rx="10" fill="#ffffff" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3"/>
|
||||
<text x="66" y="166" font-size="13" font-weight="bold" fill="#334155">DailyForecastsTest</text>
|
||||
<text x="66" y="192" font-size="11" font-family="Menlo, Consolas, monospace" fill="#334155">capture System.out</text>
|
||||
<text x="66" y="212" font-size="11" font-family="Menlo, Consolas, monospace" fill="#334155">Approvals.verify(...)</text>
|
||||
<text x="66" y="248" font-size="12" font-weight="bold" fill="#dc2626">red & flaky ✗</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="380" y="100" width="330" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="396" y="124" font-size="13" font-weight="bold" fill="#0f172a">printSalesForecasts()</text>
|
||||
<rect x="396" y="138" width="298" height="126" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="410" y="162" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (String name : names)</text>
|
||||
<text x="410" y="186" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">Scorer.flavour = ... / updateSelection()</text>
|
||||
<text x="410" y="210" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#dc2626">score = scorer.getSalesForecast();</text>
|
||||
<text x="410" y="238" font-size="11" font-family="Menlo, Consolas, monospace" fill="#d97706">System.out.println(...)</text>
|
||||
|
||||
<line x1="294" y1="210" x2="372" y2="190" stroke="#475569" stroke-width="1.5" marker-end="url(#arrG)"/>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="560" y="330" width="170" height="70" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="645" y="358" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="645" y="378" font-size="10" fill="#7f1d1d" text-anchor="middle">Random → different every run</text>
|
||||
<line x1="645" y1="328" x2="645" y2="286" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 2 — extract the dependency-using code into a lambda</text>
|
||||
<text x="24" y="56" font-size="13" fill="#dc2626" font-weight="bold">ALL of it goes inside — the static write too. The lambda is the adapter; it is not supposed to be pure.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="380" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">printSalesForecasts()</text>
|
||||
|
||||
<!-- loop -->
|
||||
<rect x="80" y="184" width="340" height="152" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="204" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (String name : names)</text>
|
||||
|
||||
<!-- lambda -->
|
||||
<rect x="94" y="214" width="312" height="90" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="106" y="236" font-size="12" font-weight="bold" fill="#9333ea">λ nameToSalesScore</text>
|
||||
<text x="106" y="258" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">if (Steve) Scorer.flavour = Strawberry;</text>
|
||||
<text x="106" y="276" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">else scorer.updateSelection();</text>
|
||||
<text x="106" y="294" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.getSalesForecast();</text>
|
||||
|
||||
<circle cx="90" cy="326" r="4" fill="#9333ea"/>
|
||||
<text x="102" y="330" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8">println(name + " score: " + λ.apply(name));</text>
|
||||
|
||||
<text x="94" y="366" font-size="10" fill="#d97706" font-weight="bold">System.out.println stays in the loop — next time's split</text>
|
||||
<text x="94" y="414" font-size="10" fill="#64748b">names list lives above the loop, unchanged</text>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="580" y="200" width="150" height="100" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">static field + Random</text>
|
||||
<text x="655" y="268" font-size="11" fill="#7f1d1d" text-anchor="middle">both touches in the λ now</text>
|
||||
|
||||
<line x1="410" y1="250" x2="572" y2="234" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="410" y1="288" x2="572" y2="262" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,48 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 3 — slide the lambda definition to the top of the method</text>
|
||||
<text x="24" y="56" font-size="13" fill="#9333ea" font-weight="bold">Slide, don't move: the definition slides up, the invocation stays exactly where it was.</text>
|
||||
|
||||
<!-- method -->
|
||||
<rect x="60" y="74" width="380" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="76" y="98" font-size="14" font-weight="bold" fill="#0f172a">printSalesForecasts()</text>
|
||||
|
||||
<!-- lambda at top -->
|
||||
<rect x="94" y="104" width="312" height="90" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="106" y="126" font-size="12" font-weight="bold" fill="#9333ea">λ nameToSalesScore</text>
|
||||
<text x="106" y="148" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">if (Steve) Scorer.flavour = Strawberry;</text>
|
||||
<text x="106" y="166" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">else scorer.updateSelection();</text>
|
||||
<text x="106" y="184" font-size="11" font-family="Menlo, Consolas, monospace" fill="#dc2626">return scorer.getSalesForecast();</text>
|
||||
|
||||
<!-- loop -->
|
||||
<rect x="80" y="252" width="340" height="110" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="94" y="274" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (String name : names)</text>
|
||||
|
||||
<!-- ghost -->
|
||||
<rect x="94" y="286" width="312" height="30" rx="6" fill="none" stroke="#9333ea" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="106" y="306" font-size="10" fill="#9333ea" font-style="italic">λ used to be defined here</text>
|
||||
|
||||
<circle cx="90" cy="346" r="4" fill="#9333ea"/>
|
||||
<text x="102" y="350" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#6b21a8">println(name + " score: " + λ.apply(name));</text>
|
||||
|
||||
<!-- slide arrow -->
|
||||
<line x1="250" y1="284" x2="250" y2="200" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
|
||||
<text x="260" y="244" font-size="12" font-weight="bold" fill="#9333ea">slide</text>
|
||||
|
||||
<!-- Scorer -->
|
||||
<rect x="580" y="200" width="150" height="100" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="655" y="230" font-size="14" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="250" font-size="11" fill="#7f1d1d" text-anchor="middle">static field + Random</text>
|
||||
<text x="655" y="268" font-size="11" fill="#7f1d1d" text-anchor="middle">both touches in the λ now</text>
|
||||
|
||||
<line x1="410" y1="140" x2="572" y2="230" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<line x1="410" y1="180" x2="572" y2="264" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,69 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrO" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 4 — extract the loop; the lambda becomes a parameter</text>
|
||||
<text x="24" y="56" font-size="13" fill="#475569">collectSalesScores is static and scorer-free. The printing loop stays behind — one split at a time.</text>
|
||||
|
||||
<!-- left: composed method -->
|
||||
<rect x="50" y="74" width="300" height="356" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="66" y="96" font-size="14" font-weight="bold" fill="#0f172a">printSalesForecasts()</text>
|
||||
|
||||
<!-- lambda -->
|
||||
<rect x="64" y="104" width="272" height="68" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="78" y="126" font-size="12" font-weight="bold" fill="#9333ea">λ nameToSalesScore</text>
|
||||
<text x="78" y="146" font-size="10" font-family="Menlo, Consolas, monospace" fill="#dc2626">static write + updateSelection +</text>
|
||||
<text x="78" y="162" font-size="10" font-family="Menlo, Consolas, monospace" fill="#dc2626">getSalesForecast()</text>
|
||||
|
||||
<!-- call site -->
|
||||
<rect x="64" y="188" width="272" height="48" rx="8" fill="#ffffff" stroke="#475569" stroke-width="1.5"/>
|
||||
<text x="78" y="208" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">var scores =</text>
|
||||
<text x="78" y="226" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">collectSalesScores(names, <tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
|
||||
|
||||
<!-- print loop -->
|
||||
<rect x="64" y="252" width="272" height="72" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="78" y="276" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#d97706">for (entry : scores)</text>
|
||||
<text x="78" y="296" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#d97706">System.out.println(...)</text>
|
||||
<text x="78" y="316" font-size="10" fill="#d97706">IO stays — next time</text>
|
||||
|
||||
<!-- purple arrow -->
|
||||
<line x1="340" y1="212" x2="452" y2="146" stroke="#9333ea" stroke-width="1.5" marker-end="url(#arrP)"/>
|
||||
<text x="396" y="188" font-size="10" font-weight="bold" fill="#9333ea" text-anchor="middle" transform="rotate(-48 396 188)">λ becomes a parameter</text>
|
||||
|
||||
<!-- right: extracted method -->
|
||||
<rect x="440" y="74" width="290" height="280" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="456" y="96" font-size="13" font-weight="bold" fill="#0f172a">collectSalesScores</text>
|
||||
<text x="456" y="112" font-size="11" fill="#475569">static — no fields, all inputs are parameters</text>
|
||||
|
||||
<rect x="456" y="122" width="258" height="32" rx="16" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="585" y="142" font-size="11" font-weight="bold" fill="#9333ea" text-anchor="middle">λ Function<String, Integer> nameToSalesScore</text>
|
||||
|
||||
<rect x="456" y="166" width="258" height="110" rx="8" fill="#eef2ff" stroke="#818cf8"/>
|
||||
<text x="470" y="190" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for (String name : names)</text>
|
||||
<text x="470" y="214" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scores.put(name,</text>
|
||||
<text x="470" y="234" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> <tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(name))</text>
|
||||
<text x="470" y="260" font-size="10" fill="#64748b">LinkedHashMap — keeps the order</text>
|
||||
|
||||
<text x="456" y="308" font-size="11" font-family="Menlo, Consolas, monospace" fill="#0f172a">return scores;</text>
|
||||
|
||||
<!-- Scorer, out of reach -->
|
||||
<rect x="560" y="386" width="160" height="50" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4 3"/>
|
||||
<text x="640" y="408" font-size="12" font-weight="bold" fill="#dc2626" text-anchor="middle">Scorer</text>
|
||||
<text x="640" y="426" font-size="10" fill="#7f1d1d" text-anchor="middle">out of reach from in here</text>
|
||||
<line x1="640" y1="384" x2="640" y2="360" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<text x="650" y="376" font-size="10" font-weight="bold" fill="#dc2626">✗ cannot reach in</text>
|
||||
|
||||
<!-- System.out note -->
|
||||
<text x="64" y="360" font-size="10" font-weight="bold" fill="#d97706">grep scorer → no hits in here</text>
|
||||
<text x="64" y="378" font-size="10" font-weight="bold" fill="#d97706">…but System.out is still on this side</text>
|
||||
<line x1="200" y1="348" x2="330" y2="326" stroke="#d97706" stroke-width="1" stroke-dasharray="4 3" marker-end="url(#arrO)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,54 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrGr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">Step 5 — the overload, and the test goes green</text>
|
||||
<text x="24" y="56" font-size="13" fill="#16a34a" font-weight="bold">The test passes nameToSalesScore through the seam. Scorer is never touched: deterministic, glanceable.</text>
|
||||
|
||||
<!-- test -->
|
||||
<rect x="50" y="150" width="230" height="140" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="66" y="176" font-size="13" font-weight="bold" fill="#14532d">DailyForecastsTest</text>
|
||||
<text x="66" y="202" font-size="11" font-family="Menlo, Consolas, monospace" fill="#334155">capture System.out</text>
|
||||
<text x="66" y="224" font-size="11" font-family="Menlo, Consolas, monospace" font-weight="bold" fill="#16a34a">name -> Steve ? 9 : 11</text>
|
||||
<text x="66" y="248" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">printSalesForecasts(λ)</text>
|
||||
<text x="66" y="272" font-size="11" font-family="Menlo, Consolas, monospace" fill="#14532d">Approvals.verify(...)</text>
|
||||
|
||||
<!-- middle: overload -->
|
||||
<rect x="340" y="150" width="190" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="354" y="176" font-size="11" font-weight="bold" fill="#0f172a">printSalesForecasts(<tspan fill="#9333ea">λ</tspan>)</text>
|
||||
<text x="354" y="196" font-size="10" fill="#475569">no-arg overload bakes</text>
|
||||
<text x="354" y="210" font-size="10" fill="#475569">in the real λ</text>
|
||||
<text x="354" y="238" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">collectSalesScores</text>
|
||||
<text x="354" y="256" font-size="11" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">(names, <tspan fill="#9333ea" font-weight="bold">λ</tspan>)</text>
|
||||
<text x="354" y="284" font-size="11" font-family="Menlo, Consolas, monospace" fill="#d97706">print loop</text>
|
||||
<text x="354" y="304" font-size="11" font-family="Menlo, Consolas, monospace" fill="#d97706">(still in here)</text>
|
||||
|
||||
<!-- right: extracted method -->
|
||||
<rect x="570" y="150" width="170" height="180" rx="10" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="584" y="176" font-size="10" font-weight="bold" fill="#0f172a">collectSalesScores(λ)</text>
|
||||
<text x="584" y="202" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">for name : names</text>
|
||||
<text x="584" y="224" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">scores.put(name,</text>
|
||||
<text x="584" y="242" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b"> <tspan fill="#9333ea" font-weight="bold">λ</tspan>.apply(name))</text>
|
||||
<text x="584" y="270" font-size="10" font-family="Menlo, Consolas, monospace" fill="#1e1b4b">return scores</text>
|
||||
<text x="584" y="304" font-size="10" fill="#16a34a" font-weight="bold">pure ✓</text>
|
||||
|
||||
<!-- green arrows -->
|
||||
<line x1="284" y1="225" x2="334" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
|
||||
<text x="284" y="214" font-size="10" fill="#16a34a">λ</text>
|
||||
<line x1="534" y1="225" x2="564" y2="225" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrGr)"/>
|
||||
|
||||
<!-- scorer, never called -->
|
||||
<rect x="570" y="370" width="170" height="56" rx="8" fill="#f8fafc" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.8"/>
|
||||
<text x="655" y="392" font-size="12" font-weight="bold" fill="#94a3b8" text-anchor="middle">Scorer</text>
|
||||
<text x="655" y="412" font-size="10" fill="#64748b" text-anchor="middle">never called by the test ✗</text>
|
||||
<line x1="655" y1="368" x2="655" y2="336" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<text x="665" y="356" font-size="11" font-weight="bold" fill="#dc2626">✗</text>
|
||||
|
||||
<text x="50" y="430" font-size="12" fill="#475569">Approved: Steve score: 9 / Julie score: 11 / Francis score: 11 — every run, not just a lucky roll.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,41 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="Helvetica, Arial, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<marker id="arrP" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#9333ea"/>
|
||||
</marker>
|
||||
<marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#475569"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="760" height="470" fill="#ffffff"/>
|
||||
<text x="24" y="34" font-size="20" font-weight="bold" fill="#0f172a">How to read the diagrams</text>
|
||||
|
||||
<rect x="50" y="58" width="110" height="32" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
|
||||
<text x="180" y="79" font-size="13" fill="#334155">composed method / class under test</text>
|
||||
|
||||
<rect x="50" y="106" width="110" height="32" rx="8" fill="#eef2ff" stroke="#818cf8" stroke-width="1.5"/>
|
||||
<text x="180" y="127" font-size="13" fill="#334155">loop — the thing we split / extract</text>
|
||||
|
||||
<rect x="50" y="154" width="110" height="32" rx="8" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="180" y="175" font-size="13" fill="#334155">unwanted dependency — Scorer ≈ weather service</text>
|
||||
|
||||
<line x1="50" y1="216" x2="160" y2="216" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrR)"/>
|
||||
<text x="180" y="221" font-size="13" fill="#334155">dependency call — the thing that wants to disappear</text>
|
||||
|
||||
<rect x="50" y="250" width="110" height="32" rx="8" fill="#faf5ff" stroke="#9333ea" stroke-width="1.5"/>
|
||||
<text x="180" y="271" font-size="13" fill="#334155">λ — a function value</text>
|
||||
|
||||
<rect x="50" y="298" width="110" height="32" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="180" y="319" font-size="13" fill="#334155">pure / test</text>
|
||||
|
||||
<line x1="50" y1="360" x2="160" y2="360" stroke="#9333ea" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrP)"/>
|
||||
<text x="180" y="365" font-size="13" fill="#334155">slide — definition moves, invocation stays</text>
|
||||
|
||||
<line x1="50" y1="408" x2="160" y2="408" stroke="#475569" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrG)"/>
|
||||
<text x="180" y="413" font-size="13" fill="#334155">extraction / other motion</text>
|
||||
|
||||
<text x="24" y="452" font-size="12" fill="#64748b">Code lines inside boxes are monospace. Red = the unwanted dependency; purple = the lambda and everything it makes possible.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |