Ivy Wallet 项目中的数据建模最佳实践
前言
在软件开发中,数据模型的设计直接影响着系统的复杂度和健壮性。本文将结合 Ivy Wallet 项目中的实践经验,深入探讨如何通过合理的数据建模来消除非法状态,提高代码质量。
代数数据类型(ADT)的应用
问题场景
假设我们需要实现一个具有三种状态(加载中、成功、错误)的界面。初学者可能会这样建模:
data class ScreenUiState(
val content: String?,
val loading: Boolean,
val error: String?
)
这种设计存在明显问题:
- 当
loading=false
、content=null
、error=null
时,界面应该显示什么? - 当
loading=true
且error!=null
时,如何处理?
解决方案:使用密封接口
sealed interface ScreenUiState {
data object Loading : ScreenUiState
data class Content(val text: String) : ScreenUiState
data class Error(val msg: String) : ScreenUiState
}
这种设计优势明显:
- 完全消除了非法状态的可能性
- 编译时就能捕获类型错误
- 代码逻辑更加清晰直观
最佳实践
在 Ivy Wallet 项目中,我们建议:
- 使用
sealed interface
表示互斥的状态(和类型) - 使用
data class
表示复合数据结构(积类型) - 可以自由组合这两种类型来精确表达业务领域
显式类型设计
常见问题案例
考虑一个订单模型的初始设计:
data class Order(
val id: UUID,
val userId: UUID,
val itemId: UUID,
val count: Int,
val time: LocalDateTime,
val trackingId: String,
)
这个设计存在多个隐患:
count
可能为0或负数- 不同类型的UUID容易混淆
trackingId
可能为空或包含空格- 时间可能不是UTC格式
改进方案:使用值类和精确类型
data class Order(
val id: OrderId,
val user: UserId,
val item: ItemId,
val count: PositiveInt,
val time: Instant,
val trackingId: NotBlankTrimmedString
)
// 定义各种值类
@JvmInline
value class OrderId(val value: UUID)
@JvmInline
value class UserId(val value: UUID)
@JvmInline
value class PositiveInt private constructor(val value: Int) {
companion object : Exact<Int, PositiveInt> {
override val exactName = "PositiveInt"
override fun Raise<String>.spec(raw: Int): PositiveInt {
ensure(raw > 0) { "$raw is not > 0" }
ensure(raw.isFinite()) { "Is not a finite number" }
return PositiveInt(raw)
}
}
}
改进后的优势
- 编译时类型安全:不同类型的ID不能互相混淆
- 运行时验证:
PositiveInt
确保数值有效性 - 明确语义:
Instant
强制使用UTC时间 - 数据清洁:
NotBlankTrimmedString
自动处理字符串
实践建议
在 Ivy Wallet 项目中,我们遵循以下原则:
- 领域层严格:在核心业务逻辑中使用精确类型
- 边界层灵活:在DTO和实体层可以使用原始类型
- 尽早验证:在数据进入系统时就进行验证
- 类型即文档:通过类型系统表达业务约束
总结
良好的数据建模是构建健壮应用的基础。通过合理使用代数数据类型和显式类型设计,可以:
- 消除大量非法状态
- 减少运行时错误
- 提高代码可读性
- 简化业务逻辑
Ivy Wallet 项目的实践表明,前期在数据建模上的投入会显著降低后期的维护成本,值得开发者重视。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考