学到类和对象时,课程里的例子通常是学生、汽车和动物。我能照着写字段和 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;数量必须携带单位;解析错误保留原始行;汇总前所有行必须通过校验。之后改代码时,先检查有没有破坏这张表。

那份零件清单程序没有实际投入使用,但它把“类和对象”从语法题变成了我熟悉的现实问题。数据模型不是凭空设计出来的,它是把过去由人脑默默维持的约束,一条条变成程序可以执行的规则。