使用Record与Sealed类简化Java DTO设计
告别样板代码:当DTO遇上Record与Sealed类
如果你写过几年Java,一定对那些动辄几十行的DTO类记忆犹新:私有字段、getter/setter、equals/hashCode、toString,有时候还得手写一个全参构造器。IDE的自动生成功能虽然能救命,但每新增一个字段,就要重新生成一遍,代码仓库里充满了无意义的重复劳动。直到Java 14预览、Java 16正式转正的Record,才终于让我们看到了“数据载体”本该有的清爽模样。
Record不是简单的语法糖,它从语义上就声明了自己是“不可变数据的透明载体”。一个`record UserDTO(Long id, String name)`就完成了过去二十行代码的事:构造器、访问器(注意是`id()`不是`getId()`)、equals、hashCode、toString全部内置。更重要的是,它的不可变性让DTO在并发场景下天然安全,也避免了“对象被意外修改”这类隐晦bug。当你在代码review中看到`record`,立刻就能明白:“这是一个纯数据对象”,这种自解释性远比一堆注解更有价值。
不过,Record也带来了新的设计思考:当业务需要对同一DTO的不同状态建模时怎么办?比如订单有“待支付”“已支付”“已取消”,每个状态下允许的字段不同。传统做法是建一个大DTO,塞进所有字段,再用null表示不适用。这种方式不仅浪费内存,更在业务判断时埋下无数`if (xxx != null)`的烦恼。此时,Sealed类就派上了用场。
Sealed类(Java 17正式引入)允许你限定一个父类的直接子类集合。结合Record,我们可以设计出真正贴合领域状态的类型层次:
public sealed interface OrderDTO permits PendingOrder, PaidOrder, CancelledOrder {}
public record PendingOrder(Long orderId, Long userId, Integer amount) implements OrderDTO {}
public record PaidOrder(Long orderId, Long userId, Integer amount, LocalDateTime paidAt) implements OrderDTO {}
public record CancelledOrder(Long orderId, Long userId, String reason) implements OrderDTO {}
这个设计带来了两个巨大优势。第一,穷尽性变得可验证:当你用`switch`对`OrderDTO`进行模式匹配时,编译器会强制你处理所有可能的子类型,漏掉一个都无法通过编译。第二,字段职责清晰:`PendingOrder`中根本不存在`paidAt`或`reason`,再也不用靠null来判断状态。配合Java 21中的模式匹配switch,代码可以写得像函数式语言一样优雅:
String display(OrderDTO order) {
return switch (order) {
case PendingOrder p -> "待支付,金额:" + p.amount();
case PaidOrder p -> "已支付,时间:" + p.paidAt();
case CancelledOrder p -> "已取消,原因:" + p.reason();
};
}
作为社区开发者,我常在Controller层返回这种分层DTO:基础信息用`BaseOrderDTO`,扩展信息用`OrderDetailDTO extends BaseOrderDTO`——但如果你试过Record继承就会知道,Record之间无法继承,只能通过接口组合。而Sealed接口恰好弥补了这一点。你可以定义:
public sealed interface OrderView permits OrderSummary, OrderDetail {}
让`OrderSummary`只含基础字段,`OrderDetail`在接口层面聚合更多数据。这种方式比类继承更灵活,也更符合组合优于继承的原则。
当然,任何工具都有适用边界。如果你的DTO字段超过十几个,并且需要频繁变更,Record的不可变性反而会成为重构的阻碍——每次修改都要创建新对象。但这恰恰是好信号:也许该思考一下这个DTO是否责任过大了?Sealed类把“可选状态”显式建模后,很多大而全的DTO自然会拆分成小且精确的Record。这不仅是语法层面的简化,更是对领域建模的一次重新梳理。
Java的演进正把开发者从“写冗余代码”推向“表达业务意图”。Record与Sealed类组合,相当于给了我们一套现代、安全、自文档化的DTO设计工具。下次当你又要新建一个DTO时,不妨先停下来想想:这个对象到底该是不可变的Record,还是该用Sealed层级去表达不同的业务分支?答案通常会让你写出更简洁也更健壮的代码。
管理员
黑卡会员