こんにちは。虎の穴ラボのH.Kです。 クラスを一通り作ったあとに、これメソッドチェーンできるようにしておけばよかったなーみたいなのがあったので、あとから適用できるように実装してみました。
なお、Javaのversionは8以降を想定して実装しています。 詳しくは実装の解説のところで。
結論
結論から先に示すと以下のようなインタフェースを定義して、メソッドチェーンを使いたいクラスでimplements
してあげれば良いです。
インタフェース
package chain.dto.interfaces; import java.util.function.Consumer; public interface Chainable<T> { // Consumerで引数がT型(実装クラスの型)で戻り値なしの関数を受け取る // defaultで定義しているので実装クラスでのオーバーライドは不要 default T chain(Consumer<T> consumer) { // Chainableの実装クラスがresourceに入る T resource = (T) this; // 戻り値なしの関数を実行する consumer.accept(resource); // 実装クラスを返す return resource; } }
実装クラス
package chain.dto; import chain.dto.interfaces.Chainable; public class HogeChainableDto implements Chainable<HogeChainableDto> { private String name; public void setName(String name) { this.name = name; } // toStringは省略 }
メソッドチェーンの使いかたは以下のような形になります。
new HogeChainableDto().chain(x -> x.setName("hogeChainableDto")).chain(System.out::println); // HogeChainableDto{name='hogeChainableDto'}
メソッドチェーンの実装
そもそもメソッドチェーンとはメソッドの戻り値に対して.
で処理をつなげていくことで無駄な変数宣言をせずにスッキリ書けるようにするものです。
メソッドチェーンはsetterなどの戻り値がないメソッドに全てreturn this;
を書いていったり、デザインパターンであるビルダーパターンを使ったりします。
以下は実装例となります。
メソッドチェーン未適用の実装
package chain.dto; public class HogeDto { private String name; public void setName(String name) { this.name = name; } // toStringは省略 }
setterを使った実装
package chain.dto; public class HogeChainDto { private String name; public HogeChainDto setName(String name) { this.name = name; // 自インスタンスを返却する return this; } // toStringは省略 }
ビルダーパターンを使った実装
package chain.dto; public class HogeBuilderDto { private String name; // privateにすることによりbuilder経由以外からのインスタンス生成をできなくする private HogeBuilderDto(Builder builder) { this.name = builder.name; } public static class Builder { private String name; public Builder name(String name) { this.name = name; return this; } public HogeBuilderDto build() { return new HogeBuilderDto(this); } } // toStringは省略 }
最初からメソッドチェーンを使うぞ、と実装を始めていれば、そこまで大変な実装ではありませんが、あとからたくさんのメンバ変数を持つクラスに対して適用するのはなかなか骨が折れます。
実装の解説
今回のインターフェースの実装について少しだけ解説します。
ポイントは2つで、defaultメソッドとして定義することと、Consumerを使うことです。
まずdefaultメソッドですが、Java 8で追加された機能で、インタフェースに実装内容を含めることができます。
次にConsumerですが、こちらもJava 8で追加された機能になります。Consumerは関数型インタフェースで、引数が1つで戻り値がない関数を表します。setterなどは引数が1つで戻り値がない関数なので、Consumerにより、メソッド内の好きなタイミングで関数を実行することができます。
今回の実装ではinstance.chain(x -> x.setXxx(yyy))
という形でラムダ式を使い関数を作成し、利用することを想定しています。
ついでの実装
Immutableにしてみる
需要があるかわかりませんが、Cloneableと組み合わせることでImmutableな実装にすることも可能です。
以下がサンプルのコードになります。
インタフェース
package chain.dto.interfaces; import java.util.function.Consumer; public interface ImmutableChainable<T> extends Cloneable { default T chainImmutable(Consumer<T> consumer) { T clone; try { clone = this.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } consumer.accept(clone); return clone; } T clone() throws CloneNotSupportedException; }
実装クラス
package chain.dto; import chain.dto.interfaces.ImmutableChainable; public class HogeImmutableChainableDto implements ImmutableChainable<HogeImmutableChainableDto> { private String name; public void setName(String name) { this.name = name; } @Override public HogeImmutableChainableDto clone() throws CloneNotSupportedException { return (HogeImmutableChainableDto) super.clone(); } @Override public String toString() { return "HogeChainableDto{" + "name='" + name + '\'' + '}'; } }
実際に動かしてみる
package chain; import chain.dto.*; public class Main { public static void main(String[] args) { // 通常のBean相当クラス HogeDto hogeDto = new HogeDto(); hogeDto.setName("hogeDto"); System.out.println(hogeDto.toString()); // HogeDto{name='hogeDto'} // setterの戻り値を変更してメソッドチェーンを使えるようにしたクラス System.out.println(new HogeChainDto().setName("hogeChainDto")); // HogeChainDto{name='hogeChainDto'} // builderパターンを適用したクラス System.out.println(new HogeBuilderDto.Builder().name("hogeBuilderDto").build()); // HogeBuilderDto{name='hogeBuilderDto'} // chainableの実装クラス // System.out::printlnも引数を1つとり、戻り値がない処理なのでメソッドチェーンで書ける(戻り値はHogeChainableDtoインスタンス) new HogeChainableDto().chain(x -> x.setName("hogeChainableDto")).chain(System.out::println); // HogeChainableDto{name='hogeChainableDto'} // immutableでchainableの実装クラス HogeImmutableChainableDto hogeImmutableChainableDto = new HogeImmutableChainableDto() .chainImmutable(x -> x.setName("hogeImmutableChainableDto")) .chainImmutable(System.out::println); // HogeChainableDto{name='hogeImmutableChainableDto'} // immutableになっているか確かめるため書き換える hogeImmutableChainableDto.chainImmutable(x -> x.setName("名前の書き換え")).chainImmutable(System.out::println); // HogeChainableDto{name='名前の書き換え'} // Immutableなので元のインスタンスには影響しない hogeImmutableChainableDto.chainImmutable(System.out::println); // HogeChainableDto{name='hogeImmutableChainableDto'} } }
まとめ
出来上がったクラスに対して簡単にメソッドチェーンが実装できるようになりました。 が、実はちょっとした課題がありますので、今後はそれを解決できるように実装を考えていきたいと思います。
※この実装の課題
クラスの継承を行うとうまくいかないケースがあります。
具体的にはChainableインタフェースの実装クラスを継承し、継承した子クラス側でもChainableインタフェースをimplements
したいケースです。
この場合、Chainableのジェネリクスの指定が重複してしまい、コンパイルエラーとなります。
継承した子側でimplements
しない場合、ラムダ式で使えるメソッドは親クラスのメソッドのみに制限されてしまいます。
一応以下のように変更すれば解決することはわかっていますが、呼び出し時にちょっと冗長になるので改善したいです。
インターフェースの実装
// クラスの型指定をやめる public interface Chainable { // メソッドにて型指定 default <T extends Chainable> T chain(Consumer<T> consumer) { T resource = (T) this; consumer.accept(resource); return resource; } }
呼び出し
// chainのラムダ式で型を確定させる new HogeChainableDto().chain((HogeChainableDto x) -> x.setName("hogeChainableDto")).chain(System.out::println); // HogeChainableDto{name='hogeChainableDto'}
P.S.
【オンライン開催】とらのあなエンジニア&マーケター採用説明会【地方勤務可能!!】
11/20(金)に、採用説明会をオンラインにて開催します。 虎の穴ラボへの転職を考えている方や地元に住みながらWEBエンジニアの仕事に就きたい方、ちょっと話を聞いてみたい方など、ご参加をお待ちしております! yumenosora.connpass.com
カジュアル面談
弊社エンジニアと1on1で話せます、カジュアル面談も現在受付中です!こちらも是非ご検討ください。 news.toranoana.jp
その他採用情
虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。
カジュアル面談では虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今期何見ました?」といったオタクトークから業務の話まで何でもお応えします。
カジュアル面談や採用情報はこちらをご確認ください。
yumenosora.co.jp