mike-neckのブログ

Java or Groovy or Swift or Golang

Java8でJavaっぽくないコード書いてみた

こんにちわ、みけです。

今日はラムダ式を使って簡単なビルダーパターンを作りたいと思います。

なお、結論を先に書くと…次のツイートを読むのが一番早いです

インスタンスを作るのが面倒いクラス

例えば次のようなイミュータブルなクラスを考えます。

public class Issue {
    private final long id;
    private final String title;
    private final String description;
    private final Status status;
    private final Member reporter;
    private final Member assignedTo;
    private final LocalDateTime dueDate;
    private final LocalDateTime created;
    private final LocalDateTime updated;

    public Issue(long id, String title, String description,
          Status status, Member reporter, Member assignedTo,
          LocalDateTime dueDate, LocalDateTime created, LocalDateTime updated) {
        this.id = id;
        this.title = title;
        this.description = description;
        this.status = status;
        this.reporter = reporter;
        this.assignedTo = assignedTo;
        this.dueDate = dueDate;
        this.created = created;
        this.updated = updated;
    }
    // getterとかtoStringは省略
}

このクラスを生成する場合、コンストラクターはめちゃめちゃ面倒です。

    final Issue issue = new Issue(issueId(), title, desc,
            Status.Created, reportedBy, assignee, dueDate,
            LocalDateTime.now(), LocalDateTime.now());

何が面倒かというと、引数が多くてこのパラメーターは何を意味しているのか、短期記憶が破壊的なまでに欠落している僕にはわからなくなって、コードを書いている途中で頭のなかが「?」だらけになります。なお、IntelliJ IDEAを使っている場合は「⌘+P」で現在書いているパラメーターが何かを参照することができます。

この時、書いているのがObjective-Cであれば、次のような式になるので、パラメーターの意味がわかってうれしかったりします。

    Issue issue = [Issue allocWithId: issueId()
                  title: title
            description: desc
                 status: Status.Created
               reporter: reportedBy
             assignedTo: assignee
                  dueTo: dueDate
                created: now
                updated: now];

(Objective-C書いていないので、文法的にこれであってるかわからん…)

また、groovyだと、名前付き引数でコンストラクターを呼び出せるので、これもうれしかったりします

    def issue = new Issue(id: issueId(), description: desc,
            title: title, reporter: reportedBy, created: now, updated: now
            dueTo: dueDate, assignedTo: assignee, status: Status.Created)

(ただし、このコンストラクターの形式で呼び出す場合、final修飾子が付けられなかったと思う)

ただ、まあ、今はJavaをやってるので、他の言語のことは忘れましょう。Javaでも名前付き引数でコンストラクターを呼び出したいということだけ言いたいのです。

そこでなんかビルダーっぽいやつを作る

さっきのクラスのインスタンスを作るのに、次のように書きたいわけです。

    final Issue issue = title("チャットの最初にお疲れ様ですって書くのヤメロ")
            .description("本題を待つまでの時間勿体無いからやめてほしいです")
            .reportedBy(reporter)
            .assignedTo(assignee)
            .dueTo(LocalDateTime.now().plusMonths(1l));

これをとりあえず簡単に作ると次のようなクラスが出来上がると思います。

    public static Builder title(String t) {
        return new Builder(t);
    }

    public static class Builder {
        private String title;
        private String desc;
        private Member reporter;
        private Member assignee;

        private Builder(String t) {
            this.title = t;
        }

        //一部のメソッドは省略

        public Issue dueTo(LocalDateTime d) {
            return new Issue(issueId(), title,
                    desc, Status.Created, reportedBy,
                    assignee, dueDate, LocalDateTime.now(),
                    LocalDateTime.now());
        }
    }

ただ、この方法だと、メソッドdescription(String)を呼び出したかどうかわからないし、もし呼び出してなかったらnullを渡してしまうので後々面倒なことになります。

先ほどのtitle(String)から始まるメソッドチェーンはオプショナルな呼び出しなしで、呼び出しの順番を強制したいのです。

インターフェースでチェーンを作る

メソッド呼び出しを強制したいので、Issueインスタンスを作るまでの一連のメソッド一つにつき、一つのインターフェースをつくります。

