首页 > 编程语言 >防御性编程:让系统坚不可摧

防御性编程:让系统坚不可摧

时间:2024-07-25 13:56:27浏览次数:17  
标签:防御性 return pageSize int 编程 param 坚不可摧

1. 引言

面对复杂多变的运行环境、不可预测的用户输入以及潜在的编程错误,如何确保软件在遭遇异常情况时依然能够稳定运行,是每位开发者必须面对的挑战。防御性编程(Defensive Programming)正是为解决这一问题而生的一种编程范式,它强调在编程过程中预见并防范潜在的错误和异常情况,从而增强软件的健壮性和稳定性。作为一种细致、谨慎的编程方法,通过提前考虑并防范可能出现的错误,从而有效减少软件漏洞和故障。本文将详细介绍防御性编程的基本概念、关键策略,并通过实际案例展示其在实际项目中的应用。

2. 防御性编程的基本概念

防御性编程的核心思想在于承认程序总会存在问题和需要修改,因此聪明的程序员会提前考虑并防范可能的错误。它强调在编程过程中不仅要实现功能,还要确保程序在面对错误输入、异常情况和并发操作时能够稳定运行

3. 防御性编程的核心原则

3.1 风险识别

非系统性风险:只影特定场景下的响单次调用,不对系统整体稳定性产生影响。比如空指针异常、数据越界等。

系统性风险:导致整个服务不可用的风险。比如 死循环,分页查询pageSize过大等。

3.2 防御原则

1.假设输入总是错误的:不依赖外部输入的绝对正确性,对所有输入进行验证和清理。 2.最小化错误的影响范围:通过异常处理、错误隔离等措施,限制错误对系统整体的影响。 3.使用断言进行内部检查:在代码的关键位置加入断言,确保程序状态符合预期。 4.代码清晰易懂:编写易于理解和维护的代码,便于团队成员发现潜在问题。 5.持续测试:通过单元测试、集成测试等手段,不断验证软件的正确性和稳定性。

4. 防御性编程案例

4.1 输入验证与清理

场景

用户输入数据到Web表单中,系统需要处理这些数据以进行后续操作。

防御性编程实践

风险识别:系统性风险,可能导致系统整体不可用。

防御策略

验证数据类型:确保用户输入的数据类型符合预期(如数字、字符串、日期等)。如果类型不匹配,应给出错误提示并要求用户重新输入。 •长度和范围检查:对于字符串、数字等类型的数据,进行长度和范围检查,确保它们不超过系统处理能力的限制。 •清理输入数据:去除输入数据中的非法字符或格式,如去除字符串两端的空格、将特殊字符转换为普通字符等。

分页参数防御式编程案例

下面以分页参数防御式编程为案例进行举例说明:

场景描述: 假设开发一个Web API,该API需要根据用户请求返回特定数据的分页结果。分页请求包含以下参数:

pageSize:每页应显示的记录数。 •pageNumber:用户请求的当前页码。

防御性编程措施

1.验证pageSize:确保pageSize是一个正整数,并且不超过一个合理的最大值(例如100),以防止资源过度消耗。 2.验证pageNumber:确保pageNumber是一个正整数,并且不会请求到不存在的页码(即基于总记录数和pageSize计算出的最大页码之后)。 3.处理无效参数:如果参数无效,则返回清晰的错误消息,并可能设置一个默认的页码或每页记录数。 4.计算总页数:基于总记录数和pageSize计算总页数,以便在返回分页信息时包含给用户。

示例代码(伪代码):

public class PaginationService {        
    private static final int MAX_PAGE_SIZE = 100;        
        /**       
         * 获取分页信息并进行参数校验       
         *        
         * @param totalRecords 总记录数       
         * @param pageSize 每页记录数       
         * @param pageNumber 当前页码       
         * @return 分页信息,包括总页数、当前页码等       
         */      
         public PaginationInfo getPaginationInfo(int totalRecords, int pageSize, int pageNumber) {          
             // 校验pageSize          
             if (pageSize <= 0 || pageSize > MAX_PAGE_SIZE) {              
                 throw new IllegalArgumentException("pageSize必须为正整数且不超过" + MAX_PAGE_SIZE);          
             }            
             // 校验pageNumber          
             if (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 方法首先验证了 pageSizepageNumber 参数的有效性,确保了它们符合预期的约束条件。如果参数无效,方法会抛出一个 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) {  
            // 捕获并处理IllegalArgumentException  
            log.info("错误:" + e.getMessage());  
        }  
  
        // 如果需要从用户输入中获取出生年份,你可以添加相应的逻辑来处理字符串到整数的转换和验证  
    }  
  
