Protocol Buffers 3中的多态性

13

当前设计

我正在重构一些返回用户事件流的API代码。该API是一个普通的RESTful API,当前实现只是查询数据库并返回流。

代码长而繁琐,因此我决定将流生成移到微服务中,然后从API服务器调用该服务。

新设计

为了解耦,我认为数据可以作为Protobuf对象在API服务器和微服务之间来回传输。这样,我可以在任意一端更改编程语言,并仍然享受protobuf的类型安全性和精简大小。

enter image description here

问题

该动态消息源包含多种类型(例如,点赞、图片和语音消息)。将来可能会添加新类型。它们都共享一些属性,如时间戳和标题,但除此之外,它们可能完全不同。

在经典的面向对象编程中,解决方案很简单 - 创建一个基本的FeedItem类,所有动态消息项都继承自该类,以及一个包含一系列FeedItem类的Feed类。

如何在协议缓冲区3中表达多态的概念,或者至少在列表中启用不同类型的消息?

我检查了什么

  • Oneof:“无法重复使用oneof”。
  • Any: 太广泛了(就像Java的List<Object>)。

1
您可以通过将其放置在重复的子消息中来重复一个oneof。 - jpa
3
最近在protobuf邮件列表中有一个关于这个问题的讨论:https://groups.google.com/d/msg/protobuf/ojpYHqx2l04/bfyAhqBxAQAJ。我认为这是一个常见问题,通常的解决方案是将共同的数据放入一个消息中,然后不同类型的数据可以将其作为子消息包含。 - Adam Cozzette
@AdamCozzette 很好,这正是我想要的。似乎我们不能做得比这更好了。你能否将主要内容重写成答案(我很愿意采纳),还是你希望我自己来做? - Adam Matan
我今天有点忙,如果你能做的话那就太好了! - Adam Cozzette
特别是处理方面对我很有趣。如何避免使用继承和无法“预览”消息的情况下避免使用switch-case? - user4063815
1个回答

2
最初的回答:使用鉴别器多态性来解决序列化协议的问题。传统的面向对象继承是一种带有一些非常不好特性的形式。在像 OpenAPI 这样的新协议中,这个概念更加干净。
让我解释一下 proto3 是如何工作的。
首先,您需要声明您的多态类型。假设我们采用经典的动物物种问题,不同的物种具有不同的属性。我们首先需要定义一个所有动物都将识别其物种的根本类型。然后我们声明一个猫和狗的消息,这些消息扩展了基本类型。请注意,在所有三个消息中都投影了鉴别器“物种”。
 message BaseAnimal {
   string species = 1;
 }

 message Cat {
   string species = 1;
   string coloring = 10;
 }

 message Dog {
   string species = 1;
   int64 weight = 10;
 }

这里有一个简单的Java测试,用来演示实践中的工作原理。

最初的回答:Original Answer

    ByteArrayOutputStream os = new ByteArrayOutputStream(1024);

    // Create a cat we want to persist or send over the wire
    Cat cat = Cat.newBuilder().setSpecies("CAT").setColoring("spotted")
            .build();

    // Since our transport or database works for animals we need to "cast"
    // or rather convert the cat to BaseAnimal
    cat.writeTo(os);
    byte[] catSerialized = os.toByteArray();
    BaseAnimal forWire = BaseAnimal.parseFrom(catSerialized);
    // Let's assert before we serialize that the species of the cat is
    // preserved
    assertEquals("CAT", forWire.getSpecies());

    // Here is the BaseAnimal serialization code we can share for all
    // animals
    os = new ByteArrayOutputStream(1024);
    forWire.writeTo(os);
    byte[] wireData = os.toByteArray();

    // Here we read back the animal from the wire data
    BaseAnimal fromWire = BaseAnimal.parseFrom(wireData);
    // If the animal is a cat then we need to read it again as a cat and
    // process the cat going forward
    assertEquals("CAT", fromWire.getSpecies());
    Cat deserializedCat = Cat.parseFrom(wireData);

    // Check that our cat has come in tact out of the serialization
    // infrastructure
    assertEquals("CAT", deserializedCat.getSpecies());
    assertEquals("spotted", deserializedCat.getColoring());

整个技巧在于proto3绑定保留它们不理解的属性,并根据需要对它们进行序列化。通过这种方式,可以实现proto3转换(转换)而不会丢失数据。
请注意,“proto3转换”是非常不安全的操作,应该在进行鉴别器检查之后才能应用。在我的示例中,您可以将猫转换为狗而不会出现问题。下面的代码会失败。
    try {
        Dog d = Dog.parseFrom(wireData);
        fail();
    } catch(Exception e) {
        // All is fine cat cannot be cast to dog
    }

当相同索引的属性类型匹配时,可能会出现语义错误。例如,在我所拥有的示例中,索引10在dog中是int64或在cat中是string,proto3会将它们视为不同的字段,因为它们在传输时的类型代码不同。在某些情况下,如果类型是字符串并且结构体,则proto3实际上可能会抛出一些异常或产生完全垃圾的结果。


传统的继承方式有很多问题,这也是为什么proto3和XML之后的新序列化协议不支持它的原因。考虑一个简单的版本问题,其中序列化方可能会升级以使用新的更具体的异常类型,例如FileNotFound,它继承自IOError。消息的读取者可能知道如何处理IOError,但在线上只找到FileNotFound并不能告诉读取者这是IOError的子类,因此读取者将把它处理为通用的未知异常,可能导致系统中的错误。还有一些类似的问题。 - Kiril

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接