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:
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.
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.
fuente
Respuestas:
Donde trabajo, se tomó la decisión de ocultar el uso de protobuf. No distribuimos los
.proto
archivos 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:
.proto
archivoPartyInvitationMessage
clase, generada porprotoc
PartyInvitation
interfaz, definida en la biblioteca compartidaActualPartyInvitation
, una implementación concreta dePartyInvitation
definida por la aplicación cliente (¡en realidad no se llama así!)StubPartyInvitation
, una implementación simple dePartyInvitation
definida por la biblioteca compartidaPartyInvitationConverter
, que puede convertir aPartyInvitation
a aPartyInvitationMessage
, yPartyInvitationMessage
a aStubPartyInvitation
.proto
archivoRSVPMessage
clase, generada porprotoc
RSVP
interfaz, definida en la biblioteca compartidaActualRSVP
, una implementación concreta deRSVP
definida por la aplicación del servidor (¡en realidad no se llama así!)StubRSVP
, una implementación simple deRSVP
definida por la biblioteca compartidaRSVPConverter
, que puede convertir unaRSVP
a unaRSVPMessage
, y unaRSVPMessage
a 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.fuente
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.
fuente