Статьи

FizzBuzz Kata с потоками Java

После пары недель занятий дзюдо моему сыну стало скучно. Он жаловался, что ничего не учил, потому что продолжал делать одно и то же снова и снова.

Это не просто маленькие дети, которые путают обучение и делать новые вещи. Например, сколько разработчиков программного обеспечения сталкиваются с проблемой преднамеренной практики , выполняя каты или посещая додзё ?

Может показаться глупым повторять упражнения, которые вы уже делали много раз, но это не так. Это единственный способ стать черным поясом в своей области. И помните, что мастерство является одним из трех внутренних мотиваторов (другие — это автономия и цель).

Практика означает замедление и перемещение внимания от результата к процессу. Лучше всего использовать простые упражнения, которые вы можете выполнить за ограниченное время, чтобы вы могли выполнять одно и то же упражнение несколько раз.

Я обнаружил, что практически всегда узнаю что-то новое, когда практикуюсь. Это не потому, что я забыл, как решить проблему с прошлого раза, а потому, что с тех пор я узнал что-то новое и, таким образом, вижу мир новыми глазами.

Например, после выхода Java 8 я пытался использовать новые потоковые классы, чтобы помочь перейти к более функциональному стилю программирования. Это изменило мой взгляд на старые проблемы, такие как FizzBuzz .

Давайте посмотрим это в действии. Конечно, я начинаю с добавления теста:

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 WhenFizzingAndBuzzing {
+
+   private final FizzBuzz fizzbuzz = new FizzBuzz();
+
+   @Test
+   public void shouldReplaceWithFizzAndBuzz() {
+     assertEquals(“1”, “1”, fizzbuzz.get(1));
+   }
+
+ }

В этом тесте используется форма модульного тестирования « Когда… Следует», которая помогает сосредоточиться на поведении, а не на деталях реализации. Я позволил Eclipse сгенерировать код, необходимый для компиляции:

01
02
03
04
05
06
07
08
09
10
+ package remonsinnema.blog.fizzbuzz;
+
+
+ public class FizzBuzz {
+
+   public String get(int i) {
+     return null;
+   }
+
+ }

Простейший код, который делает тестовый проход, состоит в том, чтобы подделать его :

1
2
3
4
5
6
7
package remonsinnema.blog.fizzbuzz;
  public class FizzBuzz {
    public String get(int i) {
–     return null;
+     return 1”;
    }
  }

Теперь, когда тест пройден, пришло время рефакторинга . Я удаляю дублирование из теста:

01
02
03
04
05
06
07
08
09
10
11
public class WhenFizzingAndBuzzing {
    @Test
    public void shouldReplaceWithFizzAndBuzz() {
–     assertEquals(“1”, “1”, fizzbuzz.get(1));
+     assertFizzBuzz(“1”, 1);
+   }
+
+   private void assertFizzBuzz(String expected, int n) {
+     assertEquals(Integer.toString(n), expected, fizzbuzz.get(n));
    }
  }

