工程代码中的错误处理

简介: 概要:在前序的文章中,我们已经陈述过了工程代码是在对被设计对象的了解不完全清楚的情况下所作出的设计。这就使得工程代码中不可避免的会出现相关的错误。由于工程代码是复杂的,如果对错误不加以控制,将可能会导致错误的扩大化。因此,对代码中可能出现的错误进行分类,并基于错误分类进行不同的错误处理是必要的工作。错误的扩大化对于高度复杂的系统,由于系统之间的联动,局部的错误可能会导致全局性的错误。用系统论的话来

概要:

前序的文章中,我们已经陈述过了工程代码是在对被设计对象的了解不完全清楚的情况下所作出的设计。这就使得工程代码中不可避免的会出现相关的错误。由于工程代码是复杂的,如果对错误不加以控制,将可能会导致错误的扩大化。因此,对代码中可能出现的错误进行分类,并基于错误分类进行不同的错误处理是必要的工作。

错误的扩大化

对于高度复杂的系统,由于系统之间的联动,局部的错误可能会导致全局性的错误。用系统论的话来说,就是我们设计的系统之间可能存在正反馈,使得某一局部的错误被正反馈放大。这种观点是Nam Suh 的《公理设计》中,要求我们所设计的系统必须是完全独立的基础出发点之一。对于人类所设计的系统,由于我们对对被设计对象的了解不完全清楚,我们往往不能用数学的方式对被设计对象进行完全建模,计算系统的是否存在是否存在大于1的特征值也无从谈起。因此,默认系统是存在正反馈的是更为安全的做法。

举一个现实的例子,对于ETL组件(数据传输模块,用于写日志),我们往往需要给用户提供一个sdk,便于用户调用,并写入下游。如果这个sdk出现了错误,导致写入下游失败,而相应的错误(在java中,throw了一个exception;或是在Golang中panic)在用户代码中没有被处理,那么就会导致用户的程序崩溃。对于大部分的用户来说,因为日志组件的故障,导致业务代码崩溃,这是不能接受的。

因此,一种比较聪明的做法(仅就错误处理而言)是,将sdk分为两部分,一部分独立于用户的代码,专门做数据打包,网络传输,拥塞控制等工作;一部分则只负责做数据适配等较为简单的,不易出现问题的工作,并将数据传输给传输组件(两者可以通过某个固定的端口通信)。这样,即便独立的部分出现了panic,用户的程序也不会崩溃。(这种做法是我学习到的别人的做法,并不是我的发明)

错误的分类与处理

对于错误,我们可以将其分为四种:

  1. 完全已知的错误:错误的原因,范围,特性等均已知。
  2. 已知特性的错误:了解错误的范围或特性,但是不知道错误的原因;且可以根据错误的特性,给出相应的处理手段。
  3. 有备用方案的已知范围的错误:不了解错误的特性和原因,已知错误的范围,且有备用方案。
  4. 无备用方案的已知范围的错误:不了解错误的特性和原因,已知错误的范围,且无备用方案。

相应的处理手段:

  1. 完全已知的错误:既然已经知道原因,就应该处理问题。带病上线是不对的,代码中就不该出现这种错误。
  2. 已知特性的错误:在模块的父模块,做出相应的处理。且处理后,父模块应该能够正常工作。(如果不能,那么这个错误属于未知特性错误)
  3. 有备用方案的已知范围的错误:采用备用方案,且处理后,父模块应该能够正常工作。(注意这里的备用方案是广义的,只要是根据范围给出的处理方案,均可认为是备用方案,如游戏里的单机模式)
  4. 向上抛出,直至某一层存在备用方案。如果没有,应当反馈给客户和开发者。

另外,在实践中,应当遵循防御式编程思路,不要假设依赖的模块一定不会出现问题,不要假设模块只会出现已知特性的错误。在模块的连接处,至少应当有一个无备用方案的已知范围的错误的上报方案,且该方案为默认兜底方案,而不是已知特性的错误处理方案为兜底方案。

        switch (e.errorCode){
            case 1:
                processForErrorCodeOne();
            default:
                processForUnknownErrorCode();
        }

给出这种处理手段的原因在于,我们需要构建一个系统的模块树与相应的状态树,为了保证模块树的正常运行,我们的错误分类与状态分类必须保证其在状态树上是可传递的,即我们需要保证错误的分类能够涵盖所有类型的错误,且父节点的状态可以根据子节点推出,并且仍旧符合我们对错误状态分类的定义。也就是说,状态树应该满足:

  1. 状态树中的非叶子结点的状态只与其子节点的状态相关。
  2. 状态树中每个节点的状态,仅应该描述自身的状况,而不允许跨级描述。
  3. 某个节点的子节点的状态都是正常,那么该节点应该是正常。

