Patrones de diseño Protobuf

19

Estoy evaluando Google Protocol Buffers para un servicio basado en Java (pero espero patrones agnósticos de lenguaje). Tengo dos preguntas:

La primera es una pregunta general amplia:

¿Qué patrones estamos viendo que usa la gente? Dichos patrones están relacionados con la organización de la clase (por ejemplo, mensajes por archivo .proto, empaquetado y distribución) y la definición del mensaje (por ejemplo, campos repetidos versus campos encapsulados repetidos *), etc.

Hay muy poca información de este tipo en las páginas de ayuda de Google Protobuf y en los blogs públicos, mientras que hay una gran cantidad de información para protocolos establecidos como XML.

También tengo preguntas específicas sobre los siguientes dos patrones diferentes:

  1. Represente los mensajes en archivos .proto, empaquételos como un contenedor separado y envíelos a los consumidores del servicio, lo cual es básicamente el enfoque predeterminado, supongo.

  2. Haga lo mismo, pero también incluya envoltorios hechos a mano (¡no subclases!) Alrededor de cada mensaje que implemente un contrato que soporte al menos estos dos métodos (T es la clase de envoltura, V es la clase de mensaje (usando sintaxis genérica pero simplificada por brevedad) :

    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());
    }
    

Una ventaja que veo con (2) es que puedo ocultar las complejidades introducidas por V.newBuilder().addField().build()y agregar algunos métodos significativos como isOpenForTrade()o isAddressInFreeDeliveryZone()etc. en mis envoltorios. La segunda ventaja que veo con (2) es que mis clientes manejan objetos inmutables (algo que puedo imponer en la clase wrapper).

Una desventaja que veo con (2) es que duplico el código y tengo que sincronizar mis clases de contenedor con archivos .proto.

¿Alguien tiene mejores técnicas o más críticas sobre cualquiera de los dos enfoques?


* Al encapsular un campo repetido me refiero a mensajes como este:

message ItemList {
    repeated item = 1;
}

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

en lugar de mensajes como este:

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

Me gusta este último pero estoy feliz de escuchar argumentos en contra.

Apoorv Khurasia
fuente
Necesito 12 puntos más para crear nuevas etiquetas y creo que protobuf debería ser una etiqueta para esta publicación.
Apoorv Khurasia

Respuestas:

13

Donde trabajo, se tomó la decisión de ocultar el uso de protobuf. No distribuimos los .protoarchivos entre aplicaciones, sino que cualquier aplicación que expone una interfaz protobuf exporta una biblioteca de cliente que puede comunicarse con ella.

Solo he trabajado en una de estas aplicaciones de exposición de protobuf, pero en eso, cada mensaje de protobuf corresponde a algún concepto en el dominio. Para cada concepto, hay una interfaz Java normal. Luego hay una clase de convertidor, que puede tomar una instancia de una implementación y construir un objeto de mensaje apropiado, y tomar un objeto de mensaje y construir una instancia de una implementación de la interfaz (como sucede, generalmente se define una clase simple anónima o local definida) dentro del convertidor). Las clases de mensajes generados por protobuf y los convertidores juntos forman una biblioteca que es utilizada tanto por la aplicación como por la biblioteca del cliente; la biblioteca del cliente agrega una pequeña cantidad de código para configurar conexiones y enviar y recibir mensajes.

Las aplicaciones del cliente luego importan la biblioteca del cliente y proporcionan implementaciones de cualquier interfaz que deseen enviar. De hecho, ambas partes hacen lo último.

