Protobuf设计模式


19

我正在评估基于Java的服务的Google协议缓冲区(但希望使用与语言无关的模式)。我有两个问题:

第一个是一个广泛的一般性问题:

我们看到人们使用什么模式?所述模式与类组织(例如,每个.proto文件,打包和分发的消息)和消息定义(例如,重复字段vs.重复封装字段*)等有关。

在Google Protobuf帮助页面和公共博客上,此类信息很少,而对于已建立的协议(例如XML)则有大量信息。

对于以下两种不同的模式,我也有特定的问题:

  1. 将消息表示为.proto文件,将它们打包为一个单独的jar,然后将其发送给服务的使用者-这基本上是我认为的默认方法。

  2. 这样做,但在每条消息周围包括手工包装器(不是子类!),这些消息实现了至少支持这两种方法的协定(T是包装器类,V是消息类(使用泛型,但为简化起见简化了语法) :

    public V toProtobufMessage() {
        V.Builder builder = V.newBuilder();
        for (Item item : getItemList()) {
            builder.addItem(item);
        }
        return builder.setAmountPayable(getAmountPayable()).
                       setShippingAddress(getShippingAddress()).
                       build();
    }
    
    public static T fromProtobufMessage(V message_) { 
        return new T(message_.getShippingAddress(), 
                     message_.getItemList(),
                     message_.getAmountPayable());
    }
    

我从(2)中看到的一个优点是,我可以隐藏由它引入的复杂性,V.newBuilder().addField().build()并在包装器中添加一些有意义的方法,例如isOpenForTrade()isAddressInFreeDeliveryZone()等。我在(2)中看到的第二个优点是我的客户端处理了不可变的对象(可以在包装类中强制执行的事情)。

我在(2)中看到的一个缺点是我复制了代码,并且不得不将包装类与.proto文件进行同步。

有没有人对这两种方法有更好的技巧或进一步的批评?


*通过封装重复字段,我的意思是像这样的消息:

message ItemList {
    repeated item = 1;
}

message CustomerInvoice {
    required ShippingAddress address = 1;
    required ItemList = 2;
    required double amountPayable = 3;
}

而不是像这样的消息:

message CustomerInvoice {
    required ShippingAddress address = 1;
    repeated Item item = 2;
    required double amountPayable = 3;
}

我喜欢后者,但很高兴听到反对它的争论。


我还需要12点来创建新标签,我认为protobuf应该是此帖子的标签。
Apoorv Khurasia 2012年

Answers:


13

在我工作的地方,决定隐瞒使用protobuf。我们不在.proto应用程序之间分发文件,而是公开任何protobuf接口的任何应用程序都会导出一个可以与之通信的客户端库。

我仅处理了这些暴露于原始气泡的应用程序之一,但是,每条原生气泡消息都对应于该领域中的某个概念。对于每个概念,都有一个普通的Java接口。然后有一个转换器类,它可以采用实现的实例并构造适当的消息对象,并采用消息对象并构造接口的实现实例(通常发生时,通常定义一个简单的匿名或本地类)在转换器内部)。由protobuf生成的消息类和转换器一起形成一个库,供应用程序和客户端库使用;客户端库添加了少量代码来建立连接以及发送和接收消息。

然后,客户端应用程序导入客户端库,并提供其希望发送的任何接口的实现。的确,双方都做后一件事。

需要说明的是,这意味着如果您有一个请求-响应周期,其中客户端正在发送聚会邀请,而服务器正在以RSVP进行响应,则所涉及的是:

  • PartyInvitation消息,写在.proto文件中
  • PartyInvitationMessage 类,由 protoc
  • PartyInvitation 接口,在共享库中定义
  • ActualPartyInvitation,是PartyInvitation由客户端应用定义的具体实现(实际上不是这样!)
  • StubPartyInvitationPartyInvitation由共享库定义的简单实现
  • PartyInvitationConverter,可以将a转换PartyInvitationPartyInvitationMessage,以及将a转换PartyInvitationMessageStubPartyInvitation
  • RSVP消息,写在.proto文件中
  • RSVPMessage 类,由 protoc
  • RSVP 接口,在共享库中定义
  • ActualRSVPRSVP由服务器应用定义的具体实现(实际上也未称为!)
  • StubRSVPRSVP由共享库定义的简单实现
  • RSVPConverter,可以将an转换RSVPRSVPMessage,以及将an转换RSVPMessageStubRSVP

我们将实际实现和存根实现分开的原因是,实际实现通常是JPA映射的实体类。服务器要么创建并持久化它们,要么从数据库中查询它们,然后将它们交给protobuf层进行传输。在连接的接收端创建这些类的实例并不适合,因为它们不会与持久性上下文相关联。此外,实体所包含的数据通常比通过导线传输的数据要多得多,因此,甚至不可能在接收方创建完整的对象。我并不完全相信这是正确的举动,因为它给我们每条消息提供了比我们原本应多的类。

确实,我并不完全相信完全使用protobuf是个好主意。如果我们坚持使用简单的旧RMI和序列化,则不必创建几乎一样多的对象。在很多情况下,我们可以将实体类标记为可序列化并继续进行下去。

综上所述,现在,我有一个在Google工作的朋友,他的代码库大量使用protobuf进行模块之间的通信。他们采用了完全不同的方法:他们根本不包装所生成的消息类,并热情地将其传递给代码。这是一件好事,因为这是保持接口灵活的一种简单方法。当消息进化时,没有脚手架代码保持同步,并且生成的类提供hasFoo()了接收代码以检测随着时间推移而添加的字段是否存在所需的所有必要方法。不过请记住,在Google工作的人往往(a)相当聪明,(b)有点疯。


有一次,我研究了使用JBoss序列化作为或多或少的标准序列化的替代品。它快了很多。但是,没有protobuf快。
汤姆·安德森

使用jackson2的JSON序列化也非常快。我讨厌GBP是主要接口类的不必要重复。
Apoorv Khurasia

0

为了补充Andersons的回答,在巧妙地将消息嵌套到另一个消息中并过度处理时,有一条分界线。问题在于每条消息都会在后台创建一个新类,并为数据提供各种访问器和处理程序。但是,如果您必须复制数据或更改一个值或比较消息,则会产生成本。如果您有大量数据或受时间限制,这些过程可能会非常缓慢且痛苦。


2
这读起来更像是切线评论,请参阅如何回答
gna

1
好吧,它不是:没有领域,到最后都是类,这是所有措辞的问题(哦,我正在用C ++开发所有东西,但这绝对不是问题)
Marko Bencik
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.