Compare commits

...
6 Commits
Author SHA1 Message Date
Your Name 125bb7ad01 README: point to deck at localhost:9001 with refactoring/ path 2026-09-29 14:37:06 +00:00
Your Name d14614f78a Fix storyboard deck reference: new refactoring/ path, served at localhost:9001 2026-09-29 14:37:01 +00:00
Your Name 578162851b Add solution (after) files for the three C# demos: seam overloads, extracted tails, pure xUnit stub tests 2026-09-29 13:32:47 +00:00
Your Name f9c3c68e79 Add README for C# Slice-with-Lambda demo examples (ramp, table, mechanics links) 2026-09-29 13:32:47 +00:00
Your Name f9bb7b982a Rename pattern to Slice with Lambda; add no-loop minimal shape and capture-to-parameter lesson 2026-09-29 13:32:47 +00:00
Your Name c153aba61e Add three C# demo examples for Slice with Lambda
- MetaphorFree.cs: guard -> SomeDependency.mightThrowWhenPreconditionNotSatisfied(a) -> tail,
  capture-to-parameter story for the deck
- WeatherStation.cs: SprinklerController, LightSensor.Read() byte -> SunExposure, tail branches
- IceCreamShop.cs: third repetition, Thermometer byte -> Warmth, tail branches
2026-09-29 13:15:45 +00:00
10 changed files with 563 additions and 4 deletions
@@ -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;
}
}
@@ -1,6 +1,6 @@
# Split Loop with Lambda — storyboard / handout # Slice with Lambda — storyboard / handout
Step-by-step mechanics of the **Split Loop with Lambda** refactoring, one diagram per 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 `←` 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. comments). Written to be sliced into slides later; until then it works as a handout.
@@ -8,6 +8,7 @@ Sources: `../zeroth-draft-of-refactoring-pattern.md` (the mechanics),
`../split-loop-with-lambda-learning-hour.md` (where we stop: after step 3 + the test), `../split-loop-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!" `../exercise/rehearsal-findings.md` (the drift anti-pattern and the "both calls!"
caution). Java only. Pristine start: `../exercise/java`, end state: `../exercise/solution`. 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 ## How to read the diagrams
@@ -4,7 +4,7 @@
# Introduction # Introduction
This is a zeroth draft. Our meta-goals are being more explicit in naming the refactorings and patterns we used to ourselves, and enable others to name the patterns, so they can reproduce the mechanics reliably on their own. This is a zeroth draft of the **Slice with Lambda** refactoring pattern (formerly called "Split Loop with Lambda"). Our meta-goals are being more explicit in naming the refactorings and patterns we used to ourselves, and enable others to name the patterns, so they can reproduce the mechanics reliably on their own.
Initial literature survey (see related patterns below) has not yet revealed an existing Refactoring Pattern description. In order to fit some or all of this refactoring in a learning hour, it is worth writing down what we mean, with examples (so, in case the resulting exercise does not fit in a learning hour, we have a blog post to refer to for more details and/or recapturing the mechanics). Initial literature survey (see related patterns below) has not yet revealed an existing Refactoring Pattern description. In order to fit some or all of this refactoring in a learning hour, it is worth writing down what we mean, with examples (so, in case the resulting exercise does not fit in a learning hour, we have a blog post to refer to for more details and/or recapturing the mechanics).
@@ -24,7 +24,39 @@ Split loop suggests “‘introduce variable’ refactorings and ‘slide statem
3. Extract the method below the slides statement. You now have a new method, that takes the lambda as a parameter 3. Extract the method below the slides statement. You now have a new method, that takes the lambda as a parameter
4. Move the method to another class where the unwanted dependency is not present 4. Move the method to another class where the unwanted dependency is not present
5. Move and/or create tests, using the lambda as a stub instead of a concrete invocation of the unwanted dependency. 5. Move and/or create tests, using the lambda as a stub instead of a concrete invocation of the unwanted dependency.
6. Check that “everything that could possibly break” is tested \- the extracted lambda allows more combinations to be easily tested. 6. Check that "everything that could possibly break" is tested \- the extracted lambda allows more combinations to be easily tested.
## The no-loop minimal shape
The mechanics above are usually applied inside a loop, but the pattern does not require one. The minimal shape is a method consisting of guard clauses, then ONE call on an unwanted dependency in the middle, then a tail that depends on the value the call produced:
```csharp
public int Calculate(int a)
{
if (a < 0) return 0;
int b = _someDependency.mightThrowWhenPreconditionNotSatisfied(a);
int c = a + b;
return c * 2;
}
```
The same mechanics apply: extract the call into a lambda, slide the lambda definition to the top, extract the method below the slide statement with the lambda as a parameter. There is simply no loop to split — hence "Slice".
### Capture-to-parameter: the lambda cannot capture the method's parameters
When the lambda is moved up into the function head (as a parameter via the overload seam), it cannot capture the method's parameters — they are not in scope there — so the lambda must take them explicitly:
```csharp
Func<int, int> readB = a => _someDependency.mightThrowWhenPreconditionNotSatisfied(a);
```
and every invocation becomes
```csharp
int b = readB(a);
```
Note that the Java storyboard lambda already took `daysForward` as its parameter, so the Java walkthrough does not hit this; the C# metaphor-free demo (`exercise/csharp/MetaphorFree.cs`) does.
# Consequences / resulting context. # Consequences / resulting context.