Java Records: ทำ value object ให้สั้น แต่ยังตรวจ invariant ได้
class ที่มีแต่ field, constructor, accessor, equals, hashCode และ toString มักยาวกว่าความหมายจริง Record ให้ประกาศ state description แล้ว compiler สร้างสมาชิกเหล่านี้ให้ เหมาะกับ value object และข้อความที่ข้าม boundary ซึ่ง identity ขึ้นกับข้อมูล ไม่ใช่อายุของ object
WHY: ลด boilerplate โดยไม่ทิ้ง invariant
Record ไม่ได้แปลว่า “ถุงข้อมูลที่รับอะไรก็ได้” compact constructor ทำ validation หรือ normalize ก่อน field ถูกกำหนด ทำให้ object ทุกตัวที่สร้างสำเร็จรักษากติกาเดียวกัน ในตัวอย่าง Money เราบังคับสกุลเงินเป็นอักษรสามตัวและไม่รับจำนวนติดลบ
import java.util.Objects;
public class Main {
record Money(String currency, long minorUnits) {
Money {
currency = Objects.requireNonNull(currency).toUpperCase();
if (!currency.matches("[A-Z]{3}")) {
throw new IllegalArgumentException("currency must have three letters");
}
if (minorUnits < 0) {
throw new IllegalArgumentException("minorUnits must be non-negative");
}
}
}
public static void main(String[] args) {
var price = new Money("thb", 12500);
assert price.currency().equals("THB");
assert price.minorUnits() == 12500;
assert price.equals(new Money("THB", 12500));
boolean rejected = false;
try {
new Money("THB", -1);
} catch (IllegalArgumentException expected) {
rejected = true;
}
assert rejected;
}
}
Record ไม่ได้ทำทุกอย่าง immutable
component reference เป็น final แต่ object ที่มันชี้อาจแก้ได้ ถ้า record รับ List แล้วเก็บ reference ตรงๆ ผู้เรียกยังเปลี่ยนรายการภายหลังได้ สำหรับ value object ควร defensive copy ด้วย List.copyOf() ใน constructor และอย่าคืน mutable internals
Record เหมาะกับ DTO, command, event และ key ที่มีโครงสร้าง ไม่เหมาะเมื่อ framework ต้องการ no-arg constructor/field mutation หรือ object มี lifecycle และ identity ที่ไม่ควรเปลี่ยนตามทุก field การเปลี่ยน class เดิมเป็น record ยังเปลี่ยน serialization shape หรือ reflection behavior ได้ ต้องทดสอบ boundary จริง
Equality คือสัญญาที่ต้องตั้งใจ
record equality ใช้ component ทั้งหมดตามลำดับประกาศ ถ้าเพิ่ม field metadata ในอนาคต equality กับ cache key จะเปลี่ยนทันที จึงควรแยกข้อมูลที่เป็น identity ออกจากข้อมูลประกอบ แทนการใส่ทุกอย่างใน record เดียวเพราะสะดวก
Recap
ใช้ record เมื่อชนิดข้อมูลคือค่าที่นิยามด้วย components ใส่ invariant ใน compact constructor และ copy collection ที่แก้ได้ งานวันนี้คือเลือก DTO เล็กหนึ่งตัว ลบ boilerplate แล้วเพิ่ม regression test ของ equality และ input ผิดก่อน migrate ครับ