Oceniam bufory protokołu Google dla usługi opartej na Javie (ale oczekuję wzorców agnostycznych języka). Mam dwa pytania:
Pierwsze to ogólne pytanie ogólne:
Jakie wzory widzimy, ludzie używają? Wspomniane wzorce są powiązane z organizacją klas (np. Wiadomości na plik .proto, pakowanie i dystrybucja) i definicją wiadomości (np. Pola powtarzane vs. pola powtarzane *) itp.
Na stronach pomocy Google Protobuf i blogach publicznych jest bardzo mało tego rodzaju informacji, podczas gdy istnieje mnóstwo informacji o ustalonych protokołach, takich jak XML.
Mam również konkretne pytania dotyczące następujących dwóch różnych wzorów:
Reprezentuj wiadomości w plikach .proto, pakuj je jako osobny słoik i wysyłaj do docelowych klientów usługi - co jest w zasadzie domyślnym podejściem.
Zrób to samo, ale dołącz również ręcznie wykonane opakowania (nie podklasy!) Wokół każdej wiadomości, która implementuje kontrakt obsługujący co najmniej te dwie metody (T to klasa opakowania, V to klasa wiadomości (używająca ogólnych, ale uproszczonej składni dla zwięzłości) :
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()); }
Jedną z zalet, które widzę w (2), jest to, że mogę ukryć złożoność wprowadzoną przez V.newBuilder().addField().build()
i dodać kilka znaczących metod, takich jak isOpenForTrade()
lub isAddressInFreeDeliveryZone()
itp. W moich opakowaniach. Drugą zaletą, którą widzę w (2), jest to, że moi klienci radzą sobie z niezmiennymi obiektami (coś, co mogę wymusić w klasie opakowania).
Jedną wadą, którą widzę w (2), jest to, że duplikuję kod i muszę zsynchronizować swoje klasy opakowań z plikami .proto.
Czy ktoś ma lepsze techniki lub dalsze krytyki któregokolwiek z tych dwóch podejść?
* Przez enkapsulację powtarzającego się pola mam na myśli takie wiadomości jak ten:
message ItemList {
repeated item = 1;
}
message CustomerInvoice {
required ShippingAddress address = 1;
required ItemList = 2;
required double amountPayable = 3;
}
zamiast wiadomości takich jak ten:
message CustomerInvoice {
required ShippingAddress address = 1;
repeated Item item = 2;
required double amountPayable = 3;
}
Podoba mi się to drugie, ale cieszę się, że mogę wysłuchać argumentów przeciwko niemu.
źródło
Odpowiedzi:
Tam, gdzie pracuję, postanowiono ukryć użycie protobufa. Nie dystrybuujemy
.proto
plików między aplikacjami, ale każda aplikacja, która udostępnia interfejs protobuf, eksportuje bibliotekę klienta, która może z nią rozmawiać.Pracowałem tylko nad jedną z tych aplikacji ujawniających protobuf, ale pod tym względem każda wiadomość protobuf odpowiada pewnej koncepcji w tej dziedzinie. Dla każdej koncepcji istnieje normalny interfejs Java. Jest wtedy klasa konwertera, która może wziąć instancję implementacji i zbudować odpowiedni obiekt komunikatu, a także wziąć obiekt komunikatu i zbudować instancję implementacji interfejsu (jak to się dzieje, zwykle jest to zwykła anonimowa lub lokalna klasa zdefiniowana wewnątrz konwertera). Generowane przez protobuf klasy komunikatów i konwertery razem tworzą bibliotekę, która jest używana zarówno przez aplikację, jak i bibliotekę klienta; biblioteka klienta dodaje niewielką ilość kodu do konfigurowania połączeń oraz wysyłania i odbierania wiadomości.
Następnie aplikacje klienckie importują bibliotekę klienta i zapewniają implementacje dowolnych interfejsów, które chcą wysłać. Rzeczywiście, obie strony robią to drugie.
Aby to wyjaśnić, oznacza to, że jeśli masz cykl żądanie-odpowiedź, w którym klient wysyła zaproszenie na imprezę, a serwer odpowiada przez RSVP, to są to następujące rzeczy:
.proto
plikuPartyInvitationMessage
klasa, wygenerowana przezprotoc
PartyInvitation
interfejs zdefiniowany we wspólnej biblioteceActualPartyInvitation
, konkretna implementacjaPartyInvitation
zdefiniowana przez aplikację kliencką (tak naprawdę jej nie nazywa!)StubPartyInvitation
, prosta implementacjaPartyInvitation
zdefiniowana przez bibliotekę współdzielonąPartyInvitationConverter
, które mogą konwertować aPartyInvitation
na aPartyInvitationMessage
, iPartyInvitationMessage
aStubPartyInvitation
.proto
plikuRSVPMessage
klasa, wygenerowana przezprotoc
RSVP
interfejs zdefiniowany we wspólnej biblioteceActualRSVP
, konkretne wdrożenieRSVP
zdefiniowana przez aplikację serwera (też tak naprawdę nie jest tak nazywana!)StubRSVP
, prosta implementacjaRSVP
zdefiniowana przez bibliotekę współdzielonąRSVPConverter
, które mogą konwertować anRSVP
na anRSVPMessage
iRSVPMessage
na aStubRSVP
Powodem, dla którego mamy osobne implementacje rzeczywiste i pośrednie, jest fakt, że rzeczywiste implementacje są zazwyczaj klasami jednostek odwzorowanych na JPA; serwer albo je tworzy i utrwala, albo wysyła do nich zapytania z bazy danych, a następnie przekazuje je do warstwy protobuf, która ma zostać przesłana. Nie uważano za stosowne tworzenie instancji tych klas po stronie odbierającej połączenie, ponieważ nie byłyby one powiązane z kontekstem trwałości. Co więcej, byty często zawierają raczej więcej danych niż jest przesyłanych przewodowo, więc nie byłoby nawet możliwe stworzenie nienaruszonych obiektów po stronie odbiorczej. Nie jestem do końca przekonany, czy to był właściwy ruch, ponieważ pozostawił nam o jedną klasę więcej na wiadomość niż w przeciwnym razie.
Rzeczywiście, nie jestem do końca przekonany, że użycie protobuf w ogóle było dobrym pomysłem; gdybyśmy trzymali się zwykłego starego RMI i serializacji, nie musielibyśmy tworzyć prawie tylu obiektów. W wielu przypadkach moglibyśmy po prostu oznaczyć nasze klasy bytów jako możliwe do serializacji i zająć się tym.
Teraz, powiedziawszy to wszystko, mam znajomego, który pracuje w Google, na bazie kodu, która intensywnie wykorzystuje protobuf do komunikacji między modułami. Przyjmują zupełnie inne podejście: nie zawijają generowanych klas komunikatów i entuzjastycznie przekazują je głęboko (ish) do swojego kodu. Jest to postrzegane jako dobra rzecz, ponieważ jest to prosty sposób na zachowanie elastyczności interfejsów. Nie ma kodu rusztowania, który byłby zsynchronizowany, gdy wiadomości ewoluują, a wygenerowane klasy zapewniają wszystkie niezbędne
hasFoo()
metody potrzebne do odbierania kodu w celu wykrycia obecności lub braku pól, które zostały dodane z czasem. Pamiętaj jednak, że ludzie pracujący w Google są (a) raczej sprytni i (b) trochę szaleni.źródło
Aby dodać odpowiedź Andersona, istnieje cienka linia sprytnego zagnieżdżania wiadomości i przesadzania. Problem polega na tym, że każda wiadomość tworzy za kulisami nową klasę oraz różnego rodzaju akcesoria i moduły obsługi danych. Ale wiąże się to z pewnym kosztem, jeśli musisz skopiować dane lub zmienić jedną wartość lub porównać wiadomości. Procesy te mogą być bardzo powolne i bolesne, jeśli masz dużo danych lub czas.
źródło