在大型 Swift 项目中,模块边界(Module Boundary) 是最脆弱的地方。防御式编程的目标是在数据进入核心业务逻辑之前,像“海关检查”一样对其进行严格的校验、清洗和转换。
下面通过一个用户档案更新的场景,展示如何构建多层防御体系。
1. 第一层防御:契约式 API (The Contract)
不要直接传递 String 或 Int,使用强类型和 Result 模式来定义边界。
Swift
// 明确错误类型,不留模糊地带
enum ProfileError: Error {
case invalidUsername
case underageUser(age: Int)
case networkFailure(reason: String)
}
// 使用 Result 确保调用方必须处理失败分支
typealias ProfileResult = Result<UserProfile, ProfileError>
2. 第二层防御:输入清洗与校验 (The Sanitizer)
在 API 内部,利用 guard 语句和数据清洗技术,防止非法数据向内渗透。
Swift
struct ProfileService {
func updateProfile(username: String, age: Int) -> ProfileResult {
// 防御点 1: 清洗数据(去除空格、非法字符)
let sanitizedName = username.trimmingCharacters(in: .whitespacesAndNewlines)
// 防御点 2: 显式校验业务规则
guard sanitizedName.count >= 3 else {
return .failure(.invalidUsername)
}
guard age >= 18 else {
// 失败时提供具体的上下文数据,便于调试
return .failure(.underageUser(age: age))
}
// 只有通过了所有海关检查,才能进入核心逻辑
return .success(UserProfile(name: sanitizedName, age: age))
}
}
3. 第三层防御:模型安全转换 (The Mapper)
当处理网络返回的 JSON 数据时,利用 Decodable 的初始化方法进行防御,而不是事后解包。
Swift
struct UserDTO: Decodable {
let id: Int
let email: String
// 在解析阶段就拦截错误数据
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let rawEmail = try container.decode(String.self, forKey: .email)
// 防御点 3: 正则匹配校验
if !rawEmail.contains("@") {
throw DecodingError.dataCorruptedError(forKey: .email,
in: container, debugDescription: "邮件格式非法")
}
self.email = rawEmail
self.id = try container.decode(Int.self, forKey: .id)
}
}
4. 第四层防御:状态锁定 (The Shield)
使用 private(set) 保护内部状态,防止模块外部通过“后门”修改数据。
Swift
public class ProfileManager {
// 外部可读,但不可写,保证了状态的一致性
public private(set) var currentProfile: UserProfile?
// 只有通过这个受控的入口才能改变状态
public func syncProfile(_ profile: UserProfile) {
// 最后的防御:确保不会被旧数据覆盖
guard profile.id == currentProfile?.id || currentProfile == nil else {
assertionFailure("试图更新错误的账户 ID") // 开发环境及时崩溃
return
}
self.currentProfile = profile
}
}
实践总结:防御式编程检查单
- 不信任外部 String:永远进行
trimming和长度校验。 - 不使用强制解包:在边界处,
!是绝对的禁令。 - 失败显式化:用
Error枚举代替nil。 - 开发期断言:使用
assertionFailure拦截逻辑上的“不可能”情况,但在 Release 中静默失败或优雅降级。 - 单向修改权:利用访问控制符屏蔽外部的随意赋值。