こんにちわ、みけです。
今日はラムダ式を使って簡単なビルダーパターンを作りたいと思います。
なお、結論を先に書くと…次のツイートを読むのが一番早いです
メソッドチェーンの実装を
arg1 -> arg2 -> arg3 -> doSomething(arg1, arg2, arg3);
って書けるのに気づいた
— もちだでしたが、衰退しました (@mike_neck) 2014, 12月 19
インスタンスを作るのが面倒いクラス
例えば次のようなイミュータブルなクラスを考えます。
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
のインスタンスを作るまでの一連のメソッド一つにつき、一つのインターフェースをつくります。
たとえば、description
とreported
を設定するためのインターフェースを次のようにつくります。
// 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っぽくないコードになった٩(๑❛ᴗ❛๑)۶
ちなみにインターフェースを設定したいパラメーターの分だけ設定しないといけないのが若干(かなり)面倒です(´・ω・`)
以上