首页 / 资讯中心 / 文章详情

HoRain云--Java 17 密封类型实战:用 sealed interface 建模业务结果

HoRain云--Java 17 密封类型实战:用 sealed interface 建模业务结果 ★ FEATURED ARTICLE
调用报价方法可能得到价格也可能因为库存不足无法报价还可能因为输入数量非法而被拒绝。若方法只返回null或一个负数调用方很难区分原因若所有情况都抛异常又会把正常的业务分支变成故障。Java 17 的密封类型可以限定一个结果家族允许哪些子类型让状态在类型层面可见。用三种结果表达三种状态下面的QuoteResult只允许Available、SoldOut和InvalidRequest三个实现。示例使用record保存每种状态需要的数据避免靠固定字符串和魔法数字猜测结果。为保持 Java 17 普通编译方式分支使用instanceof模式匹配而不是依赖预览版的模式switch。publicclassSealedResultDemo{sealedinterfaceQuoteResultpermitsAvailable,SoldOut,InvalidRequest{}recordAvailable(longtotalCents)implementsQuoteResult{}recordSoldOut(intrequested,intremaining)implementsQuoteResult{}recordInvalidRequest(Stringreason)implementsQuoteResult{}staticQuoteResultquote(intquantity,intstock){if(quantity0){returnnewInvalidRequest(数量必须大于零);}if(stockquantity){returnnewSoldOut(quantity,stock);}returnnewAvailable(quantity*199L);}staticStringdescribe(QuoteResultresult){if(resultinstanceofAvailableavailable){return应付金额: available.totalCents() 分;}if(resultinstanceofSoldOutsoldOut){return库存不足: 需要 soldOut.requested() 件剩余 soldOut.remaining() 件;}if(resultinstanceofInvalidRequestinvalid){return参数错误: invalid.reason();}thrownewIllegalStateException(未处理的报价结果);}publicstaticvoidmain(String[]args){System.out.println(describe(quote(2,10)));System.out.println(describe(quote(20,10)));System.out.println(describe(quote(0,10)));}}正常报价输出 398 分库存不足返回请求数和剩余数数量为零则返回明确的参数错误。示例中的单价是固定演示值真实报价还涉及价格版本、优惠、税费和库存时效。返回Available也不代表库存已经被锁定后续下单仍需要自己的库存并发控制。sealed到底限制了什么普通接口允许其他代码新增任意实现密封接口列出许可的直接实现类编译器会检查继承关系。许可的子类型还须明确为final、sealed或non-sealedrecord天生不可继承适合做这里的终止分支。这样设计适合状态集合由当前模块拥有且相对稳定的领域模型不适合需要外部插件任意扩展的开放接口。与enum相比密封类型的每个分支可以携带不同数据成功包含金额售罄包含请求数和库存非法输入包含原因。enum更适合只有少量固定名称、附带数据形状基本一致的状态。与“状态码加一堆可空字段”的单个 DTO 相比分支类型能避免“成功状态却填了错误原因”“失败状态却意外有价格”这类无效组合。为什么还要最后的throwJava 17 中普通的if instanceof链不会因为接口密封就自动得到穷尽性编译检查。将来新增第四种结果如果忘了更新describe最后的异常可以暴露遗漏而不是静默返回空字符串。这里的throw不表示业务有第四种正常状态只是守护分支处理的完整性。较新语言版本的模式switch语法有所演进但本文刻意保持 Java 17 默认编译即可运行。新增子类型本身还可能影响已有调用方。内部封闭模块里编译错误或测试失败能推动各处更新发布给其他团队的公共库则要考虑二进制兼容、序列化协议和版本演进。密封层次让扩展受控不意味着修改永远没有兼容成本。将结果转换成 HTTP 响应时也要决定新状态如何映射为状态码和消息。结果类型与异常各管什么库存不足往往是预期内、调用方可以展示给用户的业务分支适合显式结果。数据库不可用、网络超时等系统故障不应伪装成SoldOut可以沿异常路径传播或者在有清晰恢复策略时定义单独的失败结果。关键在于业务“正常但未成功”和系统“无法完成查询”是否被区分而不是规定所有方法都只能用某一种机制。测试可分别构造三种结果断言金额、库存数据和提示文本再针对边界值测试数量为零、负数、恰好等于库存以及大数量。特别注意示例quantity * 199L先把单价提升为long避免以int计算后才赋给long的溢出问题。若价格与数量本身可能逼近long上限仍需校验范围或使用精确数值方案。从结果模型走到接口契约内部代码有三个子类型对外 JSON 协议不一定能直接把 Java 类型名当成字段。更稳妥的设计是稳定的判别字段例如kind为available、sold_out、invalid_request再按状态返回相应数据。序列化和反序列化框架是否支持这个层次要以项目所用版本和配置为准不能假定实现sealed interface后网络协议自动具有穷尽性。前端遇到未知kind时应有明确兜底老客户端不能因为服务器新增状态就显示一个看似成功的空页面。发布公共 API 时可在协议层预留未知状态的处理策略同时在服务内部继续利用密封类型约束已知分支。内部闭合与外部兼容是两个目标不能把它们混作同一个保证。还有一种反例是把每一个数据库错误、参数错误、网络错误都做成同一个巨大密封树。结果类型分支过多时调用方需要处理大量与自身无关的情况代码反而难读。应围绕某个明确用例定义局部结果家族跨业务共享的系统故障可走统一异常与错误响应机制。类型建模要让“不可能状态”难以表达也要让最常见路径容易使用。若需要对每种结果执行相同操作例如记录审计信息可以考虑在接口上定义公共行为但不要为了避免三段instanceof就给所有分支塞入不存在的字段。示例以模式匹配展示分支处理实际项目可把文本展示留在接口层领域层只携带稳定数据。这样国际化、渠道文案变化时不必修改报价核心逻辑。最后检查状态之间是否真的互斥。库存为负数是“售罄”还是数据异常价格为零是否仍是成功这些边界必须由领域规则定义。密封类型能精确表达选定的状态集合却不能替团队决定状态集合本身。把状态表、输入边界和转换规则写进测试比只验证三个record能创建更重要。若以后要为成功报价增加币种、过期时间或价格来源可以在Available中扩展明确字段并评估现有构造调用和序列化契约。不要为了暂时少改代码放一个含义模糊的MapString, Object作为“扩展字段”那会放弃刚建立的类型约束。模型演进应围绕真实业务概念而不是无限增加可空占位字段。总结当业务结果是有限且每种状态携带不同数据的集合密封类型配合记录类能使状态更清晰。它限制实现范围却不替你解决业务并发、异常边界和接口演进。参考Java 语言规范第 8 章Oracle Java 17 关于 sealed classes 与 records 的语言文档。
阅读完成 · 觉得有帮助?
咨询建站