Para aclarar, eso significa que si tiene un ciclo de solicitud-respuesta en el que el cliente envía una invitación a una fiesta y el servidor responde con un RSVP, entonces las cosas involucradas son:

  • Mensaje de invitación a la fiesta, escrito en el .protoarchivo
  • PartyInvitationMessage clase, generada por protoc
  • PartyInvitation interfaz, definida en la biblioteca compartida
  • ActualPartyInvitation, una implementación concreta de PartyInvitationdefinida por la aplicación cliente (¡en realidad no se llama así!)
  • StubPartyInvitation, una implementación simple de PartyInvitationdefinida por la biblioteca compartida
  • PartyInvitationConverter, que puede convertir a PartyInvitationa a PartyInvitationMessage, y PartyInvitationMessagea aStubPartyInvitation
  • Mensaje de RSVP, escrito en el .protoarchivo
  • RSVPMessage clase, generada por protoc
  • RSVP interfaz, definida en la biblioteca compartida
  • ActualRSVP, una implementación concreta de RSVPdefinida por la aplicación del servidor (¡en realidad no se llama así!)
  • StubRSVP, una implementación simple de RSVPdefinida por la biblioteca compartida
  • RSVPConverter, que puede convertir una RSVPa una RSVPMessage, y una RSVPMessagea unaStubRSVP

La razón por la que tenemos implementaciones separadas y actuales es que las implementaciones reales son generalmente clases de entidades asignadas a JPA; el servidor los crea y los persiste, o los consulta desde la base de datos, luego los entrega a la capa de protobuf para su transmisión. No se consideró apropiado crear instancias de esas clases en el lado receptor de la conexión, porque no estarían vinculadas a un contexto de persistencia. Además, las entidades a menudo contienen más datos de los que se transmiten a través del cable, por lo que ni siquiera sería posible crear objetos intactos en el lado receptor. No estoy completamente convencido de que este fue el movimiento correcto, porque nos ha dejado con una clase más por mensaje de lo que tendríamos de otra manera.

De hecho, no estoy completamente convencido de que usar protobuf sea una buena idea; Si nos hubiéramos quedado con la RMI y la serialización antiguas, no habríamos tenido que crear casi tantos objetos. En muchos casos, podríamos haber marcado nuestras clases de entidad como serializables y seguir adelante.

Ahora, dicho todo eso, tengo un amigo que trabaja en Google, en una base de código que hace un uso intensivo de protobuf para la comunicación entre módulos. Adoptan un enfoque completamente diferente: no envuelven las clases de mensajes generadas en absoluto y las transmiten con entusiasmo (ish) a su código. Esto se ve como algo bueno, porque es una forma simple de mantener las interfaces flexibles. No hay un código de andamiaje que se mantenga sincronizado cuando los mensajes evolucionan, y las clases generadas proporcionan todos los hasFoo()métodos necesarios para recibir código para detectar la presencia o ausencia de campos que se han agregado con el tiempo. Sin embargo, tenga en cuenta que las personas que trabajan en Google tienden a ser (a) bastante inteligentes y (b) un poco locas.

Tom Anderson
fuente
En un momento, busqué el uso de la serialización JBoss como un reemplazo más o menos directo para la serialización estándar. Fue bastante más rápido. Sin embargo, no es tan rápido como el protobuf.
Tom Anderson el
La serialización JSON usando jackson2 también es bastante rápida. Lo que odio de GBP es la duplicación innecesaria de las principales clases de interfaz.
Apoorv Khurasia
0

Para agregar a la respuesta de Anderson, hay una línea muy fina en los mensajes de anidación ingeniosamente entre sí y exagerando. El problema es que cada mensaje crea una nueva clase detrás de escena y todo tipo de accesores y manejadores para los datos. Pero eso tiene un costo si tiene que copiar los datos o cambiar un valor o comparar los mensajes. Esos procesos pueden ser muy lentos y dolorosos si tienes muchos datos o estás limitado por el tiempo.

Marko Bencik
fuente
2
esto se lee más como un comentario tangencial, vea Cómo responder
mosquito
1
Bueno, no lo es: no hay dominios, hay clases, todo es un problema de redacción al final (oh, estoy desarrollando todas mis cosas en C ++, pero esto no debe ser un problema)
Marko Bencik