示例代码均为匿名化改写,用于说明模式;服务端仍需重新校验业务规则与最终金额。
先准备表单快照,再生成请求
创建页面输入较多:项目、目标地点、日期、时段、数量、备注、关联内容和附加费用。若必填条件散落在多个按钮回调中,很容易因为入口变化而遗漏校验。
一种简单做法是先把表单当前值收集为不可变输入,再交给独立请求构造器。缺少项目、地点或计划时段时返回明确错误;若某种创建需要附加费用,费用仍在加载或缺失时就不生成请求。备注长度限制也放在提交边界,避免其他入口绕过。
struct CreationInput {
let itemID: UUID?
let placeID: UUID?
let startsAt: Date?
let note: String
let requiresExtraFee: Bool
let extraFeeCents: Int?
}
enum CreationValidationError: Error {
case missingItem, missingPlace, missingStartTime, feeUnavailable
case noteTooLong, invalidAmount, amountOverflow
}
func makeValidatedInput(_ input: CreationInput) throws -> CreationInput {
guard input.itemID != nil else { throw CreationValidationError.missingItem }
guard input.placeID != nil else { throw CreationValidationError.missingPlace }
guard input.startsAt != nil else { throw CreationValidationError.missingStartTime }
guard input.note.count <= 300 else { throw CreationValidationError.noteTooLong }
if input.requiresExtraFee, input.extraFeeCents == nil {
throw CreationValidationError.feeUnavailable
}
return input
}
func previewTotalCents(
unitPriceCents: Int,
quantity: Int,
extraFeeCents: Int
) throws -> Int {
guard unitPriceCents >= 0, quantity >= 0, extraFeeCents >= 0 else {
throw CreationValidationError.invalidAmount
}
let subtotal = unitPriceCents.multipliedReportingOverflow(by: quantity)
guard !subtotal.overflow else { throw CreationValidationError.amountOverflow }
let total = subtotal.partialValue.addingReportingOverflow(extraFeeCents)
guard !total.overflow else { throw CreationValidationError.amountOverflow }
return total.partialValue
}
传输格式和最终金额分开看
金额使用固定小数格式可以避免设备区域设置改变小数点的传输字符,但不能消除二进制浮点误差,也不能使客户端金额变得可信。客户端负责预览;服务端应根据当前价格、数量、附加费用和业务规则重新计算并返回权威金额。
不同创建类型需要的字段可能不同。选择某一类创建时,应明确省略另一类专属参数,避免把默认值误编码成有效业务数据。
金额传输用整数分可避开二进制小数展示与序列化差异;乘法和加法都要检查溢出。客户端金额仅用于预览,应清楚标为估算值,并以服务端返回的最终金额为准。
纯构造器更容易测
请求构造器无需访问 SwiftUI、定位权限或网络,因此可以直接检查缺失字段、附加费用未准备好、输入长度和金额格式。页面模型再通过可替换依赖验证提交结果与账号变化的交互。
这样的边界让测试覆盖业务合同,而不只是检查按钮是否调用了某个方法。
