防御性编程理论笔记
1. 防御性编程概述
1.1 定义
防御性编程是一种软件开发方法,旨在通过预见和处理潜在问题来提高软件的健壮性和可靠性。其核心思想是”不信任”任何外部输入、依赖或环境条件,包括来自用户、其他系统、甚至程序员自身的数据和调用。
1.2 基本原则
- 永远不要假设输入是有效的
- 永远不要假设环境是稳定的
- 永远不要假设代码不会被误用
- 所有错误都应该被明确处理
- 代码应该清晰表达其意图和约束
1.3 主要目标
- 防止软件在异常情况下崩溃
- 减少漏洞和安全风险
- 提高代码的可维护性
- 使错误更容易诊断和修复
2. 输入验证
2.1 输入源分类
- 用户输入:表单、命令行、GUI等
- 系统输入:文件、网络、API响应等
- 程序间输入:函数参数、方法调用等
2.2 验证策略
- 白名单验证:只允许已知有效的输入
- 黑名单验证:拒绝已知无效的输入(不推荐作为主要方法)
- 范围检查:数值、长度等限制
- 类型检查:确保输入符合预期类型
- 格式验证:正则表达式等模式匹配
- 业务逻辑验证:检查输入在业务上下文中的有效性
2.3 验证时机
- 尽早验证:在数据进入系统时立即验证
- 多次验证:在不同层次重复验证
- 边界验证:特别关注系统边界处的验证
3. 错误处理
3.1 错误处理策略
- 错误预防:通过设计避免错误发生
- 错误检测:主动检查错误条件
- 错误恢复:从错误中恢复并继续执行
- 错误报告:向用户或日志提供有意义的信息
3.2 错误传播
- 返回码:使用特定返回值表示错误
- 异常处理:通过异常机制传播错误
- 回调函数:通过错误回调处理
- 全局状态:设置全局错误标志(谨慎使用)
3.3 错误处理最佳实践
- 不要忽略错误
- 提供有意义的错误信息
- 记录错误上下文
- 考虑错误恢复策略
- 区分可恢复错误和不可恢复错误
4. 断言与契约
4.1 断言
- 前置条件:函数开始执行前必须满足的条件
- 后置条件:函数执行后必须满足的条件
- 不变式:在特定点必须始终满足的条件
4.2 设计契约
- 明确接口的期望和责任
- 定义调用方和被调用方的义务
- 使用契约式设计方法
4.3 断言使用指南
- 用于检查不应发生的条件
- 生产环境中可以禁用(但需谨慎)
- 不应替代输入验证
- 断言失败应导致明显失败
5. 资源管理
5.1 资源类型
- 内存
- 文件句柄
- 网络连接
- 数据库连接
- 锁
5.2 资源管理原则
- 获取即初始化(RAII)
- 明确所有权
- 及时释放
- 限制资源使用量
5.3 资源管理策略
- 使用try-finally或类似结构
- 实现自动清理机制
- 设置资源使用上限
- 监控资源泄漏
6. 并发编程防御
6.1 并发风险
- 竞态条件
- 死锁
- 活锁
- 资源争用
6.2 防御策略
- 最小化共享状态
- 使用不可变对象
- 正确使用同步机制
- 避免嵌套锁
- 使用线程安全的数据结构
6.3 并发测试
- 压力测试
- 随机延迟测试
- 静态分析工具
7. 安全考虑
7.1 常见安全威胁
- 缓冲区溢出
- SQL注入
- 跨站脚本(XSS)
- 跨站请求伪造(CSRF)
- 权限提升
7.2 安全编程实践
- 最小权限原则
- 深度防御
- 安全默认值
- 输入净化
- 输出编码
8. 代码质量与可维护性
8.1 代码清晰性
- 有意义的命名
- 适当的注释
- 一致的风格
- 合理的模块化
8.2 可测试性
- 单一职责
- 低耦合
- 可注入依赖
- 明确的接口
8.3 文档化
- API文档
- 设计决策记录
- 假设和限制说明
- 使用示例
9. 防御性编程模式
9.1 空对象模式
提供默认行为而非返回null
9.2 哨兵值
使用特殊值表示特定状态
9.3 保护性子句
提前返回以减少嵌套
9.4 不变性
使用不可变对象避免意外修改
9.5 包装器
在不信任的接口周围添加安全层
10. 测试与验证
10.1 测试策略
- 单元测试
- 集成测试
- 模糊测试
- 边界测试
- 负面测试
10.2 静态分析
- 代码审查
- 静态分析工具
- 类型系统利用
- 契约检查
10.3 运行时监控
- 日志记录
- 性能监控
- 异常跟踪
- 健康检查
11. 防御性编程的局限性
- 可能增加代码复杂性
- 可能影响性能
- 需要权衡防御程度
- 不能替代良好的设计和架构
- 过度防御可能导致掩盖真正问题
12. 实施建议
- 将防御性编程纳入代码审查标准
- 建立团队编码规范
- 使用静态分析工具
- 编写防御性编程检查清单
- 定期进行安全培训