This is an experiment along the lines of the Anti-If Campaign using the FizzBuzz Kata as a test base. The FizzBuzz problem statement is as follows:
Write a program that prints the numbers in order from 1 to 100, with the exception that when a number is a multiple of 3, it prints “Fizz”. When a number is a multiple of 5, it prints “Buzz”. And when a number is multiple of both, it prints “FizzBuzz”. In all other cases, it just prints the decimal representation of the number.
First the test for the Fizz portion.
public class When_asked_to_GetFizzBuzz { private List<int> _inputs; private List<Pair<int, string>> _result; [SetUp] public void BeforeEachTest() { _inputs = new List<int>(); } [Test] public void Given_numbers_divisible_by_3_that_are_indivisible_by_5_should_get__Fizz() { Test.Verify( with_numbers, that_are_divisible_by_3, that_are_not_divisible_by_5, when_asked_to_get_FizzBuzz, should_get_same_number_of_outputs_as_inputs, should_get__Fizz__for_each ); } private void with_numbers() { _inputs.AddRange(Enumerable.Range(1, 100)); } private void that_are_divisible_by_3() { _inputs.RemoveAll(x => x % 3 != 0); } private void that_are_not_divisible_by_5() { _inputs.RemoveAll(x => x % 5 == 0); } private void when_asked_to_get_FizzBuzz() { _result = _inputs.GetFizzBuzz().ToList(); } private void should_get_same_number_of_outputs_as_inputs() { _result.Count.ShouldBeEqualTo(_inputs.Count); } private void should_get__Fizz__for_each() { _result.All(x => x.Second == "Fizz").ShouldBeTrue(); }
and its implementation
public static class IntExtensions { public static IEnumerable<Pair<int, string>> GetFizzBuzz(this IEnumerable<int> values) { return values .Select(x => new Pair<int, string>(x, "") .When(NumberIsDivisibleBy3, SetResultToFizz)); } private static Func<Pair<int, string>, bool> NumberIsDivisibleBy3 { get { return y => y.First % 3 == 0; } } private static Action<Pair<int, string>> SetResultToFizz { get { return y => y.Second = "Fizz"; } } }
Now the DSL
public static class TExtensions { public static T When<T>(this T item, Func<T, bool> condition, Action<T> doIfTrue) { if (condition(item)) { doIfTrue(item); } return item; } }
Pretty interesting. Now the Buzz test:
[Test] public void Given_numbers_indivisible_by_3_that_are_divisible_by_5_should_get__Buzz() { Test.Verify( with_numbers, that_are_not_divisible_by_3, that_are_divisible_by_5, when_asked_to_get_FizzBuzz, should_get_same_number_of_outputs_as_inputs, should_get__Buzz__for_each ); } private void that_are_not_divisible_by_3() { _inputs.RemoveAll(x => x % 3 == 0); } private void that_are_divisible_by_5() { _inputs.RemoveAll(x => x % 5 != 0); } private void should_get__Buzz__for_each() { _result.All(x => x.Second == "Buzz").ShouldBeTrue(); }
and implementation change
public static class IntExtensions { public static IEnumerable<Pair<int, string>> GetFizzBuzz(this IEnumerable<int> values) { return values .Select(x => new Pair<int, string>(x, "") .When(NumberIsDivisibleBy3, SetResultToFizz) .When(NumberIsDivisibleBy5, SetResultToBuzz)); } private static Func<Pair<int, string>, bool> NumberIsDivisibleBy5 { get { return y => y.First % 5 == 0; } } private static Action<Pair<int, string>> SetResultToBuzz { get { return y => y.Second += "Buzz"; } }
Next a test for the FizzBuzz part:
[Test] public void Given_numbers_divisible_by_3_and_5_should_get__FizzBuzz() { Test.Verify( with_numbers, that_are_divisible_by_3, that_are_divisible_by_5, when_asked_to_get_FizzBuzz, should_get_same_number_of_outputs_as_inputs, should_get__FizzBuzz__for_each ); } private void should_get__FizzBuzz__for_each() { _result.All(x => x.Second == "FizzBuzz").ShouldBeTrue(); }
and implementation change
public static class IntExtensions { public static IEnumerable<Pair<int, string>> GetFizzBuzz(this IEnumerable<int> values) { return values .Select(x => new Pair<int, string>(x, "") .When(NumberIsDivisibleBy3, SetResultToFizz) .When(NumberIsDivisibleBy5, AppendBuzzToResult)); } private static Action<Pair<int, string>> AppendBuzzToResult { get { return y => y.Second += "Buzz"; } }
Finally, a test for getting the number when it doesn’t match any of the filters:
[Test] public void Given_numbers_indivisible_by_3_and_5_should_get_the_number() { Test.Verify( with_numbers, that_are_not_divisible_by_3, that_are_not_divisible_by_5, when_asked_to_get_FizzBuzz, should_get_same_number_of_outputs_as_inputs, should_get_the_number_for_each ); } private void should_get_the_number_for_each() { _result.All(x => x.Second == x.First.ToString()).ShouldBeTrue(); }
implementation change
public static class IntExtensions { public static IEnumerable<Pair<int, string>> GetFizzBuzz(this IEnumerable<int> values) { return values .Select(x => new Pair<int, string>(x, "") .When(NumberIsDivisibleBy3, SetResultToFizz) .When(NumberIsDivisibleBy5, AppendBuzzToResult) .Unless(NumberIsDivisibleBy3Or5, SetResultToTheNumber)); } private static Func<Pair<int, string>, bool> NumberIsDivisibleBy3Or5 { get { return y => NumberIsDivisibleBy3(y) || NumberIsDivisibleBy5(y); } } private static Action<Pair<int, string>> SetResultToTheNumber { get { return y => y.Second = y.First.ToString(); } }
and DSL addition
public static class TExtensions { public static T Unless<T>(this T item, Func<T, bool> condition, Action<T> doIfNotTrue) { if (!condition(item)) { doIfNotTrue(item); } return item; }
This is an interesting start but rather anemic at the moment. Desired:
* optionally provide multiple actions for a particular match
* optionally handle exceptions for each one
* optionally break out of the DSL chain, or
* skip to a previous/future named match instead of falling through
e.g.
x.When(IsDivisibleBy3) .Do(DoubleTheNumber) .On<ArgumentOutOfRangeException>() .LogIt(...) .SkipToEnd() .Do(Add3ToTheNumber) .When(IsDivisibleBy5) ...
The code for this post is available on github.