はじめに
Java8で導入されたラムダ式、Stream APIを利用した、デザインパターンの新しい実装手法を検討していきます。
前回の記事では、ラムダ式を使ったBuilder
パターンの実装方法について考察しました。
今回のテーマ
今回は、FizzBuzz問題
を題材に、Template Method
パターンについて考えてみます。
従来の実装方法
共通的な振る舞い(ロジック)を再利用するために、抽象クラスにテンプレートなるメソッドをfinal
で定義し、可変的な部分をフックメソッドとしてabstract
定義しておきます。具象クラスにてフックメソッドをインプリすることで、共通的な振る舞いは残しつつ、個別の振る舞いが表現可能となります。
(テンプレートメソッドをfinal
定義することで、サブクラスで共通的な振る舞いを上書きすることを禁止します)
抽象クラスは以下のようになります。
count(int,int)
がテンプレートメソッドです。
public abstract class AbstractCounter {
public final void count(final int from, final int to) {
for (int i = from; i <= to; i++) {
final String expression;
if (condition1(i)) {
expression = condition2(i) ? onBoth(i) : onCondition1(i);
} else {
expression = condition2(i) ? onCondition2(i) : String.valueOf(i);
}
System.out.print(expression + ",");
}
}
abstract boolean condition1(int num);
abstract boolean condition2(int num);
abstract String onCondition1(int num);
abstract String onCondition2(int num);
abstract String onBoth(int num);
}
FizzBuzzの実装です。5つのabstract
メソッドをオーバーライドします。
public class FizzBuzzCounter extends AbstractCounter {
@Override
boolean condition1(int num) {
return num % 3 == 0;
}
@Override
boolean condition2(int num) {
return num % 5 == 0;
}
@Override
String onCondition1(int num) {
return "Fizz";
}
@Override
String onCondition2(int num) {
return "Buzz";
}
@Override
String onBoth(int num) {
return "FizzBuzz";
}
}
もう一つの実装、ナベアツカウンターはアホに言うかわりに、!
を付与することとします。(3が付く3の倍数の場合は、!!
)
public class NabeatsuCounter extends AbstractCounter {
@Override
boolean condition1(int num) {
return num % 3 == 0;
}
@Override
boolean condition2(int num) {
return String.valueOf(num).contains("3");
}
@Override
String onCondition1(int num) {
return String.valueOf(num) + "!";
}
@Override
String onCondition2(int num) {
return String.valueOf(num) + "!";
}
@Override
String onBoth(int num) {
return String.valueOf(num) + "!!";
}
}
使用例:
System.out.println("FizzBuzz : ");
FizzBuzzCounter counter = new FizzBuzzCounter();
//1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,Buzz,
counter.count(1, 20);
System.out.println();
System.out.println("Nabeatsu : ");
NabeatsuCounter counter2 = new NabeatsuCounter();
//21!,22,23!,24!,25,26,27!,28,29,30!!,31!,32!,33!!,34!,35!,36!!,37!,38!,39!!,40,
counter2.count(21, 40);
ラムダ式を使った実装
可変処理は、親の抽象クラスのフックメソッドをオーバーライドするのではなく、関数として渡す形とします。
なお、利用側のコードの可読性のため、前回のBuilder
パターンを適用していますが、シンプルに書くならテンプレートメソッドの引数として渡す形となります。
また、サンプルコードのため、Builder
の前提条件が満たされているかのチェック等は省いています。
public class Counter {
private Predicate<Integer> condition1;
private Predicate<Integer> condition2;
private Function<Integer, String> converter1;
private Function<Integer, String> converter2;
private Function<Integer, String> converterBoth;
private int from;
private int to;
private Counter() {
}
public Counter first(Predicate<Integer> condition1, Function<Integer, String> converter1) {
this.condition1 = condition1;
this.converter1 = converter1;
return this;
}
public Counter second(Predicate<Integer> condition2, Function<Integer, String> converter2) {
this.condition2 = condition2;
this.converter2 = converter2;
return this;
}
public Counter onBoth(Function<Integer, String> converterBoth) {
this.converterBoth = converterBoth;
return this;
}
public Counter fromTo(int from, int to) {
this.from = from;
this.to = to;
return this;
}
public static void count(final Consumer<Counter> consumer) {
Counter counter = new Counter();
consumer.accept(counter);
counter.doCount();
}
private void doCount() {
String result = IntStream.rangeClosed(from, to)
.mapToObj(this::map)
.collect(Collectors.joining(","));
System.out.println(result);
}
private String map(int num) {
if (condition1.test(num)) {
return condition2.test(num) ? converterBoth.apply(num) : converter1.apply(num);
} else {
return condition2.test(num) ? converter2.apply(num) : String.valueOf(num);
}
}
}
実際のFizzBuzz、ナベアツの組み立てと実行は以下のようになります。
//1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,Buzz
Counter.count(c -> c.first(num -> num % 3 == 0, num -> "Fizz")
.second(num -> num % 5 == 0, num -> "Buzz")
.onBoth(num -> "FizzBuzz")
.fromTo(1, 20));
//21!,22,23!,24!,25,26,27!,28,29,30!!,31!,32!,33!!,34!,35!,36!!,37!,38!,39!!,40
Counter.count(c -> c.first(num -> num % 3 == 0, num -> String.valueOf(num) + "!")
.second(num -> String.valueOf(num).contains("3"), num -> String.valueOf(num) + "!")
.onBoth(num -> String.valueOf(num) + "!!")
.fromTo(21, 40));
考察
今回の手法のメリットが何かというと、ロジックを利用するために特定のクラスを継承する必要がなくなることです。実装の再利用を目的とした継承は避けるべき、というオブジェクト指向の考え方もあります。
もちろん、従来の実装方法の方が見通しがよかったり、管理しやすい場合も多いと思いますので、ケースバイケースで設計できればよいのかなと思います。
Tips
フックメソッドを関数としてテンプレートメソッドの引数に渡す場合、数が多いとコードの見通しが悪くなってしまいます。その場合、今回の例のようにBuilder
パターンを適用するなど工夫が必要です。
また、各々の引数の型がPredicate<T>
とかFunction<T,R>
ばかり並んでいると、フックメソッドの責務がわかりにくくなりがちなので、敢えて個別に関数型インタフェースとして定義することで意図を明確化することも検討の余地があります。