在我的职业生涯中,我花了很多韶光来谈论protobuffers的问题。PB显然是由业余选手写的临时作品,随意马虎陷入困境且难于编译,当然也办理了Google的问题。如果protobuffers的这些问题在序列化抽象中隔离出来,那么我也不会连续抱怨。 但不幸的是,protobuffers的糟糕设计是如此有传染力,以至于这些问题也会渗透到你的代码中。
由业余爱好者创建和临时性
我曾经在Google事情过。 Google是我第一次利用protobuffers地方(很遗憾不是末了一个)。 我本日要谈论的所有问题都存在于Google的代码库中; 这不是“缺点利用protobuffers”的问题。
protobuffers的最大问题是其恐怖的类型系统。 Java的粉丝该当觉得宾至如归,但不幸的是,大家都不认为Java有一个设计良好的类型系统。利用动态措辞的人抱怨这样的类型系统太令人窒息了,而像我这样的静态措辞粉丝会抱怨这种设计没有给你真正想要的类型系统。
临时性和业余爱好者的建立是相辅相成的。大量protobuffer规范都是亡羊补牢的做法。 规范的许多限定会让你禁不住发问,为何利用PB如此困难。然而这都只是表象,真正的缘故原由是:
Protobuffers显然是由业余爱好者建造的,由于它们为广为人知且已经办理的问题供应了不好的办理方案。
没有组合性
Protobuffers供应了几个“特性”,但多数无法相互合营。 例如,以下是几个正交但受约束的特性列表。
oneof字段不能repeated 。
map<k,v>字段具有专用语法,不能用于任何其他类型。
只管可以对map字段进行参数化,但是不支持用户定义的类型。 这意味着须要手动处理很多事情。
map字段不能repeated 。
map键可以是string s,但不能是bytes 。 它们也不能enum ,纵然enum在protobuffer规范中实际上是整数。
map值不能是其他map 。
这种猖獗的限定列表是无原则设计和事后打补丁的结果。 例如,oneof字段不能repeated是由于代码天生器不会产生副产品类型,而是为您供应互斥的可选字段的产品。 这种转换仅对单个字段有效。
map字段无法repeated也是干系的,但揭示了类型系统的不同限定。 事理上,map<k,v>该当类似于repeated Pair<k,v> 。 但是由于repeated是一个措辞关键词,而不是一个独立的类型,它不能再次润色自己,因此map字段无法利用repeated。
估计你对为什么enum不能用作map键,已经有了自己猜想。(译者注:没想到的或者想印证自己想法的,该当参加GIAC了)
令人沮丧的是,哪怕对当代类型系统如何事情有一点理解,就可以在大大简化 protobuffer规范的同时肃清很多限定。
办理方案如下:
使所有字段都必须是required。这使得都是产品类型。
将oneof字段更换为独立数据类型。这供应副产品类型。
供应通过其他类型参数化产品和副产品类型的能力。
只须要这三个功能,你就能定义任何可能的数据类型。 我们可以根据它们重新实现别的的protobuffer规范。
例如,我们可以重修optional字段:
构建repeated字段也很大略:
当然,实际许可的序列化逻辑比通过网络推送链表更高明 - 毕竟, 实现和语义不须要一对一对齐
有问题的选择
在Java的根本上,protobuffers区分了标量类型和类型。 标量类型或多或少与机器原语相对应 - 比如int32 , bool和string 。 另一方面,其他类型都是类型。 所有库和用户定义的类型都是类型。
当然,这两种类型的语义完备不同。
纵然你没有设置它们,标量类型的字段也存在。 我提到过(至少在proto 3中 )所有protobuffers都可以零初始化。标量字段获取false-y值---例如, uint32初始化为0 , string初始化为\公众\"大众 。将protobuffer中缺失落的字段与默认值字段区分开来是不可能的。 这么做是为了优化默认值(减少传输的数据)。
protobuffers声称可以向后向和向前兼容,然而无法区分未设置值和默认值是一场噩梦。 如果确实是为了每个字段节约一位(有或没有)而做出如此设计,那么有点不值当。
比较之下, 虽然标量类型设计的不足好,但类型字段的行为就完备放飞自我了。类型字段无论是否存在,它们的行为都非常猖獗。 其访问代码值得细细阐发。 假设如下伪Java代码:
我们的想法是,如果未设置foo字段,则无论何时要求都会看到默认初始化的副本,但实际上不会修正其容器。 但是如果修正foo ,它也会修正它的父级!
所有这统统只是为了避免利用Maybe Foo类型和干系的细微差别,须要弄清楚未设置值意味着什么。
这种行为特殊令人震荡,由于它毁坏了规律!
我们期望赋值不会引起别的动作。 而PB将悄悄地变动msg以得到foo的零初始化副本。
与标量字段不同,我们至少可以检拆字段是否未设置。 protobuffers供应了天生的bool has_foo方法。如果想复制foo,则须要编写以下代码:
请把稳,至少在静态类型措辞中,由于方法foo , set_foo和has_foo之间的命名关系, 我们无法抽象处理。 除了预处理器宏之外,我们无法以编程办法天生它们:
(但预处理器宏是由Google code style指南禁止的。)
如果所有可选字段都被实现为Maybe s,那么将很随意马虎抽象处理这种情形。
让我们谈谈另一个有问题的决定。 虽然你可以在protobuffers中定义一个字段,但它们的语义不是副产品类型!
相反,对付每种情形你得到一个可选字段,以及setter中的魔术代码。如果设置了一个,它会将其他情形打消掉。
乍一看,这彷佛该当在语义上等同于union类型。 但相反,它是bug之源!
这种行为许可默默地删除任意数量的数据!
在protobuffers上编写通用的,无缺点的,多态的代码实际上是不可能的。
这不是任何人都喜好听到的东西,更不用说我们这些已经爱上参数多态性的人 - 这给了我们完备相反的承诺。
向后兼容的谎话
protobuffers的另一个杀手特性是它们“编写向后兼容API的能力”。
protobuffers 默认情形下通过偷偷地实行缺点操作来实现其兼容性。 当然,谨慎的程序员可以(并且该当)会对吸收到的protobuffers进行检讨。 但是你须要一直编写防御性检讨代码以确保您的数据没有问题,大概这只是意味着反序列化步骤过于宽松。 您所能做的便是将健全性检讨逻辑从定义良好的边界等分散开来,并将扩散全体代码库中。
另一个论点是,protobuffers将保留他们不理解的中存在的任何信息。 原则上,这意味着发送路由(不知道其schema版本)是非毁坏性的。
当然,在纸面上它是一个很酷的功能。 但我从来没有见过一个真正保留该属性的运用程序。 除了路由软件之外,没有什么其他软件仅检讨的某些位然后在未变动的情形下转发。 利用protobuffers的绝大多数程序将解码,将其转换为别的,并将其发送给其他程序。 这些变换是须要手动编码的。 从一个protobuffer到另一个protobuffer的手动编码转换不会保留两者之间的未知字段,由于它实际上毫无意义。
这种对待protobuffers的态度总是与其他丑陋的办法并举。protobuffers的风格指南积极倡导反DRY,并建议尽可能内联定义。 这背后的缘故原由是,如果这些定义在将来发生不合,它许可您单独修正。
这个问题的根源在于Google将数据的含义与其物理表示混为一谈。 当你处于谷歌规模时,这种事情可能是有道理的。 毕竟,他们有一个内部工具,许可您比较程序员韶光与网络利用率背后(或者其他事情)的本钱。 与大多数公司不同,工程师薪水是谷歌最小的开支之一。 从财务角度来说,摧残浪费蹂躏程序员的韶光以减少几个字节是有道理的。
在排名前五的科技公司之外,我们都跟Google的规模差了好几个数量级。 你的创业公司不应该摧残浪费蹂躏工程师的韶光来减少字节数。 但是减少字节并摧残浪费蹂躏程序员的韶光正是protobuffers优化的缘故原由。
面对现实吧。大部分公司永久达不到Google的规模。 对那些只是由于“谷歌利用它”,因此“它是行业最佳实践”的技能,我们该当停滞搬到自己公司。
Protobuffers污染代码库
如果可以将protobuffer的利用限定在网络传输,我就不会如此难堪。 不幸的是,虽然原则上有一些办理方案,但它们都不敷以实际用于真实软件。
Protobuffers对应于您希望发送的数据,这常日与运用程序要利用的实际数据干系但不相同 。 这使我们处于一种令人不安的田地,须要在三种不良选择中选择一种:
掩护一个描述您实际须要的数据的单独类型,并确保两者同步。
将数据打包成传输格式以供应用程序利用。
每次须要时都可以通过传输格式获取信息。
选项1显然是“精确的”办理方案,但它与protobuffers无法匹配。 该措辞的功能不足强大,无法同时作为传输格式和运用程序数据格式。这意味着须要写一个完备独立的数据类型,与protobuffer同步,并在两者之间显式利用序列化代码来同步。而大部分人利用PB便是为了不写序列化代码,以是这种情形不会发生。
相反,利用protobuffers的代码会在全体代码库中扩散。 我在谷歌的参与的紧张项目是一个编译器,它用各种各样的protobuffer作为输入,并在另一个程序中输出一个等价的“程序”。 输入和输出格式表达能力都足够,然而保持适当并行的C++版本永久不事情。代码无法利用我们为编写编译器而实现的任何丰富技能,由于protobuffer(以及由此产生的代码)过于僵化,无法做任何有趣的事情。
结果是,可能有50行递归代码就能实现的事情须要10,000行PB代码。 我想实现的功能由于PB的限定而无法实现。
虽然这是仅仅是一个例子,但它不是伶仃的。 由于它们严格的代码天生,措辞中的protobuffers的表现形式从来都不是惯用的办法。
但纵然这样,你仍旧须要将一个糟糕的类型系统嵌入到目标措辞中。 由于大多数protobuffers的功能都是不完善的,这些令人讨厌的属性也会泄露到我们的代码库中。 这意味着我们不仅要实现,而且还要在任何与之交互的项目中延续这些糟糕的想法。
在坚实的根本上实现无用的功能很随意马虎,但走向反面则是是寻衅。
简而言之,放弃将protobuffers引入项目吧。
原文链接:
http://reasonablypolymorphic.com/blog/protos-are-wrong/