Rust 在面对返回数据的字段格式不固定时该怎么办


用静态语言去对接不那么严谨设计的别人的系统和服务,通常会遇到返回的数据结构中的字段值类型出现各种不一致的情况,例如:

  • Bool,它一会返回 false 或者 true,一会返回 1 或者 0
  • Int,它一会返回 “314”,一会又返回 314
  • Float,它一会返回 “3.14”,一会又返回 3.14
  • Array,它一会返回 [“a”, “b”],一会又返回 “all”
  • Object,它一会返回 {},一会又返回 “all”
  • 等等诸如此类,防不胜防

而这些情况,说实话,如果以我所遇到的情况来说,那就基本上是 Python 的居最多,毕竟其他语言也没咋深入接触过多少项目。

我曾经将一个 Django 1.8 基于 Python 2 的项目,先是经历一波原地结构重整,然后再升级到基于 Python 的 Sanic 版本,完美。

后来我又想用它来尝试原地用 Rust 重写,因为业务流程的代码结构已经非常整洁,所以基本上只需要按照结构一比一逐步实现上层的 API 声明,下层的外部服务对接和数据库的调用,而中层的核心业务逻辑的部分,基本上可以照抄原 Python 的结构,甚至函数名称都不需要变,也不需要增加或减少函数方法。

说实话,是非常满意的,Rust 相比原来 Python 3 的代码总量,增加了大概 50% 左右,即 Python : Rust 约等于 1 : 1.5 这样的一个比例,而这增加的 50% 基本上是什么呢?核心绝大部分都是 model 包的声明,即数据结构的声明。这些数据结构,基本就是 struct 和 enum,还有一些就是 type alias,type alias 也是我挺喜爱的一个特性之一,在表达业务语义上,有着非常优秀的体验。

在某种不那么严谨较真程度上,我甚至可以将它理解为 Rust = Python + Types 这样幼稚的粗旷的类比。

好,说回原来的问题:强类型系统去对接弱类型系统,遇到返回的数据结构和值类型不严谨不规范怎么办?

首先想到的可能是先把锅甩出去:让对方系统进行修改。

但是如果对方系统不是你可以撼动的呢?如果对方系统是已经在线上运行了很久的呢?它当前已经被很多系统对接好了,无法轻易改动了呢?

作为后来者,还是只能自己消化这个问题了。

你应该经常见到写 APP 的那帮人会抱怨后端接口的不严谨设计,但是你看 Web 这边,很多就迷迷糊糊就过去了,即便现在都在慢慢向 Typescript 去靠近,但也是不会有那么非常强烈的感受的,毕竟用下 Union Types 也就很快过去了。

而在 Rust 这里,稍显得有些难度。

Rust 有个理念我挺认同:解析优于判断。具体是不是这么翻译的记得不是很清楚了,总之大概意思就是比起你拿到一个不那么确定的数据去做各种判断和分支处理,不如将它解析到一个稳定的结构里面去,只要能解析成功,那么后面就好办很多。

这在 Python 这边深有体会,由于基本上由于 “懒” 和 “省事” 和 “快” 的理念的盛行,也由于在数据层面支持得不那么够,也更是由于内置类型又足够丰富足够好用,导致基本上不会首先上来就去想这个数据结构对象应该是什么样的,而是使用内置类型进行业务逻辑的处理,一个业务逻辑,久而久之就会生长成为庞大的充斥着各种 if 判断的处理逻辑,思维好一点,可以会多写一点 if not return 做防御性判断,但是很多时候,一旦深陷泥潭式的业务逻辑中,没几个人敢或者有心力去做太大的变动,只会越来越糟。

如果让 Python 来面对今天的问题,当然也好办,无非就是加一个 if else 的问题,但你无法想象所有的数据都等业务逻辑跑到了真正要使用到的那个地方再去做判断和分支处理是多么可怕的一个事情,经手的人多了,可能甚至九转十八弯你都没能理清楚它们到底在转换个啥。

所以 Rust 这里好的地方是,如果花在解析结构这里的时间多一些,会稍稍对此有所缓解,为什么说有所缓解,因为大家都是全功能编程语言,代码写得怎么样,始终还是看人和他当时所面临的处境。

我也有尝试过用 Rust 对一个没有经历过原地结构重整的多年老 Python 项目进行 1 : 1 重写,你知道结果怎么样,它也一样能复刻出原项目里面哪些你看半天也无法理解到底在干嘛的神奇的地方。

所以,编程语言本身的优秀与否,是一个基础因素,它能决定某些地方的下限,但是决定不了你在超出编程语言范畴之上的某些地方的下限,在编程语言范畴之上你所能处的下限,还是得看写这些代码的人和他所面临的处境。

今天时间很晚了,又不愿意把文章优化再发,那今天就只能先卖个关子了,后面再花点时间再来详细讲如何具体处理各种不同场景。

待续。