Rename split-loop-with-lambda to slice-with-lambda
The refactoring is now published as Slice on sammancoaching.org: https://sammancoaching.org/refactorings/slice.html - git mv ideas/split-loop-with-lambda -> ideas/slice-with-lambda - rename learning-hour file and update frontmatter name - update paths in tools/rehearse scripts and report title - update prose: 'Split Loop with Lambda' -> 'Slice with Lambda' in learning hour, exercise READMEs, storyboard, solution comments - link the published Slice refactoring from the pattern draft
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Example 3 — Ice cream shop demo of Slice with Lambda (C#)
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
//
|
||||
// Third repetition, same complexity as ScheduleWatering in WeatherStation.cs
|
||||
// (guard -> one dependency call -> map to enum -> constant tail), so
|
||||
// participants can practice extracting a lambda a second time. The C#
|
||||
// delegate syntax is worth the repetition:
|
||||
//
|
||||
// Func<byte> readTemperature = () => _thermometer.Read();
|
||||
//
|
||||
// Cast:
|
||||
// Thermometer — hardware-ish dependency. Read() returns a raw
|
||||
// byte value (not calibrated, the client decides
|
||||
// what it means).
|
||||
// Warmth — enum. Reading > 25 => Warm, otherwise Cold.
|
||||
// (Sunny/Shady's cousin from WeatherStation.cs.)
|
||||
// ThawAmount — enum. None / Medium / Low.
|
||||
// IceCreamShop — the client. Owns the _thawedToday flag, so the
|
||||
// guard is about client state, not sensor state.
|
||||
|
||||
using System;
|
||||
|
||||
public enum Warmth
|
||||
{
|
||||
Cold,
|
||||
Warm
|
||||
}
|
||||
|
||||
public enum ThawAmount
|
||||
{
|
||||
None,
|
||||
Medium,
|
||||
Low
|
||||
}
|
||||
|
||||
public class Thermometer
|
||||
{
|
||||
private readonly Random _rng = new Random(); // stand-in for the hardware
|
||||
|
||||
public byte Read()
|
||||
{
|
||||
return (byte)_rng.Next(50); // raw sensor value
|
||||
}
|
||||
}
|
||||
|
||||
public class IceCreamShop
|
||||
{
|
||||
private readonly Thermometer _thermometer;
|
||||
private bool _thawedToday; // event-style client state
|
||||
|
||||
public IceCreamShop(Thermometer thermometer)
|
||||
{
|
||||
_thermometer = thermometer;
|
||||
}
|
||||
|
||||
public ThawAmount ThawStock()
|
||||
{
|
||||
if (_thawedToday) // guard: client state, stays
|
||||
{
|
||||
return ThawAmount.None;
|
||||
}
|
||||
|
||||
_thawedToday = true; // set only after thawing
|
||||
byte temperature = _thermometer.Read(); // middle: unwanted dependency
|
||||
Warmth warmth = ToWarmth(temperature);
|
||||
if (warmth == Warmth.Warm) // tail: complex behaviour below
|
||||
{ // the extraction point
|
||||
return ThawAmount.Medium;
|
||||
}
|
||||
return ThawAmount.Low;
|
||||
}
|
||||
|
||||
private static Warmth ToWarmth(byte temperature)
|
||||
{
|
||||
return temperature > 25 ? Warmth.Warm : Warmth.Cold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Example 1 — Metaphor-free demo of Slice with Lambda (C#)
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
//
|
||||
// Shape of the method (per zeroth-draft-of-refactoring-pattern.md):
|
||||
//
|
||||
// guard clauses — must run first
|
||||
// middle: dependency — one call on an object we don't want in the
|
||||
// extracted method
|
||||
// tail: depends on b — uses the value the dependency produced
|
||||
//
|
||||
// Deliberately NOT a loop: this is the smallest method that shows the
|
||||
// mechanics. The dependency both validates its precondition (using the
|
||||
// parameter a) and produces b — one expression, one thing to move into
|
||||
// the lambda.
|
||||
//
|
||||
// Unwanted dependency: SomeDependency — might throw when its precondition
|
||||
// on a is not satisfied, and returns an unpredictable value. An approval
|
||||
// test on Calculate is flaky until the lambda is stubbed.
|
||||
|
||||
using System;
|
||||
|
||||
public class Calculator
|
||||
{
|
||||
private readonly SomeDependency _someDependency;
|
||||
|
||||
public Calculator(SomeDependency someDependency)
|
||||
{
|
||||
_someDependency = someDependency;
|
||||
}
|
||||
|
||||
public int Calculate(int a)
|
||||
{
|
||||
if (a < 0) return 0; // guard clause: stays
|
||||
int b = _someDependency.mightThrowWhenPreconditionNotSatisfied(a); // middle: unwanted dependency
|
||||
int c = a + b; // tail: depends on b
|
||||
return c * 2;
|
||||
}
|
||||
}
|
||||
|
||||
public class SomeDependency
|
||||
{
|
||||
private readonly Random _rng = new Random(); // stand-in for unpredictable computation
|
||||
|
||||
public int mightThrowWhenPreconditionNotSatisfied(int a)
|
||||
{
|
||||
if (a > 99)
|
||||
{
|
||||
throw new InvalidOperationException("precondition not satisfied");
|
||||
}
|
||||
return _rng.Next(100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Slice with Lambda — C# demo examples
|
||||
|
||||
Three C# demo examples for the **Slice with Lambda** refactoring.
|
||||
Condensed mechanics: https://www.sammancoaching.org/refactorings/slice.html
|
||||
(where that page says mock, we use a lambda instead).
|
||||
Full Java walkthrough: `../../storyboard/storyboard.md`.
|
||||
Slides: `~/qwan/slides-rjs/decks/refactoring/slice-with-lambda/slides.md`, served at http://localhost:9001/#/refactoring/slice-with-lambda.
|
||||
|
||||
## The three examples
|
||||
|
||||
| file | guard (client state) | unwanted dependency | lambda | tail |
|
||||
|---|---|---|---|---|
|
||||
| `MetaphorFree.cs` | `if (a < 0) return 0;` | `_someDependency.mightThrowWhenPreconditionNotSatisfied(a)` | `Func<int, int> readB = a => ...` | `int c = a + b; return c * 2;` |
|
||||
| `WeatherStation.cs` | `if (_wateredToday)` | `_lightSensor.Read()` (byte) | `Func<byte> readLight = () => ...` | sunny -> `WateringDuration.Medium`, shady -> `Short` (reading > 128 => `SunExposure.Sunny`) |
|
||||
| `IceCreamShop.cs` | `if (_thawedToday)` | `_thermometer.Read()` (byte) | `Func<byte> readTemperature = () => ...` | warm -> `ThawAmount.Medium`, cold -> `Low` (temperature > 25 => `Warmth.Warm`) |
|
||||
|
||||
In all three, the guard reads client state (a parameter or a flag the client
|
||||
owns), so it stays in the extracted method. Only the dependency call moves
|
||||
into the lambda.
|
||||
|
||||
## The ramp
|
||||
|
||||
`MetaphorFree.cs` is the smallest possible shape: no loop, one dependency
|
||||
call, a tail of one arithmetic step. It carries the capture-to-parameter
|
||||
lesson: the lambda takes `a` as its parameter (`readB(a)`) because a lambda
|
||||
moved up into the function head cannot capture method parameters. Its
|
||||
step-4 stub tests include a throwing stub.
|
||||
|
||||
`WeatherStation.cs` and `IceCreamShop.cs` are same-shape domain
|
||||
repetitions — guard, one dependency call, map to enum, constant tail — so
|
||||
participants can practice extracting the lambda twice. The C# delegate
|
||||
syntax (`Func<byte>`) is worth the repetition.
|
||||
|
||||
## Same three regions every time
|
||||
|
||||
Each example deliberately has the same three regions:
|
||||
|
||||
- **before**: guard clauses
|
||||
- **middle**: one dependency call
|
||||
- **after**: tail that depends on the value the dependency produced
|
||||
|
||||
Matching the pattern: extract the dependency-using code into a lambda,
|
||||
slide the definition to the top, extract the method below it, pass the
|
||||
lambda as parameter, test with pure lambda stubs.
|
||||
@@ -0,0 +1,71 @@
|
||||
// Example 2 — Weather Station domain demo of Slice with Lambda (C#)
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
//
|
||||
// Cast (Head First Design Patterns Weather Station, reduced):
|
||||
// LightSensor — hardware-ish dependency. Read() returns a byte.
|
||||
// SunExposure — enum. Reading > 128 => Sunny, otherwise Shady.
|
||||
// WateringDuration — enum. None / Medium / Short.
|
||||
// (Long is YAGNI until the for-loop version of this
|
||||
// exercise exists.)
|
||||
// SprinklerController — the client. Owns the _wateredToday flag, so the
|
||||
// guard is about client state, not sensor state.
|
||||
|
||||
using System;
|
||||
|
||||
public enum SunExposure
|
||||
{
|
||||
Shady,
|
||||
Sunny
|
||||
}
|
||||
|
||||
public enum WateringDuration
|
||||
{
|
||||
None,
|
||||
Medium,
|
||||
Short
|
||||
}
|
||||
|
||||
public class LightSensor
|
||||
{
|
||||
private readonly Random _rng = new Random(); // stand-in for hardware noise
|
||||
|
||||
public byte Read()
|
||||
{
|
||||
return (byte)_rng.Next(256);
|
||||
}
|
||||
}
|
||||
|
||||
public class SprinklerController
|
||||
{
|
||||
private readonly LightSensor _lightSensor;
|
||||
private bool _wateredToday; // event-style client state
|
||||
|
||||
public SprinklerController(LightSensor lightSensor)
|
||||
{
|
||||
_lightSensor = lightSensor;
|
||||
}
|
||||
|
||||
public WateringDuration ScheduleWatering()
|
||||
{
|
||||
if (_wateredToday) // guard: client state, stays
|
||||
{
|
||||
return WateringDuration.None;
|
||||
}
|
||||
|
||||
_wateredToday = true; // set only after watering
|
||||
byte reading = _lightSensor.Read(); // middle: unwanted dependency
|
||||
SunExposure exposure = ToSunExposure(reading);
|
||||
if (exposure == SunExposure.Sunny) // tail: complex behaviour below
|
||||
{ // the extraction point
|
||||
return WateringDuration.Medium;
|
||||
}
|
||||
return WateringDuration.Short;
|
||||
}
|
||||
|
||||
private static SunExposure ToSunExposure(byte reading)
|
||||
{
|
||||
return reading > 128 ? SunExposure.Sunny : SunExposure.Shady;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Example 3 solution — Ice cream shop demo of Slice with Lambda (C#), after
|
||||
//
|
||||
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||
// below the guard (step 3). The constructor now takes the lambda directly,
|
||||
// so tests construct with new IceCreamShop(() => 30).
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
|
||||
using System;
|
||||
|
||||
public enum Warmth
|
||||
{
|
||||
Cold,
|
||||
Warm
|
||||
}
|
||||
|
||||
public enum ThawAmount
|
||||
{
|
||||
None,
|
||||
Medium,
|
||||
Low
|
||||
}
|
||||
|
||||
public class Thermometer
|
||||
{
|
||||
private readonly Random _rng = new Random(); // stand-in for the hardware
|
||||
|
||||
public byte Read()
|
||||
{
|
||||
return (byte)_rng.Next(50); // raw sensor value
|
||||
}
|
||||
}
|
||||
|
||||
public class IceCreamShop
|
||||
{
|
||||
private readonly Func<byte> _readTemperature; // the thermometer, as a lambda
|
||||
private bool _thawedToday; // event-style client state
|
||||
|
||||
public IceCreamShop(Func<byte> readTemperature)
|
||||
{
|
||||
_readTemperature = readTemperature;
|
||||
}
|
||||
|
||||
public ThawAmount ThawStock()
|
||||
{
|
||||
Func<byte> readTemperature = () => _readTemperature(); // lambda at the top
|
||||
return ThawStockTail(readTemperature); // pass the lambda on
|
||||
}
|
||||
|
||||
private ThawAmount ThawStockTail(Func<byte> readTemperature)
|
||||
{
|
||||
if (_thawedToday) // guard: client state, stays
|
||||
{
|
||||
return ThawAmount.None;
|
||||
}
|
||||
|
||||
_thawedToday = true; // set only after thawing
|
||||
byte temperature = readTemperature(); // middle: the stubbed lambda
|
||||
Warmth warmth = ToWarmth(temperature);
|
||||
if (warmth == Warmth.Warm) // tail: complex behaviour below
|
||||
{ // the extraction point
|
||||
return ThawAmount.Medium;
|
||||
}
|
||||
return ThawAmount.Low;
|
||||
}
|
||||
|
||||
private static Warmth ToWarmth(byte temperature)
|
||||
{
|
||||
return temperature > 25 ? Warmth.Warm : Warmth.Cold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Example 1 solution — Metaphor-free demo of Slice with Lambda (C#), after
|
||||
//
|
||||
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||
// below the guard (step 3), overload seam added (step 3b), b inlined
|
||||
// (final cleanup).
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
|
||||
using System;
|
||||
|
||||
public class Calculator
|
||||
{
|
||||
private readonly SomeDependency _someDependency;
|
||||
|
||||
public Calculator(SomeDependency someDependency)
|
||||
{
|
||||
_someDependency = someDependency;
|
||||
}
|
||||
|
||||
public int Calculate(int a) // overload: callers unchanged
|
||||
{
|
||||
return Calculate(a, a =>
|
||||
_someDependency.mightThrowWhenPreconditionNotSatisfied(a));
|
||||
} // the real λ, baked into the head
|
||||
|
||||
public int Calculate(int a, Func<int, int> readB) // the seam: λ is a parameter
|
||||
{
|
||||
if (a < 0) return 0; // guard clause: stays
|
||||
return CalculateTail(a, readB);
|
||||
}
|
||||
|
||||
private int CalculateTail(int a, Func<int, int> readB)
|
||||
{
|
||||
int c = a + readB(a); // b inlined
|
||||
return c * 2;
|
||||
}
|
||||
}
|
||||
|
||||
public class SomeDependency
|
||||
{
|
||||
private readonly Random _rng = new Random(); // stand-in for unpredictable computation
|
||||
|
||||
public int mightThrowWhenPreconditionNotSatisfied(int a)
|
||||
{
|
||||
if (a > 99)
|
||||
{
|
||||
throw new InvalidOperationException("precondition not satisfied");
|
||||
}
|
||||
return _rng.Next(100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Tests for the three Slice-with-Lambda solutions — step 4: the lambda as a stub.
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
public class SliceWithLambdaTests
|
||||
{
|
||||
// --- Example 1: metaphor-free (Calculator) ---
|
||||
|
||||
[Fact]
|
||||
public void Guards_against_negative_input()
|
||||
{
|
||||
var calc = new Calculator(new SomeDependency()); // the real, flaky one...
|
||||
Assert.Equal(0, calc.Calculate(-1, a => 42)); // ...never asked: stub not invoked
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Throws_when_precondition_not_satisfied()
|
||||
{
|
||||
var calc = new Calculator(new SomeDependency());
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => calc.Calculate(7, a => throw new InvalidOperationException()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combines_input_and_the_stubbed_value()
|
||||
{
|
||||
var calc = new Calculator(new SomeDependency());
|
||||
Assert.Equal(84, calc.Calculate(0, a => 42)); // stub through the seam
|
||||
}
|
||||
|
||||
// --- Example 2: Weather Station (SprinklerController) ---
|
||||
|
||||
[Fact]
|
||||
public void Skips_watering_when_already_watered()
|
||||
{
|
||||
var controller = new SprinklerController(() => 200); // pure lambda: no sensor
|
||||
controller.ScheduleWatering(); // water once...
|
||||
Assert.Equal(WateringDuration.None, controller.ScheduleWatering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Waters_longer_in_full_sun()
|
||||
{
|
||||
var controller = new SprinklerController(() => 200); // sunny: > 128
|
||||
Assert.Equal(WateringDuration.Medium, controller.ScheduleWatering());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Waters_less_in_the_shade()
|
||||
{
|
||||
var controller = new SprinklerController(() => 10); // shady: ≤ 128
|
||||
Assert.Equal(WateringDuration.Short, controller.ScheduleWatering());
|
||||
}
|
||||
|
||||
// --- Example 3: Ice Cream Shop (IceCreamShop) ---
|
||||
|
||||
[Fact]
|
||||
public void Skips_thawing_when_already_thawed()
|
||||
{
|
||||
var shop = new IceCreamShop(() => 30); // pure lambda: no freezer
|
||||
shop.ThawStock(); // thaw once...
|
||||
Assert.Equal(ThawAmount.None, shop.ThawStock());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Thaws_more_when_warm()
|
||||
{
|
||||
var shop = new IceCreamShop(() => 30); // warm: > 25
|
||||
Assert.Equal(ThawAmount.Medium, shop.ThawStock());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Thaws_less_when_cold()
|
||||
{
|
||||
var shop = new IceCreamShop(() => 5); // cold: ≤ 25
|
||||
Assert.Equal(ThawAmount.Low, shop.ThawStock());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Example 2 solution — Weather Station domain demo of Slice with Lambda (C#), after
|
||||
//
|
||||
// Lambda extracted (step 1), slid to the top (step 2), method extracted
|
||||
// below the guard (step 3). The constructor now takes the lambda directly,
|
||||
// so tests construct with new SprinklerController(() => 200).
|
||||
//
|
||||
// Condensed mechanics: https://sammancoaching.org/refactorings/slice.html
|
||||
// (where that page says mock, we use a lambda instead).
|
||||
|
||||
using System;
|
||||
|
||||
public enum SunExposure
|
||||
{
|
||||
Shady,
|
||||
Sunny
|
||||
}
|
||||
|
||||
public enum WateringDuration
|
||||
{
|
||||
None,
|
||||
Medium,
|
||||
Short
|
||||
}
|
||||
|
||||
public class LightSensor
|
||||
{
|
||||
private readonly Random _rng = new Random(); // stand-in for hardware noise
|
||||
|
||||
public byte Read()
|
||||
{
|
||||
return (byte)_rng.Next(256);
|
||||
}
|
||||
}
|
||||
|
||||
public class SprinklerController
|
||||
{
|
||||
private readonly Func<byte> _readLight; // the sensor, as a lambda
|
||||
private bool _wateredToday; // event-style client state
|
||||
|
||||
public SprinklerController(Func<byte> readLight)
|
||||
{
|
||||
_readLight = readLight;
|
||||
}
|
||||
|
||||
public WateringDuration ScheduleWatering()
|
||||
{
|
||||
Func<byte> readLight = () => _readLight(); // lambda definition at the top
|
||||
return ScheduleWateringTail(readLight); // pass the lambda on
|
||||
}
|
||||
|
||||
private WateringDuration ScheduleWateringTail(Func<byte> readLight)
|
||||
{
|
||||
if (_wateredToday) // guard: client state, stays
|
||||
{
|
||||
return WateringDuration.None;
|
||||
}
|
||||
|
||||
_wateredToday = true; // set only after watering
|
||||
byte reading = readLight(); // middle: the stubbed lambda
|
||||
SunExposure exposure = ToSunExposure(reading);
|
||||
if (exposure == SunExposure.Sunny) // tail: complex behaviour below
|
||||
{ // the extraction point
|
||||
return WateringDuration.Medium;
|
||||
}
|
||||
return WateringDuration.Short;
|
||||
}
|
||||
|
||||
private static SunExposure ToSunExposure(byte reading)
|
||||
{
|
||||
return reading > 128 ? SunExposure.Sunny : SunExposure.Shady;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user