Java 8におけるストラテジーデザインパターン

Java 8の戦略設計パターン

1. 前書き

この記事では、Java8で戦略設計パターンを実装する方法を見ていきます。

最初に、パターンの概要を示し、古いバージョンのJavaで従来どのように実装されていたかを説明します。

次に、パターンをもう一度試してみますが、今回はJava 8ラムダを使用して、コードの冗長性を減らします。

2. 戦略パターン

基本的に、戦略パターンにより、実行時にアルゴリズムの動作を変更できます。

通常、アルゴリズムを適用するために使用されるインターフェイスで開始し、可能なアルゴリズムごとにそれを複数回実装します。

クリスマス、イースター、正月のいずれであるかに基づいて、購入にさまざまな種類の割引を適用する必要があるとします。 まず、各戦略で実装されるDiscounterインターフェースを作成しましょう。

public interface Discounter {
    BigDecimal applyDiscount(BigDecimal amount);
}

次に、イースターで50%の割引を適用し、クリスマスで10%の割引を適用するとします。 これらの戦略ごとにインターフェースを実装しましょう。

public static class EasterDiscounter implements Discounter {
    @Override
    public BigDecimal applyDiscount(final BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.5));
    }
}

public static class ChristmasDiscounter implements Discounter {
   @Override
   public BigDecimal applyDiscount(final BigDecimal amount) {
       return amount.multiply(BigDecimal.valueOf(0.9));
   }
}

最後に、テストで戦略を試してみましょう。

Discounter easterDiscounter = new EasterDiscounter();

BigDecimal discountedValue = easterDiscounter
  .applyDiscount(BigDecimal.valueOf(100));

assertThat(discountedValue)
  .isEqualByComparingTo(BigDecimal.valueOf(50));

これは非常にうまく機能しますが、問題は各戦略の具体的なクラスを作成しなければならないことは少し苦痛になることです。 別の方法は匿名の内部型を使用することですが、それでもかなり冗長で、以前のソリューションよりもそれほど便利ではありません。

Discounter easterDiscounter = new Discounter() {
    @Override
    public BigDecimal applyDiscount(final BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.5));
    }
};

3. Java 8の活用

Java 8がリリースされて以来、ラムダの導入により、匿名の内部型は多かれ少なかれ冗長になりました。 これは、ラインでの戦略の作成が、はるかにクリーンで簡単になることを意味します。

さらに、関数型プログラミングの宣言型スタイルにより、以前は不可能だったパターンを実装できます。

3.1. コードの冗長性の削減

今回だけラムダ式を使用してインラインEasterDiscounter,を作成してみましょう。

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5));

ご覧のとおり、コードは以前よりもはるかにクリーンで保守しやすくなり、以前と同じことを1行で実現しています。 基本的に、a lambda can be seen as a replacement for an anonymous inner typeです。

この利点は、さらに多くのDiscountersを行で宣言する場合にさらに明らかになります。

List discounters = newArrayList(
  amount -> amount.multiply(BigDecimal.valueOf(0.9)),
  amount -> amount.multiply(BigDecimal.valueOf(0.8)),
  amount -> amount.multiply(BigDecimal.valueOf(0.5))
);

たくさんのDiscounters,を定義したいときは、それらをすべて1か所で静的に宣言できます。 Java 8では、必要に応じて、インターフェイスで静的メソッドを定義することもできます。

したがって、具象クラスまたは匿名内部型のどちらかを選択する代わりに、すべて1つのクラスでラムダを作成してみましょう。

public interface Discounter {
    BigDecimal applyDiscount(BigDecimal amount);

    static Discounter christmasDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.9));
    }

    static Discounter newYearDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.8));
    }

    static Discounter easterDiscounter() {
        return amount -> amount.multiply(BigDecimal.valueOf(0.5));
    }
}

ご覧のとおり、それほど多くないコードで多くのことを達成しています。

3.2. 機能構成の活用

Discounterインターフェースを変更して、UnaryOperatorインターフェースを拡張してから、combine()メソッドを追加しましょう。

public interface Discounter extends UnaryOperator {
    default Discounter combine(Discounter after) {
        return value -> after.apply(this.apply(value));
    }
}

基本的に、Discounterをリファクタリングし、割引の適用はBigDecimalインスタンスを別のBigDecimalインスタンス,に変換して、事前定義されたメソッドにアクセスできるようにする関数であるという事実を活用しています。 .As the UnaryOperator comes with an apply() method, we can just replace applyDiscount with it.

combine()メソッドは、this.の結果に1つのDiscounterを適用することを抽象化したものです。これを実現するために、組み込み関数apply()を使用します。

それでは、複数のDiscountersを累積的に金額に適用してみましょう。 関数reduce()combine():を使用してこれを行います

Discounter combinedDiscounter = discounters
  .stream()
  .reduce(v -> v, Discounter::combine);

combinedDiscounter.apply(...);

最初のreduce引数に特に注意してください。 割引が提供されない場合、変更されていない値を返す必要があります。 これは、デフォルトのディスカウンターとしてID機能を提供することで実現できます。

これは、標準の反復を実行するよりも有用で冗長な代替手段です。 機能合成のためにすぐに使用できる方法を検討すると、さらに多くの機能が無料で提供されます。

4. 結論

この記事では、戦略パターンについて説明し、ラムダ式を使用して冗長性の少ない方法で実装する方法も示しました。

これらの例の実装はover on GitHubにあります。 これはMavenベースのプロジェクトなので、そのまま実行するのは簡単です。