Help us understand the problem. What is going on with this article?

Java8 ラムダ式&Streamによるデザインパターン再考 - Template Methodパターン -

More than 3 years have passed since last update.

はじめに

Java8で導入されたラムダ式、Stream APIを利用した、デザインパターンの新しい実装手法を検討していきます。
前回の記事では、ラムダ式を使ったBuilderパターンの実装方法について考察しました。

今回のテーマ

今回は、FizzBuzz問題を題材に、Template Methodパターンについて考えてみます。

従来の実装方法

共通的な振る舞い(ロジック)を再利用するために、抽象クラスにテンプレートなるメソッドをfinalで定義し、可変的な部分をフックメソッドとしてabstract定義しておきます。具象クラスにてフックメソッドをインプリすることで、共通的な振る舞いは残しつつ、個別の振る舞いが表現可能となります。
(テンプレートメソッドをfinal定義することで、サブクラスで共通的な振る舞いを上書きすることを禁止します)

抽象クラスは以下のようになります。
count(int,int)がテンプレートメソッドです。

AbstractCounter.java
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メソッドをオーバーライドします。

FizzBuzzCounter.java
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の倍数の場合は、!!)

NabeatsuCounter.java
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) + "!!";
    }

}

使用例:

Usage
        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の前提条件が満たされているかのチェック等は省いています。

Counter.java
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、ナベアツの組み立てと実行は以下のようになります。

Usage
        //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>ばかり並んでいると、フックメソッドの責務がわかりにくくなりがちなので、敢えて個別に関数型インタフェースとして定義することで意図を明確化することも検討の余地があります。

yonetty
某SIerでアーキテクトとしてエンタープライズ向けシステム・製品の開発に携わっています。 Twitter: @tyonekubo
https://blog.ynkb.xyz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした