Java 中应使用 enum 还是 int 表示付款频率?
结论
这种场景更适合使用 enum。
付款频率不是任意整数,而是一组有限且含义明确的选项:月付、季付、半年付和年付。enum 可以从类型层面限定合法值,避免将 3、5、10 等无效或含义不清的数字传入业务代码,也能为每种频率保存“每年付款次数”等属性。
推荐实现
public enum PaymentFrequency {
MONTHLY(12),
QUARTERLY(4),
SEMIANNUALLY(2),
ANNUALLY(1);
private final int paymentsPerYear;
PaymentFrequency(int paymentsPerYear) {
this.paymentsPerYear = paymentsPerYear;
}
public int getPaymentsPerYear() {
return paymentsPerYear;
}
}
在 Insurance 中直接使用这个类型:
import java.util.Objects;
public abstract class Insurance {
private int yearlyPremium;
private PaymentFrequency paymentFrequency;
protected Insurance(
int yearlyPremium,
PaymentFrequency paymentFrequency) {
this.yearlyPremium = yearlyPremium;
this.paymentFrequency =
Objects.requireNonNull(paymentFrequency);
}
public int getPaymentAmount() {
return yearlyPremium
/ paymentFrequency.getPaymentsPerYear();
}
public PaymentFrequency getPaymentFrequency() {
return paymentFrequency;
}
}
如果保费涉及货币和小数,就不应依赖整数除法。通常应使用 BigDecimal,并明确指定舍入规则:
import java.math.BigDecimal;
import java.math.RoundingMode;
public BigDecimal getPaymentAmount() {
return yearlyPremium.divide(
BigDecimal.valueOf(paymentFrequency.getPaymentsPerYear()),
2,
RoundingMode.HALF_UP
);
}
这段代码假设 yearlyPremium 已改为 BigDecimal。实际项目中,精度和舍入方式应根据币种的最小单位及业务规则确定。
为什么不建议直接使用 int
使用 int 时,方法签名无法说明参数的业务含义:
insurance.setPaymentFrequency(4);
单看这段代码,无法确定 4 是指每年支付四次、每四个月支付一次,还是某个内部编号。由于调用者可以传入任意整数,每个入口都可能需要进行验证:
private static boolean isValidFrequency(int paymentsPerYear) {
return paymentsPerYear >= 1
&& paymentsPerYear <= 12
&& 12 % paymentsPerYear == 0;
}
改用枚举后,调用方式会清楚得多:
insurance.setPaymentFrequency(PaymentFrequency.QUARTERLY);
调用者无法传入枚举定义之外的付款频率,类型系统也能承担一部分校验工作,后续重构会更安全。
另外,12 % n == 0 只能判断数学上是否合法,不能完整表达业务规则。例如,它会允许 n == 3 和 n == 6,但当前枚举并未定义每年三次或每年六次付款。对于这种由业务预先确定的固定集合,枚举表达得更准确。
界面显示名称的处理
使用 PaymentFrequency.values() 生成下拉选项是合理的:
for (PaymentFrequency frequency : PaymentFrequency.values()) {
System.out.println(
frequency.name() + ": "
+ frequency.getPaymentsPerYear()
);
}
不过,"Monthly"、"Quarterly" 这类面向用户的文本不宜直接作为核心领域数据。显示文本可能需要翻译,也可能由产品配置决定。可以在枚举中保存稳定的消息键:
public enum PaymentFrequency {
MONTHLY("payment.frequency.monthly", 12),
QUARTERLY("payment.frequency.quarterly", 4),
SEMIANNUALLY("payment.frequency.semiannually", 2),
ANNUALLY("payment.frequency.annually", 1);
private final String messageKey;
private final int paymentsPerYear;
PaymentFrequency(String messageKey, int paymentsPerYear) {
this.messageKey = messageKey;
this.paymentsPerYear = paymentsPerYear;
}
public String getMessageKey() {
return messageKey;
}
public int getPaymentsPerYear() {
return paymentsPerYear;
}
}
界面层再通过 messageKey 从资源文件中读取本地化文本。如果应用确定只有一种语言,也可以直接保存显示名称,只是以后扩展会稍受限制。
从数字转换为枚举
数据库、配置文件或外部接口可能仍以每年付款次数表示频率。可以将转换和校验集中到枚举中:
import java.util.Arrays;
public enum PaymentFrequency {
MONTHLY(12),
QUARTERLY(4),
SEMIANNUALLY(2),
ANNUALLY(1);
private final int paymentsPerYear;
PaymentFrequency(int paymentsPerYear) {
this.paymentsPerYear = paymentsPerYear;
}
public int getPaymentsPerYear() {
return paymentsPerYear;
}
public static PaymentFrequency fromPaymentsPerYear(int value) {
return Arrays.stream(values())
.filter(frequency ->
frequency.paymentsPerYear == value)
.findFirst()
.orElseThrow(() ->
new IllegalArgumentException(
"Unsupported payment frequency: " + value
));
}
}
这样只需在系统边界处处理一次非法数字,领域模型内部始终使用合法的 PaymentFrequency。
注意事项
- 枚举字段通常应声明为
private final,防止枚举常量创建后其属性被修改。 - 不要使用
ordinal()保存数据库值。调整枚举顺序会改变序号,应保存稳定名称或显式代码。 values()按声明顺序返回常量。如果界面顺序与业务定义顺序不同,应由界面层单独排序。- 如果将来允许任意周期、用户自定义周期或“每 45 天一次”这类复杂日程,固定枚举可能无法满足需求,此时应改用专门的值对象或计划模型。
- 新增枚举常量后,它虽然会自动出现在动态下拉框中,但数据库、序列化、报表和
switch分支仍需同步检查。
只要允许的付款频率仍是由业务预先定义的有限集合,使用 enum 就不是过度设计,而是比裸 int 更准确的领域建模。
备注:内容仅供参考。
