Add split-loop-with-lambda learning hour: draft, pre-factored exercise, solution
- Samman learning hour: 5/5/10/25/5 outline; pairs stop at extracted method taking the lambda as a parameter - exercise/java: pre-factored variant of the kata (Q1/Q4 dead branch stripped, dates as constant), approval test red/flaky by design - exercise/solution: collectExpectedWeather(LongFunction<Boolean>) + no-arg overload; deterministic approval test (all-sunny lambda) - rehearse-with-agent.md: rehearsal setup with qwen + java-refactoring-mcp
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
target/
|
||||
*.received.txt
|
||||
*.approved.txt
|
||||
.idea/
|
||||
*.iml
|
||||
@@ -0,0 +1,37 @@
|
||||
# IceCreamForecasting — Split Loop with Lambda (exercise)
|
||||
|
||||
Pre-factored variant of [IceCreamForecasting-Refactoring-Kata](https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata):
|
||||
the Q1/Q4 dead branch is stripped and the hardcoded dates are a constant now.
|
||||
Everything else — including the noise in the scoring loop — is untouched.
|
||||
|
||||
## Task
|
||||
|
||||
`longRangeForecast()` is hard to test: it calls `scorer`, and `Scorer`
|
||||
stands for an external weather service you must not call from a unit test.
|
||||
The rest of the method's inputs are only `DATES` and `ANCHOR_DATE`.
|
||||
|
||||
Get it under test:
|
||||
|
||||
1. First get any test running that calls `longRangeForecast()` —
|
||||
it will fail or be flaky, because of the dependency on `Scorer`.
|
||||
2. Then use the **Split Loop with Lambda** mechanics (6 steps, on the handout)
|
||||
so that the weather collection becomes an extracted method that takes the
|
||||
weather-reading as a **lambda parameter**.
|
||||
3. Stop when you have an extracted method that takes the lambda as a parameter,
|
||||
and a test that passes **without** touching `Scorer`.
|
||||
|
||||
## Constraint
|
||||
|
||||
Don't change `Scorer` and don't change the *behaviour* of
|
||||
`longRangeForecast()`. Compile early, compile often.
|
||||
|
||||
## Debrief questions
|
||||
|
||||
- At which step did plain **Split Loop** stop working, and why?
|
||||
(Hint: what type did your introduced variable end up being?)
|
||||
- There are **two** calls to `scorer` in the weather loop.
|
||||
What did you do with each one?
|
||||
- Could you delete either call without changing the forecast?
|
||||
Try it. Why did that work?
|
||||
- Where would you take this next — more extraction, or moving the method
|
||||
to another class where `Scorer` is unknown?
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>codingdojo</groupId>
|
||||
<artifactId>icecream-split-loop-lambda-exercise</artifactId>
|
||||
<version>1.0.0</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.approvaltests</groupId>
|
||||
<artifactId>approvaltests</artifactId>
|
||||
<version>18.6.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
package codingdojo;
|
||||
|
||||
public enum IceCream {
|
||||
Strawberry,
|
||||
Chocolate,
|
||||
Vanilla;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package codingdojo;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Pre-factored variant of LongRangeForecasts from
|
||||
* https://github.com/emilybache/IceCreamForecasting-Refactoring-Kata
|
||||
*
|
||||
* Differences from the kata original (deliberate, for the learning hour):
|
||||
* - The Q1/Q4 branch was removed. It was dead code anyway: the unconditional
|
||||
* loop right after it overwrites every value it sets. (Splitting loops
|
||||
* sometimes reveals things like this.)
|
||||
* - The hardcoded list of interesting dates is now a constant, DATES, and the
|
||||
* anchor instant is ANCHOR_DATE. (Turning the dates into a parameter is a
|
||||
* *later* refactoring — not part of this exercise.)
|
||||
*
|
||||
* Your task: see README.md
|
||||
*/
|
||||
public class LongRangeForecasts {
|
||||
|
||||
private static final Instant ANCHOR_DATE = Instant.parse("2023-04-26T14:00:00.000-07:00");
|
||||
private static final List<Instant> DATES = List.of(
|
||||
Instant.parse("2023-05-01T14:00:00.000-07:00"),
|
||||
Instant.parse("2023-05-18T14:00:00.000-07:00"),
|
||||
Instant.parse("2023-06-05T14:00:00.000-07:00"),
|
||||
Instant.parse("2023-06-23T14:00:00.000-07:00")
|
||||
);
|
||||
|
||||
private final Scorer scorer = new Scorer();
|
||||
|
||||
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();
|
||||
long daysForward = ChronoUnit.DAYS.between(ANCHOR_DATE, interestingDate);
|
||||
Boolean weather = scorer.lookupWeather(daysForward);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package codingdojo;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Trimmed version of the kata's Scorer: only the parts LongRangeForecasts uses.
|
||||
* lookupWeather stands in for an API call to a weather service — in the real
|
||||
* system this is the "unwanted dependency".
|
||||
*/
|
||||
public class Scorer {
|
||||
|
||||
public static IceCream flavour = null;
|
||||
|
||||
public int getScore() {
|
||||
boolean sunnyToday = lookupWeather();
|
||||
if (flavour == null) {
|
||||
return -1;
|
||||
}
|
||||
switch (flavour) {
|
||||
case Strawberry:
|
||||
return sunnyToday ? 10 : 5;
|
||||
case Chocolate:
|
||||
return 6;
|
||||
case Vanilla:
|
||||
return sunnyToday ? 7 : 5;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean lookupWeather() {
|
||||
return new Random().nextBoolean();
|
||||
}
|
||||
|
||||
public boolean lookupWeather(long daysForward) {
|
||||
// placeholder implementation - real version would make API call to weather service
|
||||
return new Random().nextBoolean();
|
||||
}
|
||||
|
||||
public void updateSelection() {
|
||||
// placeholder implementation - real version would use machine learning to predict sales
|
||||
var score = getScore();
|
||||
if (score > 5) {
|
||||
var randomizer = new Random();
|
||||
IceCream[] allFlavours = IceCream.values();
|
||||
flavour = allFlavours[randomizer.nextInt(allFlavours.length)];
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package codingdojo;
|
||||
|
||||
import org.approvaltests.Approvals;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public class LongRangeForecastsTest {
|
||||
|
||||
@Test
|
||||
void longRangeForecast() {
|
||||
var actual = new LongRangeForecasts().longRangeForecast();
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user