软件设计中的可调试性

软件调试是我们学习软件开发的第一课,开发往往大部分的时间不是在写代码,而是在查 Bug,相信大家也深有体会。我们有很多手段可以调试问题,调试最常用的手段包括打日志、GDB、分析堆栈、跟踪系统调用等等。但要怎么样才能从设计开始就考虑降低调试门槛,当我们的代码出现问题时能快速定位到问题呢/p>

本场 Chat 您将学到如下内容:

  • 了解如何通过设计的手段降低调试门槛;
  • 什么样的代码比较易于调试问题;
  • 出现问题怎么保存现场;
  • 怎么分析和调试问题。

软件调试是我们学习软件开发的第一课,开发往往大部分的时间不是在写代码,而是在查 Bug,相信大家也深有体会。

我们有很多手段可以调试问题,调试最常用的手段包括打日志、GDB、分析堆栈、跟踪系统调用等等。

但要怎么样才能从设计开始就考虑降低调试门槛,当我们的代码出现问题时能快速定位到问题呢/p>

本场 Chat 您将学到如下内容:

  • 了解如何通过设计的手段降低调试门槛;
  • 什么样的代码比较易于调试问题;
  • 出现问题怎么保存现场,怎么分析和调试问题;

      • 什么是可调试性
      • 为什么需要考虑可调试性
      • 怎么设计提高可调试性
        • 良好的边界
        • 可观察性
          • 留下有效日志
          • 系统状态可视
        • 可在线调试
      • 总结

什么是可调试性

关于软件开发中的可调试性,每个人都有不同的看法,通常大家会觉得就是方便查 bug,当然这个想法是没有问题的, 但过于笼统。

为了便于描述,我在这里先下个不太准确的定义。

这里讲的软件可调试性,主要包含两部分:

  1. 代码编码完成后,能快速验证是否达到预期结果
  2. 当结果与预期不一致时,能快速定位到问题原因

关于第一点,这里说的能快速验证是否达到预期结果,大家都觉得比较简单,但实际上并不容易,特别是大型软件开发的中,验证成本是很高的,比如改一行代码,有可能需要对整个项目重新编译、需要准备测试环境、需要运行各种测试案例等等。面且还不一定靠谱,因为这里的验证指的是对各种输入的验证,包括各类正常的和异常的输入,大部分情况下,如果我们只是对功能做验证,是比较难保证完全可靠的。这时就要考虑我们代码是否有设计良好的边界,比如模块与模块之间是否强耦合,单个模块是否可以很方便地做测试等。

关于第二点,就涉及到具体的技术问题了,当出现问题时,我们会分析一下结果和代码,有经验的程序员都能快速定位到问题。现代软件开发中,很多时候由于框架层面已经为我们考虑了很多事情,我们很容易就能拿到模块相关的日志、状态等信息,从而快速定位到问题点。少数情况下我们需要打开 IDE 稍加调试,或断个点。 但如果我们尝试考虑软件整体上的可调试性时,问题就会变得相对复杂很多。比如什么情况下该打日志,怎么打么将系统的状态透出来果 bug 不可重现,我们该怎么调试/p>

为什么需要考虑可调试性

很多人对于调试的第一反应是,出现 bug 就调试一下,调试那是 bug 出现之后的事情。

甚至很多人觉得,调试只能在开发过程中通过 IDE 来做,如果没有 IDE 或者开发环境调试就很难进行。

当然这是不对的,调试可以发生在软件生命周期的各个阶段,而能不能从容应对,就考验设计者对可调试性的考虑。

软件设计是一门很复杂的手艺,用户可见的需求只是冰山上的一角,软件设计者在整个设计过程中, 需要考虑到所有利益方的诉求。比如对于编码者,如何快速编码。对于测试人员,如何方便测试。对于运维方,如何开心地运维。

而可调试性,又涉及到多个利益相关方,并直接影响软件的整体设计,是非常重要的一环。

从系统层面上分析,我们会发现,可调试性做的很差的代码,往往质量也会很差,并且开发效率也不会太高。

试想如果每写一段代码都需要经过很繁琐的验证,每发现一个问题,都需要发很长时间去定位,那开发者想必也是很崩溃的。

