十七、重构SerialDate

82 阅读3分钟

重构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. 前端日期处理黄金法则

  1. 永不信任原生Date:使用date-fns/day.js等库
  2. 时区显式化:所有时间戳带时区标识(如2023-01-01T00:00Z
  3. 不可变性:每次操作返回新对象
    // 正确示例: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; // 自动校正
      }
    }
    

重构者的觉悟

  1. 历史代码尊重原则:保留原有测试用例作为"安全网"
  2. 小步快跑:每次提交只解决一类问题
  3. 领域语言:方法命名如isLeapYear()checkYearFlag()更准确

"处理日期时间的代码应该像瑞士钟表一样精密,像联合国翻译一样严谨。" —— 每个被日期bug折磨过的开发者