はじめに
Java8にてラムダ式とStream APIが導入され、Java言語にもようやく関数型プログラミングのパラダイムが持ち込まれました。これにより設計の定石
も変わりつつあります。
Stream APIとラムダ式に関しては、オライリーから出ている良書『Javaによる関数型プログラミング』で学ぶのがよいと思いますが、この書籍の4章「ラムダ式で設計する」ではラムダ式を活用した新しい設計のアイデアが紹介されています。
本記事では、上記書籍にインスパイアされた筆者が、ラムダ式とStreamを用いたデザインパターンの新しい実装方法を検証、考察します。
今回のパターン
GoFの生成に関するパターンのひとつ、Builder
パターンを考察します。
JavaでのBuilder
パターンの実装については こちらのページにまとめられていますのでご参照ください。
流暢なインターフェースの改善
『Javaによる関数型プログラミング』では以下のテクニックが紹介されています。
- コンストラクタをprivateにして直接インスタンス化されるのを防ぐ
- 代わりに、
Consumer<T>
型の関数を引数に受け取るstaticメソッドを用意する
public class MailBuilder {
private String fromAddress = "";
private String toAddress = "";
private List<String> ccAddresses = new ArrayList<>();
private String subject = "";
private String body = "";
private MailBuilder() {
}
public MailBuilder from(String address) {
this.fromAddress = address;
return this;
}
public MailBuilder to(String address) {
this.toAddress = address;
return this;
}
public MailBuilder cc(String address) {
this.ccAddresses.add(address);
return this;
}
public MailBuilder subject(String subject) {
this.subject = subject;
return this;
}
public MailBuilder body(String body) {
this.body = body;
return this;
}
private void doSend() {
StringBuilder sb = new StringBuilder();
sb.append("TO:").append(toAddress).append("\r\n");
if (!ccAddresses.isEmpty()) {
sb.append("CC:").append(String.join(",", ccAddresses)).append("\r\n");
}
sb.append("FROM:").append(fromAddress).append("\r\n");
sb.append("SUBJECT:").append(subject).append("\r\n");
sb.append("BODY:").append(body).append("\r\n");
System.out.println(sb.toString());
}
public static void send(final Consumer<MailBuilder> consumer) {
final MailBuilder mailer = new MailBuilder();
consumer.accept(mailer);
mailer.doSend();
}
上記Builder
の使用例は以下のようになります。
MailBuilder.send(mailer -> {
mailer.from("fowler@example.com")
.to("trump@example.com")
.subject("Greeting")
.body("Hello, Mr. President!");
});
従来の流暢なインターフェース
で組み立てを行うBuilderと比較したメリットは以下とされています。
-
new
キーワードを使用しないので、より可読性が高く流暢である -
Builder
のインスタンスの参照スコープが、staticメソッドに渡すコードブロック内に限定される
問題点
流暢なインターフェース
型のBuilder
を使っていて不便だなと思うのは、条件によってメソッドの呼び出しを切り替えたい場合や、繰り返してメソッドを呼び出したい場合などです。
Javaの言語仕様上、ドット.
で連結したメソッドチェーンの中に制御構文を埋め込むことはできませんから、メソッドチェーンを途中で分断してif文やfor文を用いて制御することとなり、結果として流暢
でなくなってしまいます。
この問題をうまく解決できないでしょうか?
条件制御を組み込む
条件式がtrue
の場合のみ、Consumer<T>
を呼び出すメソッドを追加します。
public MailBuilder doIf(boolean condition, final Consumer<MailBuilder> consumer) {
if (condition) {
consumer.accept(this);
}
return this;
}
ラムダ式がネストされた形となってしまうものの、メソッドチェーンを分断せずに条件制御を組み込むことができました。
MailBuilder.send(mailer -> {
mailer.from("fowler@example.com")
.to("trump@example.com")
.doIf(someCondition(), m -> m.cc("clinton@example.com"))
.subject("Greeting")
.body("Hello, Mr. President!");
});
繰り返し制御を組み込む
今度は関数型を2つ引数に取る必要があるので若干複雑です。
まずは繰り返し対象となるものを、Iterable<T>
型で受け取ります。
そして2つ目の引数はBiConsumer<T,U>
です。繰り返しの要素(T
型のインスタンス)とBuilder
インスタンスの参照を受け取って処理する必要があるので、Consumer<T>
ではなくBiConsumer<T,U>
としているのがポイントです。
public <T> MailBuilder foreach(Iterable<T> iterable, final BiConsumer<MailBuilder, T> consumer) {
iterable.forEach(t -> consumer.accept(this, t));
return this;
}
以下のように、メソッドチェーンを分断せずに繰り返し制御を埋め込むことができました。
final List<String> ccAddresses = Arrays.asList("clinton@example.com", "cockburn@example.com");
MailBuilder.send(mailer -> {
mailer.from("fowler@example.com")
.to("trump@example.com")
.foreach(ccAddresses, (m, ccAddress) -> m.cc(ccAddress))
.subject("Greeting")
.body("Hello, Mr. President!");
});
まとめ
ラムダ式をうまく活用すると、よりすっきりして見通しのよいコードを書くことが可能となります。
今回は流暢なインターフェース
型のBuilder
パターンの実装を、もっと流暢にしてみる方法を検討してみました。