LoginSignup
23
27

More than 5 years have passed since last update.

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

Posted at

はじめに

Java8にてラムダ式とStream APIが導入され、Java言語にもようやく関数型プログラミングのパラダイムが持ち込まれました。これにより設計の定石も変わりつつあります。
Stream APIとラムダ式に関しては、オライリーから出ている良書『Javaによる関数型プログラミング』で学ぶのがよいと思いますが、この書籍の4章「ラムダ式で設計する」ではラムダ式を活用した新しい設計のアイデアが紹介されています。
本記事では、上記書籍にインスパイアされた筆者が、ラムダ式とStreamを用いたデザインパターンの新しい実装方法を検証、考察します。

今回のパターン

GoFの生成に関するパターンのひとつ、Builderパターンを考察します。
JavaでのBuilderパターンの実装については こちらのページにまとめられていますのでご参照ください。

流暢なインターフェースの改善

『Javaによる関数型プログラミング』では以下のテクニックが紹介されています。

  • コンストラクタをprivateにして直接インスタンス化されるのを防ぐ
  • 代わりに、Consumer<T>型の関数を引数に受け取るstaticメソッドを用意する
FluentBuilder
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の使用例は以下のようになります。

FluentBuilder-Usage
        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>を呼び出すメソッドを追加します。

MoreFluentBuilder
    public MailBuilder doIf(boolean condition, final Consumer<MailBuilder> consumer) {
        if (condition) {
            consumer.accept(this);
        }
        return this;
    }

ラムダ式がネストされた形となってしまうものの、メソッドチェーンを分断せずに条件制御を組み込むことができました。

MoreluentBuilder-Usage
        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>としているのがポイントです。

MoreFluentBuilder2
    public <T> MailBuilder foreach(Iterable<T> iterable, final BiConsumer<MailBuilder, T> consumer) {
        iterable.forEach(t -> consumer.accept(this, t));
        return this;
    }

以下のように、メソッドチェーンを分断せずに繰り返し制御を埋め込むことができました。

MoreFluentBuilder2-Usage
        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パターンの実装を、もっと流暢にしてみる方法を検討してみました。

23
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
27