Некоторое время назад я решил ката FizzBuzz, используя потоки Java 8 и лямбды . Хотя конечный результат был функциональным, промежуточные этапы — нет. Конечно, я могу сделать лучше.
Как всегда, давайте начнем с неудачного теста:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
package remonsinnema.blog.fizzbuzz; + + import static org.junit.Assert.assertEquals; + + import org.junit.Test; + + + public class WhenFunctionallyFuzzingAndBuzzing { + + private final FizzBuzzer fizzBuzzer = new FizzBuzzer(); + + @Test + public void shouldReplaceMultiplesOfThreeWithFizzAndMultiplesOfFiveWithBuzz() { + assertEquals(“ 1 ”, “ 1 ”, fizzBuzzer.apply( 1 )); + } + + } |
01
02
03
04
05
06
07
08
09
10
11
12
13
|
package remonsinnema.blog.fizzbuzz; + + import java.util.function.Function; + + + public class FizzBuzzer implements Function<Integer, String> { + + @Override + public String apply(Integer n) { + return null ; + } + + } |
Обратите внимание, что я сразу начинаю функциональный курс, используя Function
Java.
Я подделываю реализацию, чтобы пройти тест:
1
2
3
4
5
6
7
|
public class FizzBuzzer implements Function<Integer, String> { @Override public String apply(Integer n) { – return null ; + return “ 1 ”; } } |
И рефакторинг теста для устранения дублирования:
01
02
03
04
05
06
07
08
09
10
11
|
public class WhenFunctionallyFuzzingAndBuzzing { @Test public void shouldReplaceMultiplesOfThreeWithFizzAndMultiplesOfFiveWithBuzz() { – assertEquals(“ 1 ”, “ 1 ”, fizzBuzzer.apply( 1 )); + assertFizzBuzz(“ 1 ”, 1 ); + } + + private void assertFizzBuzz(String expected, int value) { + assertEquals(Integer.toString(value), expected, fizzBuzzer.apply(value)); } } |
Затем я добавляю еще один тест для обобщения реализации:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class WhenFunctionallyFuzzingAndBuzzing { @Test public void shouldReplaceMultiplesOfThreeWithFizzAndMultiplesOfFiveWithBuzz() { assertFizzBuzz(“ 1 ”, 1 ); + assertFizzBuzz(“ 2 ”, 2 ); } private void assertFizzBuzz(String expected, int value) { public class FizzBuzzer implements Function<Integer, String> { @Override public String apply(Integer n) { – return “ 1 ”; + return Integer.toString(n); } } |
ОК, пока довольно стандартные вещи. Далее мне нужно заменить 3 на «Fizz»:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
public class WhenFunctionallyFuzzingAndBuzzing { public void shouldReplaceMultiplesOfThreeWithFizzAndMultiplesOfFiveWithBuzz() { assertFizzBuzz(“ 1 ”, 1 ); assertFizzBuzz(“ 2 ”, 2 ); + assertFizzBuzz(“Fizz”, 3 ); } nbsp; private void assertFizzBuzz(String expected, int value) { public class FizzBuzzer implements Function<Integer, String> { @Override public String apply(Integer n) { – return Integer.toString(n); + return numberReplacerFor(n).apply(n); + } + + private Function<Integer, String> numberReplacerFor(Integer n) { + return n == 3 + ? i -> “Fizz” + : i -> Integer.toString(i); } } |
Здесь я признаю, что мне нужно применить одну из двух функций, в зависимости от ввода. Этот код работает, но нуждается в некоторой очистке. Во-первых, как ступеньку, я извлекаю лямбды в поля:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
import java.util.function.Function; public class FizzBuzzer implements Function<Integer, String> { + private final Function<Integer, String> replaceNumberWithStringRepresentation + = n -> Integer.toString(n); + private final Function<Integer, String> replaceNumberWithFizz + = n -> “Fizz”; + @Override public String apply(Integer n) { return numberReplacerFor(n).apply(n); private Function<Integer, String> numberReplacerFor(Integer n) { return n == 3 – ? i -> “Fizz” – : i -> Integer.toString(i); + ? replaceNumberWithFizz + : replaceNumberWithStringRepresentation; } } |
Далее я подчеркиваю, что «3» и «Fizz» идут вместе, извлекая класс:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public class FizzBuzzer implements Function<Integer, String> { private final Function<Integer, String> replaceNumberWithStringRepresentation = n -> Integer.toString(n); – private final Function<Integer, String> replaceNumberWithFizz – = n -> “Fizz”; + private final Fizzer replaceNumberWithFizz = new Fizzer(); @Override public String apply(Integer n) { } private Function<Integer, String> numberReplacerFor(Integer n) { – return n == 3 + return replaceNumberWithFizz.test(n) ? replaceNumberWithFizz : replaceNumberWithStringRepresentation; } + package remonsinnema.blog.fizzbuzz; + + import java.util.function.Function; + import java.util.function.Predicate; + + + public class Fizzer implements Function<Integer, String>, Predicate<Integer> { + + @Override + public boolean test(Integer n) { + return n == 3 ; + } + + @Override + public String apply(Integer n) { + return “Fizz”; + } + + } |
Здесь я использую стандартный функциональный интерфейс Java Predicate
.
Чтобы добавить «Buzz», мне нужно обобщить код из одного if
(скрытого как троичный оператор) в цикл:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public class WhenFunctionallyFuzzingAndBuzzing { assertFizzBuzz(“ 1 ”, 1 ); assertFizzBuzz(“ 2 ”, 2 ); assertFizzBuzz(“Fizz”, 3 ); + assertFizzBuzz(“ 4 ”, 4 ); + assertFizzBuzz(“Buzz”, 5 ); } private void assertFizzBuzz(String expected, int value) { package remonsinnema.blog.fizzbuzz; + import java.util.Arrays; + import java.util.Collection; import java.util.function.Function; private final Function<Integer, String> replaceNumberWithStringRepresentation = n -> Integer.toString(n); – private final Fizzer replaceNumberWithFizz = new Fizzer(); + private final Collection<ReplaceNumberWithFixedText> replacers = Arrays.asList( + new ReplaceNumberWithFixedText( 3 , “Fizz”), + new ReplaceNumberWithFixedText( 5 , “Buzz”) + ); @Override public String apply(Integer n) { } private Function<Integer, String> numberReplacerFor(Integer n) { – return replaceNumberWithFizz.test(n) – ? replaceNumberWithFizz – : replaceNumberWithStringRepresentation; + for (ReplaceNumberWithFixedText replacer : replacers) { + if (replacer.test(n)) { + return replacer; + } + } + return replaceNumberWithStringRepresentation; } } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
package remonsinnema.blog.fizzbuzz; – – import java.util.function.Function; – import java.util.function.Predicate; – – – public class Fizzer implements Function<Integer, String>, Predicate<Integer> { – – @Override – public boolean test(Integer n) { – return n == 3 ; – } – – @Override – public String apply(Integer n) { – return “Fizz”; – } – – } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package remonsinnema.blog.fizzbuzz; + + import java.util.function.Function; + import java.util.function.Predicate; + + + public class ReplaceNumberWithFixedText implements Function<Integer, String>, + Predicate<Integer> { + + private final int target; + private final String replacement; + + public ReplaceNumberWithFixedText( int target, String replacement) { + this .target = target; + this .replacement = replacement; + } + + @Override + public boolean test(Integer n) { + return n == target; + } + + @Override + public String apply(Integer n) { + return replacement; + } + + } |
Ой, старые привычки … Это должен быть поток, а не цикл:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import java.util.function.Function; public class FizzBuzzer implements Function<Integer, String> { – private final Function<Integer, String> replaceNumberWithStringRepresentation + private final Function<Integer, String> defaultReplacer = n -> Integer.toString(n); private final Collection<ReplaceNumberWithFixedText> replacers = Arrays.asList( new ReplaceNumberWithFixedText( 3 , “Fizz”), } private Function<Integer, String> numberReplacerFor(Integer n) { – for (ReplaceNumberWithFixedText replacer : replacers) { – if (replacer.test(n)) { – return replacer; – } – } – return replaceNumberWithStringRepresentation; + return replacers.stream() + .filter(replacer -> replacer.test(n)) + .map(replacer -> (Function<Integer, String>) replacer) + .findFirst() + .orElse(defaultReplacer); } } |
Намного лучше. Следующий тест для кратных:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class WhenFunctionallyFuzzingAndBuzzing { assertFizzBuzz(“Fizz”, 3 ); assertFizzBuzz(“ 4 ”, 4 ); assertFizzBuzz(“Buzz”, 5 ); + assertFizzBuzz(“Fizz”, 6 ); } private void assertFizzBuzz(String expected, int value) { public class FizzBuzzer implements Function<Integer, String> { private final Function<Integer, String> defaultReplacer = n -> Integer.toString(n); – private final Collection<ReplaceNumberWithFixedText> replacers = Arrays.asList( – new ReplaceNumberWithFixedText( 3 , “Fizz”), – new ReplaceNumberWithFixedText( 5 , “Buzz”) + private final Collection<ReplaceMultipleWithFixedText> replacers = Arrays.asList( + new ReplaceMultipleWithFixedText( 3 , “Fizz”), + new ReplaceMultipleWithFixedText( 5 , “Buzz”) ); @Override |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
+ package remonsinnema.blog.fizzbuzz; + + import java.util.function.Function; + import java.util.function.Predicate; + + + public class ReplaceNumberWithFixedText implements Function<Integer, String>, + Predicate<Integer> { + + private final int target; + private final String replacement; + + public ReplaceNumberWithFixedText( int target, String replacement) { + this .target = target; + this .replacement = replacement; + } + + @Override + public boolean test(Integer n) { + return n % target == 0 ; + } + + @Override + public String apply(Integer n) { + return replacement; + } + + } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
– package remonsinnema.blog.fizzbuzz; – – import java.util.function.Function; – import java.util.function.Predicate; – – – public class ReplaceNumberWithFixedText implements Function<Integer, String>, Predicate<Integer> { – – private final int target; – private final String replacement; – – public ReplaceNumberWithFixedText( int target, String replacement) { – this .target = target; – this .replacement = replacement; – } – – @Override – public boolean test(Integer n) { – return n == target; – } – – @Override – public String apply(Integer n) { – return replacement; – } – – } |
Последний тест должен объединить Fizz и Buzz:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public class WhenFunctionallyFuzzingAndBuzzing { assertFizzBuzz(“ 4 ”, 4 ); assertFizzBuzz(“Buzz”, 5 ); assertFizzBuzz(“Fizz”, 6 ); + assertFizzBuzz(“ 7 ”, 7 ); + assertFizzBuzz(“ 8 ”, 8 ); + assertFizzBuzz(“Fizz”, 9 ); + assertFizzBuzz(“Buzz”, 10 ); + assertFizzBuzz(“ 11 ”, 11 ); + assertFizzBuzz(“Fizz”, 12 ); + assertFizzBuzz(“ 13 ”, 13 ); + assertFizzBuzz(“ 14 ”, 14 ); + assertFizzBuzz(“FizzBuzz”, 15 ); } private void assertFizzBuzz(String expected, int value) { package remonsinnema.blog.fizzbuzz; import java.util.Arrays; import java.util.Collection; import java.util.function.Function; + import java.util.stream.Collectors; + import java.util.stream.Stream; public class FizzBuzzer implements Function<Integer, String> { @Override public String apply(Integer n) { – return numberReplacerFor(n).apply(n); + return numberReplacersFor(n) + .map(function -> function.apply(n)) + .collect(Collectors.joining()); } – private Function<Integer, String> numberReplacerFor(Integer n) { – return replacers.stream() + private Stream<Function<Integer, String>> numberReplacersFor(Integer n) { + return Stream.of(replacers.stream() .filter(replacer -> replacer.test(n)) .map(replacer -> (Function<Integer, String>) replacer) .findFirst() – .orElse(defaultReplacer); + .orElse(defaultReplacer)); } } |
Я обобщил одну Function
в Stream
Function
s, к которым я применяю шаблон Map-Reduce. Я мог бы .reduce("", (a, b) -> a + b)
часть Reduce, используя что-то вроде .reduce("", (a, b) -> a + b)
, но я думаю, что Collectors.joining()
более выразителен.
Это еще не прошло тест, так как я возвращаю поток одной функции. Исправление немного сложнее, потому что мне нужно знать, были ли найдены какие-либо применимые функции-заменители, и вы не можете сделать это без прерывания потока . Поэтому мне нужно создать новый поток с помощью StreamSupport
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package remonsinnema.blog.fizzbuzz; import java.util.Arrays; import java.util.Collection; + import java.util.Iterator; + import java.util.Spliterators; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; + import java.util.stream.StreamSupport; public class FizzBuzzer implements Function<Integer, String> { } private Stream<Function<Integer, String>> numberReplacersFor(Integer n) { – return Stream.of(replacers.stream() + Iterator<Function<Integer, String>> result = replacers.stream() .filter(replacer -> replacer.test(n)) .map(replacer -> (Function<Integer, String>) replacer) – .findFirst() – .orElse(defaultReplacer)); + .iterator(); + return result.hasNext() + ? StreamSupport.stream(Spliterators.spliteratorUnknownSize(result, 0 ), false ) + : Stream.of(defaultReplacer); } } |
Вот и все. Полный код на GitHub .
Из этого небольшого упражнения я выучил два урока:
- Java поставляется с целым набором функциональных интерфейсов, таких как
Function
иPredicate
, которые легко объединяются с потоками для решения различных задач. - Стандартное преобразование
if → while
становитсяif → stream
в функциональном мире.
Ссылка: | Функциональный FizzBuzz Kata на Java от нашего партнера по JCG Ремона Синнема в блоге по разработке безопасного облака . |