跳转到内容

开发经验

1. 时间获取与时间窗口的封装经验

Section titled “1. 时间获取与时间窗口的封装经验”

在许多涉及时间窗口(如优惠券有效期校验、定时报表生成、速率限制等)的开发任务中,直接调用系统时间是一个常见的反模式:

# ❌ 不良实践:核心业务逻辑直接绑定物理系统时间,导致测试困难
def is_coupon_valid(coupon):
now = datetime.now() # 无法在单元测试中轻易 Mock
return coupon.start_time <= now <= coupon.end_time
  • 单元测试困难: 测试用例必须依赖于当前的物理时间执行,无法稳定地验证“已过期”、“未开始”等边界时间窗口逻辑。
  • 不支持手动偏移: 在测试环境下,如果想人为将时间回拨或快进以调试某项特定任务,必须修改服务器的物理时钟。

1.2. 解决方案:抽象时间提供者 (Time Provider)

Section titled “1.2. 解决方案:抽象时间提供者 (Time Provider)”

通过将获取当前时间的行为封装为一个可配置的函数:

import datetime
# 统一封装的时间获取函数
_time_provider = datetime.datetime.now
def get_current_time() -> datetime.datetime:
"""获取当前系统时间或设定的模拟时间"""
return _time_provider()
def set_mock_time(mock_now: datetime.datetime):
"""手动设定模拟的时间起点"""
global _time_provider
_time_provider = lambda: mock_now
def reset_time_provider():
"""恢复默认的物理系统时钟"""
global _time_provider
_time_provider = datetime.datetime.now

在 Go 中,推荐定义 Clock 接口以便于在 Service 结构体中进行 Mock 注入:

package timeutil
import "time"
// Clock 定义了时间接口,用于在业务层替换物理时间获取
type Clock interface {
Now() time.Time
}
// realClock 实际的物理系统时钟
type realClock struct{}
func (realClock) Now() time.Time {
return time.Now()
}
// MockClock 专门用于测试的模拟时钟
type MockClock struct {
CurrentTime time.Time
}
func (m MockClock) Now() time.Time {
return m.CurrentTime
}

在业务结构体中使用:

type OrderService struct {
Clock timeutil.Clock // 依赖注入时钟接口
}
func (s *OrderService) CheckTimeout(order *Order) bool {
// 统一通过注入的 Clock 接口获取时间,方便在单元测试中注入 MockClock
return s.Clock.Now().Sub(order.CreateTime) > 30*time.Minute
}