Далее я добавляю тест, чтобы вызвать реальную реализацию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class WhenFizzingAndBuzzing {
    @Test
    public void shouldReplaceWithFizzAndBuzz() {
      assertFizzBuzz(“1”, 1);
+     assertFizzBuzz(“2”, 2);
    }
    private void assertFizzBuzz(String expected, int n) {
  package remonsinnema.blog.fizzbuzz;
  public class FizzBuzz {
–   public String get(int i) {
–     return 1”;
+   public String get(int n) {
+     return Integer.toString(n);
    }
  }

Хорошо, теперь давайте попробуем проверить Fizz :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class WhenFizzingAndBuzzing {
public void shouldReplaceWithFizzAndBuzz() {
assertFizzBuzz(“1”, 1);
assertFizzBuzz(“2”, 2);
+ assertFizzBuzz(“Fizz”, 3);
}
private void assertFizzBuzz(String expected, int n) {
package remonsinnema.blog.fizzbuzz;
public class FizzBuzz {
public String get(int n) {
+ if (n == 3) {
+ return “Fizz”;
+ }
return Integer.toString(n);
}

Аналогично для Buzz :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class WhenFizzingAndBuzzing {
      assertFizzBuzz(“Fizz”, 3);
+     assertFizzBuzz(“4”, 4);
+     assertFizzBuzz(“Buzz”, 5);
    }
    private void assertFizzBuzz(String expected, int n) {
  public class FizzBuzz {
      if (n == 3) {
        return “Fizz”;
      }
+     if (n == 5) {
+       return “Buzz”;
+     }
      return Integer.toString(n);
    }

Здесь я просто скопировал и вставил оператор if чтобы он работал быстро . Мы не должны останавливаться на достигнутом, конечно, но избавиться от грязных вещей. В данном случае это дублирование.

Во-первых, давайте обновим код, чтобы сделать дублирование более очевидным:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package remonsinnema.blog.fizzbuzz;
  public class FizzBuzz {
    public String get(int n) {
–     if (n == 3) {
–       return “Fizz”;
+     MultipleReplacer replacer = new MultipleReplacer(3, “Fizz”);
+     if (n == replacer.getValue()) {
+       return replacer.getText();
      }
–     if (n == 5) {
–       return “Buzz”;
+     replacer = new MultipleReplacer(5, “Buzz”);
+     if (n == replacer.getValue()) {
+       return replacer.getText();
      }
      return Integer.toString(n);
    }
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
+ package remonsinnema.blog.fizzbuzz;
+
+
+ public class MultipleReplacer {
+
+   private final int value;
+   private final String text;
+
+   public MultipleReplacer(int value, String text) {
+     this.value = value;
+     this.text = text;
+   }
+
+   public int getValue() {
+     return value;
+   }
+
+   public String getText() {
+     return text;
+   }
+
+ }

Я только что создал новый объект значения для хранения двух значений, которые мне пришлось изменить после копирования / вставки.

Теперь, когда дублирование стало понятнее, его легко удалить:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package remonsinnema.blog.fizzbuzz;
+ import java.util.Arrays;
+ import java.util.Collection;
+
  public class FizzBuzz {
+   private final Collection replacers = Arrays.asList(
+       new MultipleReplacer(3, “Fizz”), new MultipleReplacer(5, “Buzz”));
+
    public String get(int n) {
–     MultipleReplacer replacer = new MultipleReplacer(3, “Fizz”);
–     if (n == replacer.getValue()) {
–       return replacer.getText();
–     }
–     replacer = new MultipleReplacer(5, “Buzz”);
–     if (n == replacer.getValue()) {
–       return replacer.getText();
+     for (MultipleReplacer replacer : replacers) {
+       if (n == replacer.getValue()) {
+         return replacer.getText();
+       }
      }
      return Integer.toString(n);
    }

Я не закончил уборку, однако. Текущий код страдает от зависти к функциям , которую я решаю, перемещая поведение в объект значения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package remonsinnema.blog.fizzbuzz;
  import java.util.Arrays;
  import java.util.Collection;
+ import java.util.Optional;
  public class FizzBuzz {
    public String get(int n) {
      for (MultipleReplacer replacer : replacers) {
–       if (n == replacer.getValue()) {
–         return replacer.getText();
+       Optional result = replacer.textFor(n);
+       if (result.isPresent()) {
+         return result.get();
        }
      }
      return Integer.toString(n);
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.Optional;
+
  public class MultipleReplacer {
      this.text = text;
    }
–   public int getValue() {
–     return value;
–   }
–   public String getText() {
–     return text;
+   public Optional<String> textFor(int n) {
+     if (n == value) {
+       return Optional.of(text);
+     }
+     return Optional.empty();
    }
  }

Теперь, когда я закончил рефакторинг, я могу продолжить с кратных значений:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class WhenFizzingAndBuzzing {
      assertFizzBuzz(“Fizz”, 3);
      assertFizzBuzz(“4”, 4);
      assertFizzBuzz(“Buzz”, 5);
+     assertFizzBuzz(“Fizz”, 6);
    }
    private void assertFizzBuzz(String expected, int n) {
  public class MultipleReplacer {
    }
    public Optional<String> textFor(int n) {
–     if (n == value) {
+     if (n % value == 0) {
        return Optional.of(text);
      }
      return Optional.empty();

Финальный тест для одновременного «Fizz» и «Buzz»:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class WhenFizzingAndBuzzing {
      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 n) {
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class FizzBuzz {
        public class FizzBuzz {
        new MultipleReplacer(3, “Fizz”), new MultipleReplacer(5, “Buzz”));
    public String get(int n) {
+     StringBuilder result = new StringBuilder();
      for (MultipleReplacer replacer : replacers) {
–       Optional<String> result = replacer.textFor(n);
–       if (result.isPresent()) {
–         return result.get();
+       Optional<String> replacement = replacer.textFor(n);
+       if (replacement.isPresent()) {
+         result.append(replacement.get());
        }
      }
+     if (result.length() > 0) {
+       return result.toString();
+     }
      return Integer.toString(n);
    }
    }

Этот код довольно сложный, но здесь на помощь приходят потоки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FizzBuzz {
        new MultipleReplacer(3, “Fizz”), new MultipleReplacer(5, “Buzz”));
    public String get(int n) {
–     StringBuilder result = new StringBuilder();
–     for (MultipleReplacer replacer : replacers) {
–       Optional<String> replacement = replacer.textFor(n);
–       if (replacement.isPresent()) {
–         result.append(replacement.get());
–       }
–     }
–     if (result.length() > 0) {
–       return result.toString();
–     }
–     return Integer.toString(n);
+     return replacers.stream()
+         .map(replacer -> replacer.textFor(n))
+         .filter(Optional::isPresent)
+         .map(optional -> optional.get())
+         .reduce((a, b) -> a + b)
+         .orElse(Integer.toString(n));
    }
  }

Обратите внимание, как исчезают операторы for и if . Вместо того, чтобы объяснять, как что-то нужно сделать, мы говорим, чего хотим достичь.

Мы можем применить тот же трюк, чтобы избавиться от оставшегося оператора if в нашей базе од:

01
02
03
04
05
06
07
08
09
10
11
public class MultipleReplacer {
    }
    public Optional<String> textFor(int n) {
–     if (n % value == 0) {
–       return Optional.of(text);
–     }
–     return Optional.empty();
+     return Optional.of(text)
+         .filter(ignored -> n % value == 0);
    }
  }

Код есть на GitHub .

Ссылка: FizzBuzz Kata с потоками Java от нашего партнера по JCG Ремона Синнема в блоге по разработке безопасного программного обеспечения .