JavaでCRTP(Curiously Recurring Template Pattern) (Lambig)

本記事は
https://qiita.com/advent-calendar/2023/java
参加記事です。

こんにちは、株式会社クラス システム開発本部 Lambigです。

今回の記事はいつも以上に新規性がないのですが、気に入って使っているテクニックの話なので軽く触れたいと思います。

「I/Fに依存する実装」を試みているときでも、時折実装あるいは子I/Fの情報が必要になってくるケースがあると思います。(SerializableなList問題とか)
例えばこんなI/Fがあるとします。

  public interface SomeInterface {
    int value();

    @NonNull
    SomeInterface withAdded(int delta); //増分を加えて新しいインスタンスを返してほしい 
  }

実装はこのように作ります。

  @Value
  @Accessors(fluent = true)
  public class SomeImplementation implements SomeInterface {
    int value;

    @Override
    public @NonNull SomeInterface withAdded(int delta) {
      return new SomeImplementation(this.value() + delta);
    }
  }

さて、これを利用するときには以下のようになります。

final SomeInterface someInstance = new SomeImplementation(1);
final SomeInterface nextInstance = new SomeImplementation(1).withAdded(1);
final SomeImplementation yetNextInstance = (SomeImplementation) new SomeImplementation(1).withAdded(1);

I/Fだけを意識していればよい場合は問題ないのですが、実装の情報が欲しくなったとたんにキャストが出てきて不安になります。
また、以下のようなうっかりが可能な設計になっています。

  @Value
  @Accessors(fluent = true)
  public class WrongImplementation implements SomeInterface {
    int value;

    @Override
    public @NonNull SomeInterface withAdded(int delta) {
      return new SomeImplementation(this.value() + delta);
    }
  }

自身のクラスの新しいインスタンスを返してほしいのですが、I/Fではそこまで約束していないため、別のインスタンスを返しても問題ないことになります。TypeScriptならthis型を返す宣言ができますが、Javaだとそうもいきません。

こういったことの回避に利用できるテクニックがCRTP、Curiously Recurring Template Patternです。もともとはC++のデザインパターンで、Javaでも利用することができます。
これを利用する場合、I/Fは以下のように切ります。

  public interface AnotherInterface<I extends AnotherInterface<I>> {
    int value();

    @NonNull
    I withAdded(int delta);
  }

型引数が再帰表現になっているのでRecurringというわけですね。見慣れないと何事かと思う(Curious)感は確かにあると思います。ポイントは実装型をIとして参照できる仕組みになっている点です(型消去の話はおいといて)。

実装はこうなります。

  @Value
  @Accessors(fluent = true)
  public class AnotherImplementation implements AnotherInterface<AnotherImplementation> {
    int value;

    @Override
    @NonNull
    public AnotherImplementation withAdded(int delta) {
      return new AnotherImplementation(this.value() + delta);
    }
  }

AnotherInterface<AnotherImplementation>と宣言した以上、withAddedの戻りの型はAnotherImplementationに固定されます。これで、Iの型宣言自体を間違えるポカをやらない限り確実に戻りの型を指定できます。

利用するときはこうなります。

final AnotherInterface<?> someInstance = new AnotherImplementation(1);
final AnotherInterface<?> nextInstance = new AnotherImplementation(1).withAdded(1);
final AnotherImplementation yetNextInstance = new AnotherImplementation(1).withAdded(1);

これで必要になった場合も安全に実装型情報を得ることができるようになりました。個人的にはエンティティや値オブジェクトの等価性評価メソッドを生やすためのI/Fなどでよく使ったりします(equalsメソッドのようにObjectを引くのではなく、同じ型のインスタンスのみ評価できるようにする場合)。

ということでざっくりCRTPの紹介でした。基盤実装によってよい制約をかけられるならそうしたほうが良いと考えているので、Javaなりに表現をうまく使っていきたいものです。

この記事が気に入ったらサポートをしてみませんか?