表象

内核中经常见到某些函数有很多的参数,而且这些参数会一级一级往下面传递,部分参数可能只有最深一级函数才会用到,但却被传递了很多级,其中不乏一些bool型的参数,比如如下的代码流程,6个参数传递了很多级。

kvm_mips_map_page() -> gfn_to_pfn_prot() -> __gfn_to_pfn_memslot() ->hva_to_pfn() -> hva_to_pfn_slow()

对于这样的代码,大家觉得如何?这样的代码是否有毛病?

这叫什么?

对于逐级传递的参数,业界有专门的术语对应,叫Tramp data,其标准定义为:

(programming) Data which is passed via one function to another, and not otherwise used by the first.

有问题么?

Tramp data是一种典型的代码坏味道,其表示:

一些代码需要了解相隔遥远的另一些代码提供的信息,需要通过中间媒介来传递这些信息。

其带来典型的问题如:

  1. 使代码变得僵硬,使得对调用链中的任何函数的重构,变得更加困难。
  2. 将信息(data/methods/architecture)分散于各个不同的地方,而其中部分地方并不关心这些信息。如果你需要声明这些流窜的tramp data,那可能需要一些新的import,这将会污染当前的name space。

另外,内核中经常使用bool类型的tramp data作为参数。bool类型的参数本身也是一种坏味道,会使代码的可读性变差。 再者,参数过多也是一种坏味道,问题很多,比如使函数变得复杂、职责不单一、难以测试等。不专门讨论~

为什么出现?

Tramp data出现的一个典型场景是用于代替全局变量,我们知道不合适的全局变量也是一种坏味道,对于全局变量的重构,最简单的方法就是使用tramp data了,这个很容易理解。

另一些场景,比如内核中,一些功能实现复杂,有很长的调用链,在底层的函数中需要了解的信息,通常都使用这样的tramp data来传递。

如果解决?

最基本的处理方式是:

至少需要保证每一级函数传递的参数名称是相同的,保证可读性。

还有一些建议,比如使用单例来重构,但单例本质上还是全局数据,又走回去了,但是一些情况下,全局变量可能比tramp data更好一些~

进一步的处理,可能就涉及代码自身设计的问题了,出现tramp data可能说明代码在设计上出了问题,理论上,整洁代码要求

  • 函数职责单一。单一职责的函数,应该不会有太多的参数,tramp data出现的几率也小。
  • 低耦合。距离遥远的代码间的依赖,说明其耦合不低。
  • 数据声明和数据使用的距离尽量短。
  • 抽象层次一致。个人理解,可能隐含的意思包括函数依赖的数据应当最好在同一抽象层次中。

重构理论中,建议用:去除中间人和提取类的方法来重构,具体操作需要针对具体的情况处理了,实际操作比理论可能复杂得多。

内核中的困惑

内核中类似这样的比比皆是,好像已经形成了一种习惯和风格,编写新代码时往往会参考已有代码方式,导致新代码中仍然有很多这样的情况。

将那前面的代码流程看,6个参数中,几乎所有的数据都可以从vcpu中获取,按整洁代码和重构理论中的建议,对于过多参数的重构,典型的处理方式就是抽象,如将相关的参数抽象成对象传递,对应内核中的这种情况,理论上,看似仅传递一个vcpu参数应该是更佳的选择,但代码中偏偏提取了vcpu中的各种数据,然后将其逐级传递。

这是内核中的普遍现象,可能原因是:为了更好性能。 这可能也是内核代码跟用户态程序相比,能不做clean code的最好的借口了~

内核社区中曾经也发起过关于内核代码clean code的争论,但最终没有结果,至今,内核中包含上千万行代码,早已形成了自己的code style了,不能说其代码不clean,可能需要你换个角度来看~