7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaAdvent Calendar 2024

Day 10

個人的に注目しているJEP 468について

Last updated at Posted at 2024-12-09

はじめに

この記事は Java Advent Calendar 2024 10日目の記事です。

今年アドベントカレンダー初参加なんですが、少しお酒が入っている時に軽い気持ちで参加してみたら、みなさんレベル高くて内心焦りながら書いてますw

というわけで?、今回は最近知ってビビッときた、個人的に注目しているJEPについて紹介したいと思います。

注目しているJEPについて

さっそく本題ですが、私が個人的に注目しているJEPは・・・

JEP 468: Derived Record Creation (Preview)

です。

私がこのJEPを知ったのは、2024/9/12に開催されたJJUGナイトセミナーでした。

簡単に言うとRecordの特定フィールドを更新しつつ複製する機能なんですが、なぜこの機能に注目しているのかを順を追って説明します。

Recordについて

まずはJava16から追加になったRecordについておさらいします。

Immutableなデータクラスを簡単に表現することが出来て、以下のような特徴があります。

  • Recordクラスは常にfinal
  • フィールド変数は常にprivate final
  • コンストラクタ、getterが自動生成される
  • equals()、hashCode()、toString()メソッドが自動生成される

例えば、ECサイトにおけるカートのコンテキストで、カートに入れた商品の個数をRecordで表現すると以下のようなイメージになるかと思います。

Quantity.java
package domain.cart;

public record Quantity(Integer value) {

    public Quantity {
        if (value <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than 0");
        }
    }
}

コンストラクタは自動生成されると書きましたが、このように生成時に引数のバリデーションチェックを行うことができます。
この例だと、商品の個数は常に0より大きいことを担保しています。

これを従来のclassで表現しようとすると以下のようなコードになります。

Quantity2.java
package domain.cart;

public final class Quantity2 {

    private final Integer value;

    public Quantity2(final Integer value) {
        if (value <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than 0");
        }
        this.value = value;
    }

    public Integer value() {
        return value;
    }

    @Override
    public boolean equals(final Object o) {
        if (!(o instanceof final Quantity2 other)) return false;
        return other.value.equals(value);
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }

    @Override
    public String toString() {
        return String.format("Quantity2[value=%d]", value);
    }
}

こうして比較してみると、Recordによってかなりスッキリと書けていることがわかります。

Recordを使うモチベーション

私自身、普段からRecordを積極的に使ってまして、データを扱う場面ではまずRecordを第一候補に挙げるほどです。
と言うわけで、私が積極的にRecordを採用するモチベーションを整理してみます。

  • Immutableゆえに、認知負荷を下げることができる
    • 変更影響を調査する際に、final classゆえに継承されている可能性を排除できる
    • setterが無いので、そのObjectが生成されたタイミングだけ見れば中身が把握できる
    • 副作用がないため、テストが書きやすい
    • 非同期処理など複数スレッドでも扱いやすい
  • DDDの文脈における、Value Objectと親和性が高い

こうやって見ると、RecordというよりImmutableにメリットを感じて採用してたみたいですね。

ここがちょっと嫌だよ、Record!

ここまでRecordの良いことばかり書いてきたんですが、パッと思いついた範囲で2つ、個人的に気になる部分があるので紹介します。

①デフォルトのコンストラクタがpublic一択である

例えば先ほどのカートのコンテキストにおいて、商品価格 x 商品点数 で 行価格を計算するビジネスロジックがあったとします。

LinePrice.java
package domain.cart;

import java.math.BigDecimal;

public record LinePrice(BigDecimal value) {

    public static LinePrice of(final SellPrice sellPrice, final Quantity quantity) {
        return new LinePrice(sellPrice.value().multiply(BigDecimal.valueOf(quantity.value())));
    }
}

この場合、行価格は常に商品個数と商品点数から算出してほしいんですが、デフォルトのコンストラクタがpublicなので、LinePriceをnewすることが出来ます。

最低限必ず守りたいルールは先ほどのバリデーションで制御することは出来ますが、LinePriceを生成する選択肢を無駄に増やしてしまうのはバグを埋め込む要因になりますし、あんまり気持ち良くないです。

ただ、自前の生成メソッドを提供している場合も、永続化層から復元する際にデフォルトのコンストラクタを使いたい場面もあるので、そうゆう用途で残しているんですかね?

public固定ではなく選択の余地はあっても良いと思うんですが、どうなんでしょう。

② 更新がある場合、冗長なコードになりがち

こちらもカートのコンテキストで考えてみましょう。
商品点数を1から2に増やしたいとき、以下のようなコードを書きます。

CartItem.java
package domain.cart;

public record CartItem(ItemId itemId, SellPrice sellPrice, Quantity quantity) {

    public LinePrice linePrice() {
        return LinePrice.of(sellPrice, quantity);
    }

    // 新しい商品点数でCartItemを生成しなおすメソッドを用意
    public CartItem withQuantity(final Quantity newQuantity) {
        return new CartItem(itemId, sellPrice, newQuantity);
    }
}
Main.java
import domain.cart.CartItem;
import domain.cart.ItemId;
import domain.cart.Quantity;
import domain.cart.SellPrice;

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {

        // カート商品を作成
        final CartItem cartItem = new CartItem(new ItemId(1L), new SellPrice(BigDecimal.valueOf(100L)), new Quantity(1));

        // カート商品の合計金額を計算
        System.out.println("合計金額①: " + cartItem.linePrice().value());

        // カート商品の数量を変更
        final CartItem updatedCartItem = cartItem.withQuantity(new Quantity(2));

        // カート商品の合計金額を再計算
        System.out.println("合計金額②: " + updatedCartItem.linePrice().value());
    }
}
実行結果
合計金額①: 100
合計金額②: 200

今回は商品点数の更新だけですが、これが複数フィールドを持ち、複数の更新パターンがあるとなかなかしんどいことになります。

そこで、JEP 468です。(ようやく本題!)

このJEPの機能が追加されると、自前でwithXXのようなメソッドを用意しなくとも、Recordの複製が可能になります。(ステキ!)

JEPのサンプルを見る限り、以下のような感じで書けることを目指しているんだと思います。

Main.java
        // カート商品の数量を変更
        //final CartItem updatedCartItem = cartItem.withQuantity(new Quantity(2));

        // カート商品の数量を変更(JEP468)
        final CartItem updatedCartItem = cartItem with {
            quantity = new Quantity(2);
        }

ただ、previewとなっているものの、Java 23でpreviewを有効にしても動かなかったのでまだ入っていないようです。
何か揉めてるんですかね? ちなみにKotlinのdata classには既に同じような機能があるので、早くJavaでも使えるようになるのが待ち遠しいです!

おわりに

いかがだったでしょうか。

Recordのおさらいをしつつ、今後Recordに追加されそうな新機能について書いてみました。

Recordしかり今回の新機能しかり、昨今の開発スタイル(トレンド)の変化に合わせて、Javaも進化しようとしている雰囲気が感じ取れて、ますますJavaが好きになりそうですね!

こちらからは以上です!

7
0
0

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
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?