🚀

[Java] Recordクラスの使い所を探る

2023/03/26に公開

概要

  • Recordクラスは単純に値を保持するイミュータブルなクラスだと思ってたが実は他にも使い道がありそう
  • Static Factoryパターンとの組み合わせに相性が良さそう
  • Mapあたりと組み合わせるとオブジェクトを集約するロジックをいい感じに書けそう

みたいな話。

Recordクラスとは

とりあえず公式のJavaDocはこちら。
確かJava14でプレビュー版として実装され、Java17で正式版となったような(間違っていたらすいません)。

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Record.html

本題

事前準備

まずはデータ保持するクラスを作っておきます。
これはよく見る形というか基本はこれでしかない。

Student.java
/**
 * 試験オブジェクト
 *
 * @param name 名前
 * @param subject 教科
 * @param score 点数
 */
record Examination(
    String name,
    String subject,
    BigDecimal score
) {}

ドメインオブジェクト的なものを作る

ここからが本題。
各生徒の試験結果の平均点数を算出するためのクラスを作ります。

AverageScore.java
/**
 * 平均点数オブジェクト
 *
 * @param map
 *     <ul>
 *       <li>key : 名前
 *       <li>value : 平均点数
 *     </ul>
 */
record AverageScore(Map<String, BigDecimal> map) {

  BigDecimal value(String name) {
    return map.getOrDefault(name, BigDecimal.ZERO);
  }

  static AverageScore aggregate(List<Examination> examinations) {
    var aggregated = new HashMap<String, BigDecimal>();

    var perName = examinations.stream()
	    .collect(Collectors.groupingBy(Examination::name));
    for (var entry : perName.entrySet()) {
      var totalScore = entry.getValue()
	      .stream()
              .map(Examination::score)
              .reduce(BigDecimal.ZERO, BigDecimal::add);
      var count = BigDecimal.valueOf(entry.getValue().size());
      var averageScore = totalScore.divide(count, 2, RoundingMode.HALF_UP);

      aggregated.put(entry.getKey(), averageScore);
    }

    return new AverageScore(aggregated);
  }
}

Static Factoryパターンのように aggregate メソッドで試験結果の一覧を引数に受け、平均点数の算出と各生徒の名前をキーに集約していきます。

テスト

AverageScoreTest.java
public class AverageScoreTest {

  @Test
  public void test() {
    var list = new ArrayList<Examination>();
    // Aさんは勉強が得意
    list.add(new Examination("A", "Mathematics", BigDecimal.valueOf(100)));
    list.add(new Examination("A", "Science", BigDecimal.valueOf(98)));
    list.add(new Examination("A", "History", BigDecimal.valueOf(92)));
    // Bさんは普通
    list.add(new Examination("B", "Mathematics", BigDecimal.valueOf(52)));
    list.add(new Examination("B", "Science", BigDecimal.valueOf(58)));
    list.add(new Examination("B", "History", BigDecimal.valueOf(77)));
    // Cさんは勉強が苦手
    list.add(new Examination("C", "Mathematics", BigDecimal.valueOf(14)));
    list.add(new Examination("C", "Science", BigDecimal.valueOf(21)));
    list.add(new Examination("C", "History", BigDecimal.valueOf(9)));

    // 各生徒の平均点数を算出
    var averageScore = AverageScore.aggregate(list);

    // 算出結果
    assertEquals(new BigDecimal("96.67"), averageScore.value("A"));
    assertEquals(new BigDecimal("62.33"), averageScore.value("B"));
    assertEquals(new BigDecimal("14.67"), averageScore.value("C"));
  }
}

まとめ

  • 単純に値を保持するだけの使い道だけではなく、ドメインオブジェクトのようなものを作って計算処理や集約処理を書くことができる
  • 従来通りロジック中で Map<String, BigDecimal> に変換するのでももちろんいいけれど、ドメインオブジェクトのように意味のある名前をつけてあげられることに有意性がある
  • 計算処理等の複雑になりがちなロジックをオブジェクト単位で分離していけるので取り回しがしやすい

最後に

この書き方を同僚の方がやっていて勉強になったと思ったので備忘を兼ねて書きました。
(人の褌で相撲を取るスタイル)

Discussion