以下图中的状态树来举例:

节点C出现了已知特性的错误,所以节点A处理该错误后,恢复了节点A的正常状态。

节点F出现了未知特性错误,错误范围为F;节点D无法处理这一错误,因此继续向上抛出错误至节点B1,此时错误范围为D;同样B1无法处理节点D的错误,向上抛出错误至节点X,错误范围为B1;节点X选取B1的备份中的一个替代B1的功能,恢复了正常工作。

常见的不好的处理方式与case

  1. 错误处理超范围:错误处理手段涵盖了其他模块。例如,节点D中,处理节点F错误的代码将节点G的错误也进行了囊括。例如:
     try {
            a = getConfig("a");
            b = getConfig("b");
        } catch (Exception e) {
            a = 0;
        }
  1. 错误处理超特性:错误的处理手段,并不能处理该特性下的所有错误。例如,在ETL软件中,为了保证数据的exactly once,在网络传输出现故障时,进行了事务性的回滚操作。但是,回滚重试这一操作,并不能处理掉一切的网络传输问题,其只能处理网络用塞带来的问题。如果是权限问题,或是某条数据本身就超过了下游允许发送的最大数据长度,那么就会导致数据的反复重新发送。特别是少量数据的反复发送,最终不仅没有实现exactly once,还会导致正常的数据也被堵死无法发送;更严重的后果是ETL模块使用CPU过高打挂用户的线上任务引发了严重故障。
  2. 跨层级错误处理:在非直接祖先节点中,处理子节点的问题。例如在节点B1中,处理节点F的问题。这会导致变更时,如果我们换掉了节点D的实现,那么原有的错误处理会成为脏代码。例如在节点B1中写如下的代码:
        if(D.getF()==0){
            D.setF(defaultFValue);
        }
        if(D.getG()==0){
            D.setG(defaultFValue);
        }

  1. 错误不抛出:当出现错误时,不向上抛出而返回正常。例如对于ETL组件,如果配置错误没有上报,用户会误认为数据已经正常收集,导致数据丢失。

目录
相关文章
|
Web App开发 移动开发 小程序
UniApp 自定义条件编译详细使用流程
UniApp 自定义条件编译详细使用流程
401 0
|
3月前
|
消息中间件 测试技术
项目环境测试问题之规范执行器的异常处理如何解决
项目环境测试问题之规范执行器的异常处理如何解决
|
5天前
|
存储 JavaScript 关系型数据库
鸿蒙开发:实现全局异常捕获和异常查看
如何灵活的拿到错误信息后,执行我们想要的逻辑,也是自研的一个诉求,比如全局监听到异常后,重启应用,或者上传到自己的服务器,或者可以在应用内查看等等,实现一个全局异常捕获,确实有很多的有用之处。
鸿蒙开发:实现全局异常捕获和异常查看
|
3月前
|
Java 测试技术
单元测试问题之想通过单元测试来驱动代码的设计与重构,如何实现
单元测试问题之想通过单元测试来驱动代码的设计与重构,如何实现
|
6月前
|
JavaScript 前端开发 测试技术
编写JavaScript模块化代码主要涉及将代码分割成不同的文件或模块,每个模块负责处理特定的功能或任务
【5月更文挑战第10天】编写JavaScript模块化代码最佳实践:使用ES6模块或CommonJS(Node.js),组织逻辑相关模块,避免全局变量,封装细节。利用命名空间和目录结构,借助Webpack处理浏览器环境的模块。编写文档和注释,编写单元测试以确保代码质量。通过这些方法提升代码的可读性和可维护性。
44 3
ESM规范如何处理循环依赖的情况
ESM规范如何处理循环依赖的情况
|
6月前
|
数据可视化 开发工具 数据安全/隐私保护
无代码开发真的可以做到吗?
无代码开发确实可以做到。 无代码开发是一种基于可视化界面和预构建模块的开发工具,使得开发者无需编写代码即可构建应用程序。这种开发方式的出现,使得非技术人员也可以轻松地构建应用程序,大大降低了开发门槛。
67 0
|
6月前
|
JSON Java 数据格式
SpringBoot - 错误处理机制与自定义错误处理实现
SpringBoot - 错误处理机制与自定义错误处理实现
47 0
|
前端开发 JavaScript
前端代码如何规范编写?
前端代码如何规范编写?
115 0