在业务系统开发中,处理时间区间是十分常见的需求,不同模块、不同第三方库往往会定义各自的时间区间对象,比如有的用开始时间和结束时间的毫秒时间戳表示,有的用日期对象组合表示,还有的用自定义的Period结构体存储。当这些不同结构的时间区间对象需要在系统间流转时,就需要统一的转换逻辑,避免为每个类型单独编写转换方法带来的代码冗余问题。

定义通用时间区间接口
首先我们需要抽象出所有时间区间对象共有的行为,定义一个通用的接口,让所有需要参与转换的时间区间类型都实现这个接口,这样后续就可以通过接口类型来统一处理不同的时间区间对象。
以Go语言为例,我们可以定义如下的接口:
// TimeRange 通用时间区间接口,定义所有时间区间对象的公共方法
type TimeRange interface {
// GetStart 返回时间区间的开始时间,返回时间戳(毫秒)
GetStart() int64
// GetEnd 返回时间区间的结束时间,返回时间戳(毫秒)
GetEnd() int64
}
然后我们可以为不同类型的时间区间对象实现这个接口,比如自定义的结构体和第三方库的结构体:
import "time"
// CustomTimeRange 自定义时间区间结构体
type CustomTimeRange struct {
StartTime time.Time
EndTime time.Time
}
// GetStart 实现TimeRange接口的GetStart方法
func (c CustomTimeRange) GetStart() int64 {
return c.StartTime.UnixMilli()
}
// GetEnd 实现TimeRange接口的GetEnd方法
func (c CustomTimeRange) GetEnd() int64 {
return c.EndTime.UnixMilli()
}
// ThirdPartyTimeRange 模拟第三方库的时间区间结构体
type ThirdPartyTimeRange struct {
StartMs int64
EndMs int64
}
// GetStart 实现TimeRange接口的GetStart方法
func (t ThirdPartyTimeRange) GetStart() int64 {
return t.StartMs
}
// GetEnd 实现TimeRange接口的GetEnd方法
func (t ThirdPartyTimeRange) GetEnd() int64 {
return t.EndMs
}
基于模式匹配实现统一转换
接下来我们需要实现统一转换的逻辑,这里可以结合模式匹配(部分语言支持原生模式匹配,不支持的语言可以通过类型断言模拟模式匹配的效果),针对不同的目标类型做对应的转换处理。
假设我们需要把所有实现了TimeRange接口的对象转换为系统内部的标准时间区间结构体StandardRange:
// StandardRange 系统内部标准时间区间结构体
type StandardRange struct {
Start int64
End int64
}
// ConvertToStandard 统一转换方法,接收TimeRange接口类型,返回StandardRange
// 这里通过类型断言模拟模式匹配的效果
func ConvertToStandard(tr TimeRange) StandardRange {
// 尝试匹配不同的原始类型,做特殊处理(如果有需要的话)
switch v := tr.(type) {
case CustomTimeRange:
// 针对自定义时间区间的特殊处理,比如可以额外校验时间合法性
if v.EndTime.Before(v.StartTime) {
// 结束时间早于开始时间,做修正处理
return StandardRange{
Start: v.EndTime.UnixMilli(),
End: v.StartTime.UnixMilli(),
}
}
return StandardRange{
Start: v.GetStart(),
End: v.GetEnd(),
}
case ThirdPartyTimeRange:
// 针对第三方时间区间的特殊处理,比如校验时间戳是否为有效值
if v.StartMs <= 0 || v.EndMs <= 0 {
return StandardRange{Start: 0, End: 0}
}
return StandardRange{
Start: v.GetStart(),
End: v.GetEnd(),
}
default:
// 其他实现了TimeRange接口的类型,通用转换逻辑
return StandardRange{
Start: tr.GetStart(),
End: tr.GetEnd(),
}
}
}
使用示例
我们可以编写测试代码验证转换逻辑是否正确:
package main
import (
"fmt"
"time"
)
func main() {
// 创建自定义时间区间对象
customRange := CustomTimeRange{
StartTime: time.Now(),
EndTime: time.Now().Add(24 * time.Hour),
}
standard1 := ConvertToStandard(customRange)
fmt.Printf("自定义时间区间转换结果:start=%d, end=%dn", standard1.Start, standard1.End)
// 创建第三方时间区间对象
thirdPartyRange := ThirdPartyTimeRange{
StartMs: time.Now().UnixMilli(),
EndMs: time.Now().Add(12 * time.Hour).UnixMilli(),
}
standard2 := ConvertToStandard(thirdPartyRange)
fmt.Printf("第三方时间区间转换结果:start=%d, end=%dn", standard2.Start, standard2.End)
// 测试结束时间早于开始时间的场景
errorRange := CustomTimeRange{
StartTime: time.Now().Add(24 * time.Hour),
EndTime: time.Now(),
}
standard3 := ConvertToStandard(errorRange)
fmt.Printf("异常时间区间转换结果:start=%d, end=%dn", standard3.Start, standard3.End)
}
方案优势
这种实现方式的核心优势在于:
- 复用性高:所有实现了通用接口的时间区间对象都可以使用统一的转换入口,不需要为每个类型单独编写转换函数。
- 扩展性强:后续新增时间区间类型时,只需要让新类型实现
TimeRange接口,再在转换逻辑中补充对应的类型处理分支即可,不需要修改原有转换逻辑的核心结构。 - 逻辑清晰:通过接口抽象和模式匹配,转换逻辑的分层明确,不同类型的特殊处理可以单独维护,降低代码的耦合度。
注意事项
在实际使用中需要注意几个问题:
- 接口定义要足够通用,不要包含只属于某几个类型的特殊方法,避免其他类型实现接口时产生冗余代码。
- 模式匹配的分支顺序要合理,如果有父子类型的匹配场景,需要把子类型的分支放在前面,避免被父类型的分支覆盖。
- 如果时间区间对象是可变的,需要注意转换过程中是否会产生副作用,建议转换时只读取原始对象的数据,不要修改原始对象的状态。