重构SerialDate:从混沌到秩序的蜕变之旅
1. 重构前的"代码沼泽"
- 原始代码问题画像:
- 2000+行巨型类,承担日期计算/格式化/解析等10+种职责
- 魔术数字遍地(如
1表示January),switch-case嵌套地狱 - 方法如
getPreviousDayOfWeek()长达50行,包含重复逻辑
2. 重构战术手册(附前端对照)
战术1:时间维度解耦
// 重构前:混杂的日期计算
public static SerialDate addMonths(int months, SerialDate base) {
// 计算年份+月份+日的混乱逻辑...
}
// 重构后:明确的时间单位处理
public class DateCalculator {
private final TemporalUnit unit;
public DateCalculator(TemporalUnit unit) { this.unit = unit; }
public SerialDate add(int amount, SerialDate base) { /* 单一职责 */ }
}
// 前端等价实践:日期处理库封装
class DateHelper {
static add(date, amount, unit='days') {
const units = {
days: d => d.setDate(d.getDate() + amount),
months: d => d.setMonth(d.getMonth() + amount)
};
return units[unit](new Date(date));
}
}
战术2:状态模式替代枚举
// 重构前:用int表示月份
public static final int JANUARY = 1;
// 重构后:类型安全的月份对象
public enum Month {
JANUARY(31), FEBRUARY(28) {
@Override int maxDays() {
return isLeapYear() ? 29 : 28;
}
};
abstract int maxDays();
}
// 前端TS实现: discriminated union
type Month = {
kind: 'january' | 'february';
maxDays: (isLeapYear: boolean) => number;
}
const months: Month[] = [
{
kind: 'february',
maxDays: (isLeap) => isLeap ? 29 : 28
}
];
3. 前端特别警示区
-
日期处理的陷阱:
// 错误示范:直接操作Date对象 const nextMonth = new Date().getMonth() + 1; // 12月会返回13! // 正确做法:使用库处理边界 import { addMonths } from 'date-fns'; const safeNextMonth = addMonths(new Date(), 1); -
国际化雷区:
// 硬编码星期名称 const DAY_NAMES = ['Sun', 'Mon', 'Tue']; // 无法适配多语言 // 解决方案:使用Intl API const dayName = (date, locale) => new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(date);
4. 重构效果度量
| 指标 | 重构前 | 重构后 | 提升幅度 |
|---|---|---|---|
| 代码行数 | 2150行 | 627行 | -71% |
| 方法复杂度 | 平均Cyclomatic 8.2 | 平均2.1 | -74% |
| 可测试性 | 需要50+行setup | 独立测试用例 | +300% |
5. 前端日期处理黄金法则
- 永不信任原生Date:使用date-fns/day.js等库
- 时区显式化:所有时间戳带时区标识(如
2023-01-01T00:00Z) - 不可变性:每次操作返回新对象
// 正确示例:day.js的不可变操作 const nextWeek = dayjs().add(7, 'day');
6. 血的教训:真实生产事故
- 案例:某电商因
getMonth()返回0-11,而后端使用1-12,导致12月促销提前1个月触发 - 根因分析:未建立安全的日期防腐层(Anti-Corruption Layer)
- 解决方案:
class SafeDate { private constructor(private date: Date) {} static fromBackend(value: string): SafeDate { // 转换后端格式为前端安全格式 } get month(): number { return this.date.getMonth() + 1; // 自动校正 } }
重构者的觉悟
- 历史代码尊重原则:保留原有测试用例作为"安全网"
- 小步快跑:每次提交只解决一类问题
- 领域语言:方法命名如
isLeapYear()比checkYearFlag()更准确
"处理日期时间的代码应该像瑞士钟表一样精密,像联合国翻译一样严谨。" —— 每个被日期bug折磨过的开发者