たとえば、descriptionreportedを設定するためのインターフェースを次のようにつくります。

    // descriptionを設定してreportedを設定するためのインターフェースを返す
    public static interface Description {
        public Reported description(String d);
    }

    // reportedを設定してassignedを設定するためのインターフェースを返す
    public static interface Reported {
        public Assignee reported(Member r);
    }
    //他のインターフェースは省略

これらのインターフェースは次に呼び出してほしいインターフェースを返すようにメソッドの戻り値のインターフェースを設定しておきます。

そして、それらを全て実装したクラスをつくります。

    public static Builder title(String t) {
        return new Builder(t);
    }

    public static class Builder implements Description,
            Reported, Assignee, DueTo {
        private String title;
        private String desc;
        private Member reporter;
        private Member assignee;
        private Builder(String t) {
            this.title = t;
        }

        public Reported description(String d){
            this.desc = desc;
            return this;
        }

        //一部のメソッドは省略

        public Issue dueTo(LocalDateTime d) {
            return new Issue(issueId(), title,
                    desc, Status.Created, reportedBy,
                    assignee, dueDate, LocalDateTime.now(),
                    LocalDateTime.now());
    }

この実装によって、引数が多いIssueコンストラクターを名前付きで呼び出すことができるようになりました。

別に実装したクラスを定義しなくてもいいよね

さて、上の例では内部のクラスを作っていましたが、これは特にクラスを実装せず、匿名クラスにしてもよさそうです。

    public static Description title(String title) {
        return new Description(){
            @Override public Reported description(String desc) {
                return new Reported() {
                    @Override public Assignee reported(Member reporter) {
                        return new Assignee() {
                            @Override public DueTo assignedTo(Member assignee) {
                                return new DueTo() {
                                    @Override public Issue dueTo(LocalDateTime due) {
                                        return new Issue(issueId(), title,
                                                desc, Status.Created,
                                                reporter, assignee,
                                                due, LocalDateTime.now(),
                                                LocalDateTime.now());
                                    }
                                };
                            }
                        };
                    }
                };
            }
        };
    }

「このようなコードを書かせられるからJavaは…」という嘆きの声が聞こえてきそうなコードになりました(´・ω・`)

そこでラムダですよ

Java8からは実装するべきメソッドが一つのインターフェースのことを関数型インターフェースと呼んで、ラムダでの記述によって楽に書くことができるようになりました。

例えば、上記のコードの一部

    return new DueTo() {
        @Override public Issue dueTo(LocalDateTime due) {
            return new Issue(issueId(), title,
                    desc, Status.Created,
                    reporter, assignee,
                    due, LocalDateTime.now(),
                    LocalDateTime.now());
        }
    };

は次のように記述できます。

    return due -> new Issue(issueId(), title, desc,
            Status.Created, reporter, assignee,
            due, LocalDateTime.now(), LocalDateTime.now());

また、このDueToインターフェースを返すAssigneeインターフェースもラムダで少し省略すると次のようになります。

    return assignee -> {
        return due -> new Issue(issueId(), title,
                desc, Status.Created, reporter,
                assignee, due, LocalDateTime.now(),
                LocalDateTime.now());
    };

ところで、1行だけでreturnをするラムダは「{」「};」「return」をまとめて省略できます。したがって、上記のAssigneeインターフェースは次のように書き換えられます。

    return assignee -> due -> new Issue(issueId(), title,
                desc, Status.Created, reporter, assignee,
                due, LocalDateTime.now(), LocalDateTime.now());

以上の原則を繰り返していくと…

    public static Description title(String title) {
        return desc -> reporter -> assignee -> due -> new Issue(
                issueId(), title, desc, Status.Created,
                reporter, assignee, due, LocalDateTime.now(),
                LocalDateTime.now());
    }

さらにdescriptionを設定するインターフェースの名前をIssueBuilderに変更します。

    public static IssueBuilder title(String title) {
        return desc -> reporter -> assignee -> due -> new Issue(
                issueId(), title, desc, Status.Created,
                reporter, assignee, due, LocalDateTime.now(),
                LocalDateTime.now());
    }

という感じで、なんかJavaっぽくないコードになった٩(๑❛ᴗ❛๑)۶

ちなみにインターフェースを設定したいパラメーターの分だけ設定しないといけないのが若干(かなり)面倒です(´・ω・`)


以上