考察:ラムダ式によって「計算を表すオブジェクト」という本質が見えてくる
以上の例より、計算を表すオブジェクトは、ラムダ式によって簡潔に生成できることが分かります。
前回紹介したCommandパターンの場合と同様に、ラムダ式を使うプログラム(リスト3)は、Strategyパターンのプログラム(リスト2)から、構造は大きく変わっていません。Logger クラスと TimestampFormatter インタフェースは、実質的に何も変更されていません。変わっているのは、時刻から文字列への変換を表す TimestampFormatter インタフェースのインスタンスを作るところだけです。リスト2では、クラスを宣言した上で、そのクラスをインスタンス化しているのに対して、リスト3では、ラムダ式で直接インスタンスを生成しています。基本的な構造が変わっていないため、ラムダ式バージョンのプログラムについても、Strategyパターンを採用しているとみなすことは可能です。
しかしながら、前回Commandパターンの例として取り上げた亀シミュレータのラムダ式バージョンと、今回Strategyパターンの例として取り上げたログ出力プログラムのラムダ式バージョンを引き比べると、2つをわざわざ別のパターンとして分類することは不要と感じられるのではないでしょうか。
亀シミュレータでは、亀を動かす処理をラムダ式で記述して渡しており、ログ出力プログラムでは、時刻から文字列への変換をラムダ式で記述して渡しています。どちらも、処理や計算をオブジェクトとして受け渡しているだけです。ラムダ式を用いて、クラスという夾雑物をプログラムから取り除くことによって、「処理や計算をオブジェクトとして扱う」という本質的な構造が直接的に現れるようになったために、あえてパターンとして分類する必要がなくなったわけです。
Strategyの役割を果たすインタフェースは関数型インタフェースであることが多いため、ログ出力プログラムに限らず、ラムダ式に書き換えられる可能性があります。たとえば、2つのオブジェクトを比較する Comparator インタフェースは、compare メソッドを唯一の抽象メソッドとして持つ関数型インタフェースです。したがって、TreeSet クラスと Comparator インタフェースを使うリスト1のプログラムは、ラムダ式を使ってリスト5のように書き換えられます。
import java.util.*;
class Employee {
private final String name;
Employee(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
public class LambdaEmployeeStorer {
public static void main(String[] args) {
NavigableSet<Employee> employees = new TreeSet<>(
(x, y) -> x.getName().compareTo(y.getName())
);
employees.add(new Employee("Stephen"));
employees.add(new Employee("Aggi"));
employees.add(new Employee("Katrina"));
employees.forEach(e -> System.out.println(e.getName()));
// 出力: Aggi <改行> Katrina <改行> Stephen
}
}
リスト5では、従業員オブジェクトを名前で比較するという計算を、ラムダ式を使って、その場で TreeSet クラスのコンストラクタに渡しています。ログ出力プログラムの場合と同様、プログラムの構造は大きく変わっていません。
Strategyパターンのプログラムは、何がなんでもラムダ式を使って置き換えなければならない、というわけではもちろんありません。たとえば、計算が複雑であり、複数のメソッドに分割されているような場合は、ConcreteStrategyの役割を果たすクラスをそのまま保っておくほうがよいかもしれません。この他にも、CDIやSpring FrameworkのようなDIコンテナを使って、ストラテジオブジェクトを他のオブジェクトに注入する場合には、ConcreteStrategyをクラスとして宣言するほうが、容易に設定できることが多いでしょう。
おわりに
次回は「Observerパターンをラムダ式で置き換える」方法について解説します。お楽しみに。
