10-6.【安全性与稳定性】给出一个防御式编程的实例,说明如何保护模块边界和 API 调用安全。

3 阅读2分钟

在大型 Swift 项目中,模块边界(Module Boundary) 是最脆弱的地方。防御式编程的目标是在数据进入核心业务逻辑之前,像“海关检查”一样对其进行严格的校验、清洗和转换。

下面通过一个用户档案更新的场景,展示如何构建多层防御体系。


1. 第一层防御:契约式 API (The Contract)

不要直接传递 StringInt,使用强类型和 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
    }
}

实践总结:防御式编程检查单

  1. 不信任外部 String:永远进行 trimming 和长度校验。
  2. 不使用强制解包:在边界处,! 是绝对的禁令。
  3. 失败显式化:用 Error 枚举代替 nil
  4. 开发期断言:使用 assertionFailure 拦截逻辑上的“不可能”情况,但在 Release 中静默失败或优雅降级。
  5. 单向修改权:利用访问控制符屏蔽外部的随意赋值。