面对复杂多变的运行环境、不可预测的用户输入以及潜在的编程错误,如何确保软件在遭遇异常情况时依然能够稳定运行,是每位开发者必须面对的挑战。防御性编程(Defensive Programming)正是为解决这一问题而生的一种编程范式,它强调在编程过程中预见并防范潜在的错误和异常情况,从而增强软件的健壮性和稳定性。作为一种细致、谨慎的编程方法,通过提前考虑并防范可能出现的错误,从而有效减少软件漏洞和故障。本文将详细介绍防御性编程的基本概念、关键策略,并通过实际案例展示其在实际项目中的应用。
2. 防御性编程的基本概念
3. 防御性编程的核心原则
3.1 风险识别
3.2 防御原则
1.假设输入总是错误的:不依赖外部输入的绝对正确性,对所有输入进行验证和清理。
2.最小化错误的影响范围:通过异常处理、错误隔离等措施,限制错误对系统整体的影响。
3.使用断言进行内部检查:在代码的关键位置加入断言,确保程序状态符合预期。
4.代码清晰易懂:编写易于理解和维护的代码,便于团队成员发现潜在问题。
5.持续测试:通过单元测试、集成测试等手段,不断验证软件的正确性和稳定性。
4. 防御性编程案例
4.1 输入验证与清理
场景
防御性编程实践
private static final int MAX_PAGE_SIZE = 100;/*** 获取分页信息并进行参数校验** @param totalRecords 总记录数* @param pageSize 每页记录数* @param pageNumber 当前页码* @return 分页信息,包括总页数、当前页码等*/public PaginationInfo getPaginationInfo(int totalRecords, int pageSize, int pageNumber) {// 校验pageSizeif (pageSize <= 0 || pageSize > MAX_PAGE_SIZE) {throw new IllegalArgumentException("pageSize必须为正整数且不超过" + MAX_PAGE_SIZE);}// 校验pageNumberif (pageNumber <= 0) {pageNumber = 1; // 默认为第一页}// 计算总页数int totalPages = (totalRecords + pageSize - 1) / pageSize;// 确保pageNumber不超过总页数if (pageNumber > totalPages) {pageNumber = totalPages;}// 计算当前页的数据起始索引(可选,根据具体需求)int startIndex = (pageNumber - 1) * pageSize;// 返回分页信息return new PaginationInfo(totalPages, pageNumber, startIndex);}// PaginationInfo 是一个简单的类,用于封装分页信息...
在这个例子中,getPaginationInfo方法首先验证了pageSize和 pageNumber参数的有效性,确保了它们符合预期的约束条件。如果参数无效,方法会抛出一个 IllegalArgumentException 异常,这有助于调用者识别并处理错误情况。然后,方法计算了总页数,并根据需要调整了 pageNumber 以确保它不会超出范围。最后,方法返回了一个包含分页信息的 PaginationInfo 对象。
这种防御性编程策略有助于防止因无效的分页参数而导致的程序错误,提高了API的健壮性和用户体验。
4.2 预防死循环
场景
在循环或者遍历场景中,没有明确的退出机制。
防御性编程实践
风险识别:系统性风险,可能导致系统整体不可用。
防御策略:
•参数验证:检查涉及循环步长的入参是否有效。
•循环终止条件必达性确认:在涉及条件校验的场景中,避免等值条件判断,防止跳过循环终止点。
•日志记录:在关键位置添加日志记录,帮助调试和追踪问题。
示例代码(Java):
/*** 生成时间段。** @param startMinutes 开始时间* @param endMinutes 结束时间* @param interval 时间段间隔* @param duration 时间的时长* @return 时间段列表*/public List<String> generateList(int startMinutes, int endMinutes, int interval, int duration) {List<String> result = new ArrayList<>();int nextStartTime = startMinutes;while (nextStartTime == endMinutes) {int currentStartMinutes = nextStartTime;int currentEndMinutes = currentStartMinutes + duration;result.add(currentStartMinutes + "-" + currentEndMinutes);nextBatchStartTime += interval;}return result;}
针对以上代码,我们可以添加一些防御式编程的元素来确保代码的健壮性和可靠性。防御式编程侧重于预防错误的发生,包括输入验证、错误处理和边界条件检查。以下是修改后的代码,包含了防御式编程的改进:
/*** 生成时间段。** @param startMinutes 开始时间* @param endMinutes 结束时间* @param interval 时间段间隔* @param duration 时间的时长* @return 时间段列表*/public List<String> generateList(int startMinutes, int endMinutes, int interval, int duration) {// 改进点1:校验 interval,以保证循环中的步长能够正向增长// 一般情况下,还需要对步长,和 endMinutes与startMinutes的区间大小做限制,避免生成“巨大”的列表。if (interval <= 0) {throw new IllegalArgumentException("Invalid parameters: interval must be positive integers.");}List<String> result = new ArrayList<>();int nextStartTime = startMinutes;//改进点2:避免使用等号做循环终止条件,以防跳过循环终止点。while (nextStartTime <= endMinutes) {int currentStartMinutes = nextStartTime;int currentEndMinutes = currentStartMinutes + duration;result.add(currentStartMinutes + "-" + currentEndMinutes);nextBatchStartTime += interval;}return result;}
4.3 异常处理
场景
程序在读取文件、进行网络请求或执行其他可能失败的操作时,需要处理潜在的异常。
防御性编程实践
风险识别:非系统性风险,影响单次请求。
防御策略:
•使用try-except语句:将可能抛出异常的代码块放在try语句中,并在except语句中捕获并处理这些异常。
•区分异常类型:根据实际需要捕获特定的异常类型,或捕获所有异常(使用Exception作为异常类型)。
•记录错误信息:在捕获异常后,记录详细的错误信息(如异常类型、错误消息、堆栈跟踪等),以便后续分析和调试。
示例代码(Java):
/*** 读取文件内容。** @param filePath 文件路径* @return 文件内容,如果文件不存在或读取失败则返回null*/public static String readFile(String filePath) {try {byte[] encoded = Files.readAllBytes(Paths.get(filePath));return new String(encoded);} catch (FileNotFoundException e) {log.info("文件未找到:" + filePath);return null;} catch (Exception e) {log.info("读取文件时发生错误:" + e.getMessage());return null;}}
4.4 边界条件检查
场景
在循环、条件判断或数组访问等操作中,需要确保不会超出预期的范围或边界。
防御性编程实践
风险识别:非系统性风险,影响单次请求。
防御策略:
•检查循环条件:确保循环条件在每次迭代后都能正确更新,以避免无限循环。
•数组和集合访问:在访问数组、列表、字典等集合的元素之前,检查索引或键是否有效。
•边界值测试:对函数或方法的输入进行边界值测试,以确保它们在边界条件下也能正常工作。
示例代码(Java):
public class ArrayAccess {public static void main(String[] args) {int[] numbers = {1, 2, 3, 4, 5};int index = getIndexFromUser(); // 假设这是从用户那里获取的索引if (index >= 0 && index < numbers.length) {log.info(numbers[index]);} else {log.info("索引超出数组范围");}}// 假设这个方法从用户那里获取索引值,并进行基本的验证private static int getIndexFromUser() {// 为了示例,我们直接返回一个示例值return 2; // 假设用户输入了有效的索引值2}}
4.5 使用断言进行内部检查
场景
在代码的关键路径上,需要确保某些条件始终为真,否则程序将无法正确执行。
防御性编程实践
•使用断言:在代码的关键位置添加断言(如Python的assert语句),以验证程序状态是否符合预期。如果断言失败,则抛出AssertionError异常。
•注意断言的使用场景:断言主要用于开发和测试阶段,用于捕获那些理论上不应该发生的错误。在生产环境中,应该依赖更健壮的错误处理机制。
示例代码(Java):
/*** 计算年龄。** @param birthYear 出生年份* @return 年龄,如果输入无效则返回-1。*/public static int calculateAge(int birthYear) {// 输入验证:确保出生年份是一个合理的值if (birthYear <= 0 || birthYear > java.time.Year.now().getValue()) {// 抛出IllegalArgumentException来指示方法接收到了非法参数throw new IllegalArgumentException("出生年份必须是一个大于0且小于当前年份的整数");}// 计算年龄int currentYear = java.time.Year.now().getValue();return currentYear - birthYear;}public static void main(String[] args) {try {// 假设我们从某个地方(如用户输入)获取了出生年份int birthYear = 1990; // 这里直接赋值作为示例int age = calculateAge(birthYear);if (age != -1) { // 注意:这个例子中calculateAge实际上不会返回-1,但为了展示如何处理可能的异常情况,我们可以这样设计log.info("年龄是:" + age);}} catch (IllegalArgumentException e) {// 捕获并处理IllegalArgumentExceptionlog.info("错误:" + e.getMessage());}// 如果需要从用户输入中获取出生年份,你可以添加相应的逻辑来处理字符串到整数的转换和验证}// 注意:在这个例子中,我们没有直接使用assert,因为Java的assert主要用于调试,且默认是禁用的。// 而是通过显式的条件检查和异常抛出来实现防御性编程。
5. 防御式编程的挑战
5.1 是不是防御式代码越多越好呢?
No,过度的防御式编程会使程序会变得臃肿而缓慢,增加软件的复杂度。
要考虑好什么地方需要进行防御,然后因地制宜地调整进行防御式编程的优先级。
一般在入口处或者接入层做通用性防御性编程,比如数据准入校验;但对于循环类逻辑,应始终在使用处做细节性防御。
5.2 通用性防御措施 优于 细节性的防御
例如对于网络请求,一般是统一处理超时、鉴权、各种错误code,而不是在业务层个别处理
5.3 根据使用场景,调整防御力度
如项目内部使用的utils函数和公开发布的package,后者防御要求更高
6. 结论

扫一扫,加入技术交流群

