Некоторое время назад я решил ката 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 Ремона Синнема в блоге по разработке безопасного облака . |