由于对预期结果难于验证、问题难于定位,往往就容易偷懒,自测做不到位,结果就会容易导致质量下降,质量下降就会导致后期更多的问题,从而陷入开发困局。

可调试性这块,设计初期就应该想考虑清楚怎么去做。一般来说可调试性很好的软件必然是一个强内聚、弱耦合、接口明确、意图明晰的软件,而可调试性差的的软件往往具有过强的耦合和混乱的逻辑。

所以可调试性的好坏,从某些方面来讲又直接代表了一个软件的好坏,那当然是值得我们去提前考虑的事情。

怎么设计提高可调试性

对于软件的设计者而言,关于可调试性的考虑,我认为至少需要包含几个方面:

  1. 良好的边界
  2. 易于测试
  3. 易于理解
  4. 可观察性

良好的边界

这里说的边界,指的是软件中模块与模块间、类与类间、代码与代码间的边界。

一段好的代码,需要保证边界清晰,职责分明,否则就会变得很难维护,一旦出现问题,就会因为范围太大而调试起来耗时耗力。

模块与模块之间耦合性太大, 不仅会影响扩展性、复用性、维护性等,还会影响单元测试、集成测试;我们在项目中会听到” 我的模块依赖太多单测没法做”,这种问题归根结底还是设计问题,耦合性太强导致。

函数与函数之间强绑定, 单测时为了测一个函数就不得不对另外一堆模块打桩;模块与模块之间强绑定, 联调测试一个模块时就不得不对另外一堆模块打桩;良好的边界也是软件易于测试和易于理解的基础,下面以 Linux 为例讲述为什么良好的边界如此重要。

Linux 系统本身的设计是很值得学习的,如下图展示的 Linux 层次结构图:

大部分读者看到这张图都有点眼花了,但这么复杂的实现,Linux 在设计上仍然能做到易于理解和维护,这很大一部分原因在于,Linux 为每个部件设置了非常清晰的边界,并给出了明确的接口。所以我们在设计系统时也应该尽可能做到边界清晰。

可观察性

留下有效日志

日志是程序开发中最常见的调试方法,也可以说是最有用的手段。Linux 之父 Linus Torvalds 就说过,他从不用任何 Debugger 工具。

虽然日志是大家都会用的一个调试手段,但并没有多少人能打的很好。打日志是很有讲究的一个事情,日志的内容需要交代清楚: 时间、地点 (程序位置)、对象、关键因素,如果打印所在的函数有多个调用点,最好交代清楚调用栈。

例如打印函数调用失败:

  • 初级打法:

  • 有效打法:

有效日志应该包含以下特征:

  1. 日志分级, 不能所有级别的日志混成一团, 出了问题时应该能快速定位到所需日志。
  2. 日志不能打太频繁,需要考虑限速。
  3. 在关键位置需要留下日志记录,比如可能出错的地方。
  4. 有运行时开关,可动态调整日志输出。

下在举几个日志的例子:

1、下面是一段存储软件的日志

这段日志详细地记录了所有会修改到磁盘的操作,并单独清晰地记录下来,这样一旦发现存储数据出现问题,只需要通过分析日志就一定能查到问题点,找出是什么时间哪个操作导致的数据问题。

2、某容器产品日志

该日志记录配置的更改过程,将变更前后的对比一并输出,这样后续如果发现配置出现问题,就可以通过分析日志打到哪个时间点导致配置出现了问题。

如下日志可以清楚地看到变化的数据,版本 从 4 到 5,update 时间修改,members 修改,这种日志一条足够了然。

系统状态可视

这里讲的系统状态可视,指的是系统设计中,考虑能够将部分核心状态透出来。从而能在运行过程中,直接查看系统的运行状态,方便动态跟踪软件情况。

1、比如下面的 Golang 内置的 pprof 机制

文章知识点与官方知识档案匹配,可进一步学习相关知识Python入门技能树首页概览209349 人正在系统学习中 相关资源:基于java的GUI图形化界面的汽车油耗软件-Java代码类资源-CSDN文库

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2018年6月1日
下一篇 2018年6月2日

相关推荐