学到类和对象时,课程里的例子通常是学生、汽车和动物。我能照着写字段和 getter,却不明白为什么一定要这样组织。机械专业的课程里刚好有一份零件明细表:零件编号、名称、材料、数量、单价。于是我决定不用“学生类”,而是把一张真实的清单写成程序。
第一版很快完成:创建几个 Part,放进数组,循环计算总价。它也很快暴露问题。数量能被写成负数;同一个编号可以出现两次;单价使用 double 后出现 19.9900000002;有的零件按“个”,有的按“米”,我却把数量都当成整数。
图 1:程序不是把表格列搬成字段,还要把表格默认依赖的人类常识写成规则。
先写不会轻易变化的事实
零件编号是标识,不应该被随意修改;名称不能为空;金额需要十进制精度。相比一堆可写字段,我更愿意让对象在创建时就合法:
import java.math.BigDecimal;
import java.util.Objects;
final class Part {
private final String code;
private final String name;
private final BigDecimal unitPrice;
Part(String code, String name, BigDecimal unitPrice) {
this.code = requireText(code, "code");
this.name = requireText(name, "name");
if (unitPrice.signum() < 0) {
throw new IllegalArgumentException("unitPrice must be >= 0");
}
this.unitPrice = unitPrice;
}
private static String requireText(String value, String field) {
String normalized = Objects.requireNonNull(value, field).trim();
if (normalized.isEmpty()) throw new IllegalArgumentException(field + " is empty");
return normalized;
}
}那时我第一次理解“封装”不是把字段改成 private 就结束了。它真正保护的是对象不变量:一个 Part 一旦存在,就至少拥有合法编号、名称和非负价格。
数量不是天然的 int
机械清单里的数量看似简单,实际依赖单位。螺钉是 12 个,密封条可能是 1.8 米,润滑剂可能是 0.5 千克。如果模型只有 int count,程序会悄悄丢掉现实信息。
enum Unit { PIECE, METER, KILOGRAM }
record Quantity(BigDecimal value, Unit unit) {
Quantity {
if (value.signum() <= 0) {
throw new IllegalArgumentException("quantity must be > 0");
}
}
}
record LineItem(Part part, Quantity quantity) {
BigDecimal amount() {
return part.unitPrice().multiply(quantity.value());
}
}这里还有一个没有被代码解决的问题:每件单价不能直接乘“米”。价格也需要单位维度。2016 年我没有继续做完整的单位系统,但把这个缺口明确写下来,比假装模型已经正确更重要。
重复编号是集合规则,不是零件自己的规则
单个零件无法判断自己的编号是否与别的零件冲突。这个约束属于清单:
final class BillOfMaterials {
private final Map<String, LineItem> items = new LinkedHashMap<>();
void add(String code, LineItem item) {
if (items.putIfAbsent(code, item) != null) {
throw new IllegalArgumentException("duplicate part code: " + code);
}
}
}这段代码让我第一次看到“边界”来自规则的归属。编号格式属于零件,编号唯一属于清单,金额汇总属于报表。把所有方法都塞进 Part,类会越来越像一个什么都知道的工具箱。
错误需要能指回原始行
我最初遇到非法数据就抛异常,最后只能看到“价格不能为负”,却不知道 CSV 的哪一行出了问题。后来给解析结果保留行号和原始文本:
| 行号 | 字段 | 原值 | 错误 |
|---|---|---|---|
| 12 | unitPrice |
-3.2 |
单价不能为负 |
| 19 | code |
A-017 |
编号重复,首次出现在第 4 行 |
| 26 | quantity |
1.5 |
PIECE 单位必须为整数 |
批量数据处理不能只告诉用户“失败”。一份可修复的错误报告,应该指出位置、规则和冲突对象。这个认识后来直接影响了我做配置校验、数据导入和 AI 工具参数验证的方式。
最后留下的是一张规则表
程序写完后,我把约束单独列出来:编号唯一且不可变;金额使用 BigDecimal;数量必须携带单位;解析错误保留原始行;汇总前所有行必须通过校验。之后改代码时,先检查有没有破坏这张表。
那份零件清单程序没有实际投入使用,但它把“类和对象”从语法题变成了我熟悉的现实问题。数据模型不是凭空设计出来的,它是把过去由人脑默默维持的约束,一条条变成程序可以执行的规则。