功能说明
直接调用 datetime.now 的代码很难测试:验证缓存过期要么真的等待几十秒,要么把过期时间设得极短,导致用例脆弱。把时间抽象成 Clock 协议后,生产代码使用 SystemClock,测试注入 FakeClock 并手动推进时间,过期、续期、边界条件都可以在毫秒内确定性地验证。
示例用带 TTL 的缓存演示这种写法:put 记录写入时间加存活时长,get 比较当前时间与过期时间。真实实现可以替换为令牌缓存、限流窗口、重试退避等依赖时间的组件。
关键代码
Clock 是只含 now 方法的协议,SystemClock 返回带时区的 UTC 时间,FakeClock 保存一个内部时间点并通过 advance 推进。FakeClock 在构造时强制要求带时区,防止本地时间与 UTC 混用。CachedValue 只依赖注入的 clock,不导入任何时间函数。get 在值缺失、未设置过期时间或当前时间已达过期时间时返回 None,注意比较使用大于等于,边界时刻即视为过期。
使用注意
所有时间比较都应基于同一时区,推荐统一使用带时区的 UTC。FakeClock 的 advance 只影响自身,不会影响其他组件,这是隔离性也是局限:跨组件协同时必须共享同一个时钟实例。不要让业务代码直接读取系统时间,否则注入失效。序列化输出时间时再转换成本地时区或指定格式,内部保持 UTC。多线程共享 FakeClock 时推进操作需要加锁。
验证方式
起始时间设为 2026 年 10 月 21 日十二点,写入 fresh 后立即可读到。推进 29 秒仍在 30 秒有效期内,返回 fresh;再推进 2 秒后达到 31 秒,get 返回 None。最后一行打印系统时钟的时区为 UTC。整个验证不需要任何真实等待,可以在单元测试中稳定复现。
代码示例
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Protocol
class Clock(Protocol):
def now(self) -> datetime: ...
class SystemClock:
def now(self) -> datetime:
return datetime.now(timezone.utc)
class FakeClock:
def __init__(self, start: datetime) -> None:
if start.tzinfo is None:
raise ValueError("start must be timezone aware")
self._current = start
def now(self) -> datetime:
return self._current
def advance(self, seconds: float) -> None:
self._current += timedelta(seconds=seconds)
@dataclass
class CachedValue:
clock: Clock
ttl: timedelta
value: str | None = None
expires_at: datetime | None = None
def get(self) -> str | None:
now = self.clock.now()
if self.value is None or self.expires_at is None or now >= self.expires_at:
return None
return self.value
def put(self, value: str) -> None:
self.value = value
self.expires_at = self.clock.now() + self.ttl
def main() -> None:
clock = FakeClock(datetime(2026, 10, 21, 12, 0, tzinfo=timezone.utc))
cache = CachedValue(clock=clock, ttl=timedelta(seconds=30))
cache.put("fresh")
print("immediately:", cache.get())
clock.advance(29)
print("after 29s:", cache.get())
clock.advance(2)
print("after 31s:", cache.get())
print("system clock tz:", SystemClock().now().tzinfo)
if __name__ == "__main__":
main()