    // 注意:在这个例子中,我们没有直接使用assert,因为Java的assert主要用于调试,且默认是禁用的。  
    // 而是通过显式的条件检查和异常抛出来实现防御性编程。  

5. 防御式编程的挑战

5.1 是不是防御式代码越多越好呢?

No,过度的防御式编程会使程序会变得臃肿而缓慢,增加软件的复杂度。

要考虑好什么地方需要进行防御,然后因地制宜地调整进行防御式编程的优先级。

一般在入口处或者接入层做通用性防御性编程,比如数据准入校验;但对于循环类逻辑,应始终在使用处做细节性防御

5.2 通用性防御措施 优于 细节性的防御

例如对于网络请求,一般是统一处理超时、鉴权、各种错误code,而不是在业务层个别处理

5.3 根据使用场景,调整防御力度

如项目内部使用的utils函数和公开发布的package,后者防御要求更高

6. 结论

防御性编程是一种积极主动的编程策略,它要求开发者在编写代码时,不仅要关注功能的实现,更要关注代码的健壮性和稳定性。通过预见并防范潜在的错误和异常情况,防御性编程能够显著提升软件的质量,减少因外部因素导致的程序崩溃,提升系统稳定性。

文章中难免会有不足之处,希望读者能给予宝贵的意见和建议。谢谢!

标签:防御性,return,pageSize,int,编程,param,坚不可摧
From: https://www.cnblogs.com/Jcloud/p/18322868

相关文章

  • JavaWeb(7) DOM编程
    目录一、什么是DOM编程二、获取页面元素的API1.在整个文档范围内查找元素结点2.在具体元素节点范围内查找子节点3.查找指定子元素节点的父节点4.查找指定元素节点的兄弟节点5.整体代码演示三、操作元素属性值API1.属性操作2.内部文本操作3.整体代码演示 四、增......
  • 油管视频《编程思维》中的题目,使用C语言编写出来,第二集,反抗
    题目,假设要在人群中找一位领袖,领袖的相关信息有,他的眼睛是绿色的,如果他长着红头发,名字至少两个连续字母相同,如果戴眼镜的话,名字中有且仅有2个元音,否则名字中会有三个元音,只有一人附和以上条件,请下达指令涉及编程的基础原理1,结构体的使用,用于存储每个人的信息2,字符串的处理,......
  • 油管视频《编程思维》中的题目,使用C语言编写出来,第三集,炉膛机器人
    题目:假设起初只有一个机器人,他的炉膛里有一个数字0,和另一个未知的任意生成的编码,随着推移,原始机器人自我复制,制造出更多一样的炉膛机器人,被原始机器人自我复制制造出的每一个子机器人的熔炉内,都继承了原始机器人未知的编码,并且有一个属于自己,独一无二的编码刻在外壳,第二代炉膛......
  • Java编程指南:高级技巧解析 - Excel单元格样式的编程设置
    最新技术资源(建议收藏)https://www.grapecity.com.cn/resources/前言在Java开发中,处理Excel文件是一项常见的任务。在处理Excel文件时,经常需要对单元格进行样式设置,以满足特定的需求和美化要求,通过使用Java中的相关库和API,我们可以轻松地操作Excel文件并设置单元格的样式。在......
  • 学了十几种编程语言后,我终于悟了!
    大家好,我是程序员鱼皮。16~24年,算下来我学编程8年多了,这期间我学过十几种编程语言,比如C、C++、Java、Python、JavaScript、Go、PHP、C#、SQL、Scala等。这么一看,目前排名前10的语言除了Fortran没接触过外,别的语言或多或少都写过点儿东西。VisualBasic是高中考计......
  • C语言面向对象风格编程解惑-全局变量性能分析
    C语言面向对象风格编程解惑-全局变量性能分析如果你是CPP老手,但在软件开发过程中要求采用C语言作为主要语言,首先遇到的是各种设计模式不方便应用了,感到非常困扰,然后就是认命之后走向另外一个极端,常常会有过度使用全局变量和goto语句的问题。CPP既然是CWithClass,自然不会排斥面......
  • 【React】箭头函数:现代 JavaScript 的高效编程方式
    文章目录一、箭头函数的基本语法二、箭头函数的特性三、在React中的常见用法四、最佳实践在现代JavaScript中,箭头函数(ArrowFunctions)是一种简洁的函数表达方式,并且在React开发中非常常见。箭头函数不仅简化了函数的语法,还带来了与普通函数不同的行为特性。本......
  • 【 Pro*C/C++ 】 Pro*C/C++ 编程
    ProC/C++编程1一、ProC/C++简介11.1、ProC/C++是什么11.2、ProC/C++处理流程2二、ProC/C++GCC环境配置32.1、ProC/C++预编译环境32.2、GCC编译器5三、开始编写第一个ProC++代码53.1、第一个ProC++代码53.2、ProC++代码预编译63.3、GCC编译73......
  • Java中的WebSocket编程:实时通信实现
    Java中的WebSocket编程:实时通信实现大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!实时通信在现代应用中变得越来越重要,从即时聊天到实时数据更新,WebSocket提供了一种高效的解决方案。本文将详细讲解如何在Java中使用WebSocket进行实时通信,涵盖基本的WebS......
  • 零基础STM32单片机编程入门(二十) 华邦W25Q32 SPI FLASH实战含源码
    文章目录一.概要二.W25Q32SPIFLASH主要参数三.W25Q32SPIFLASH芯片介绍1.W25Q32芯片内部框图2.W25Q32芯片指令表格3.W25Q32芯片通讯时序四.W25Q32SPIFLASH读写实验五.CubeMX工程源代码下载六.小结一.概要FLASH是一种存储芯片,通过程序可以修改数据,即平时所......