Proto Best Practices

Teilt geprüfte Best Practices für die Erstellung von Protocol Buffers.

Clients und Server werden nie gleichzeitig aktualisiert - auch nicht, wenn Sie versuchen, sie gleichzeitig zu aktualisieren. Einer der beiden kann zurückgerollt werden. Gehen Sie nicht davon aus, dass Sie eine Breaking Change vornehmen können und es in Ordnung ist, weil Client und Server synchron sind.

Nicht eine Tag-Nummer wiederverwenden

Verwenden Sie niemals eine Tag-Nummer wieder. Das ruiniert die Deserialisierung. Selbst wenn Sie denken, dass niemand das Feld verwendet, wiederverwenden Sie keine Tag-Nummer. Wenn die Änderung jemals live war, könnten serialisierte Versionen Ihres Protos in einem Log irgendwo vorhanden sein. Oder es könnte alter Code in einem anderen Server sein, der kaputt geht.

Tag-Nummern für gelöschte Felder reservieren

Wenn Sie ein nicht mehr benötigtes Feld löschen, reservieren Sie seine Tag-Nummer, damit niemand sie in Zukunft versehentlich wiederverwendet. reserved 2, 3; reicht aus. Kein Typ erforderlich (ermöglicht das Trimmen von Abhängigkeiten!). Sie können auch Namen reservieren, um die Wiederverwendung von jetzt gelöschten Feldnamen zu vermeiden: reserved "foo", "bar";.

Nummern für gelöschte Enum-Werte reservieren

Wenn Sie einen nicht mehr benötigten Enum-Wert löschen, reservieren Sie seine Nummer, damit niemand sie in Zukunft versehentlich wiederverwendet. reserved 2, 3; reicht aus. Sie können auch Namen reservieren, um die Wiederverwendung von jetzt gelöschten Wertnamen zu vermeiden: reserved "FOO", "BAR";.

Neue Enum-Aliase zuletzt einfügen

Wenn Sie einen neuen Enum-Alias hinzufügen, fügen Sie den neuen Namen zuletzt ein, um Diensten Zeit zu geben, ihn zu übernehmen.

Um den ursprünglichen Namen sicher zu entfernen (falls er für den Austausch verwendet wird, was er nicht sollte), müssen Sie Folgendes tun

  • Fügen Sie den neuen Namen unter den alten Namen und kennzeichnen Sie den alten als veraltet (Serialisierer verwenden weiterhin den alten Namen)

  • Nachdem jeder Parser das Schema übernommen hat, tauschen Sie die Reihenfolge der beiden Namen (Serialisierer beginnen mit der Verwendung des neuen Namens, Parser akzeptieren beide)

  • Nachdem jeder Serialisierer diese Version des Schemas übernommen hat, können Sie den veralteten Namen löschen.

Hinweis: Obwohl Clients theoretisch den alten Namen nicht für den Austausch verwenden sollten, ist es dennoch höflich, die obigen Schritte zu befolgen, insbesondere für weit verbreitete Enum-Namen.

Den Typ eines Feldes nicht ändern

Ändern Sie fast nie den Typ eines Feldes; das ruiniert die Deserialisierung, genauso wie die Wiederverwendung einer Tag-Nummer. Die Protobuf-Dokumentation beschreibt eine kleine Anzahl von Fällen, die in Ordnung sind (z. B. der Wechsel zwischen int32, uint32, int64 und bool). Das Ändern des Nachrichtentyps eines Feldes führt jedoch zu Fehlern, es sei denn, die neue Nachricht ist eine Obermenge der alten.

Kein erforderliches Feld hinzufügen

Fügen Sie niemals ein erforderliches Feld hinzu, sondern fügen Sie // required hinzu, um den API-Vertrag zu dokumentieren. Erforderliche Felder werden von so vielen als schädlich angesehen, dass sie in proto3 vollständig entfernt wurden. Machen Sie alle Felder optional oder wiederholt. Sie wissen nie, wie lange ein Nachrichtentyp bestehen wird und ob jemand in vier Jahren gezwungen sein wird, Ihr erforderliches Feld mit einem leeren String oder Null zu füllen, wenn es logisch nicht mehr erforderlich ist, das Proto es aber immer noch angibt.

Für proto3 gibt es keine required Felder, daher gilt dieser Ratschlag nicht.

Keine Nachricht mit vielen Feldern erstellen

Erstellen Sie keine Nachricht mit „vielen“ (denken Sie: Hunderten) Feldern. In C++ fügt jedes Feld etwa 65 Bits zur Speicherobjektgröße hinzu, unabhängig davon, ob es gefüllt ist oder nicht (8 Bytes für den Zeiger und, wenn das Feld als optional deklariert ist, ein weiteres Bit in einem Bitfeld, das verfolgt, ob das Feld gesetzt ist). Wenn Ihr Proto zu groß wird, kann der generierte Code möglicherweise nicht einmal kompiliert werden (z. B. gibt es in Java eine harte Obergrenze für die Größe einer Methode).

Einen unspezifizierten Wert in einem Enum aufnehmen

Enums sollten einen Standardwert FOO_UNSPECIFIED als ersten Wert in der Deklaration enthalten. Wenn neue Werte zu einem Enum hinzugefügt werden, sehen alte Clients das Feld als nicht gesetzt an und der Getter gibt den Standardwert oder den zuerst deklarierten Wert zurück, falls kein Standard vorhanden ist. Für ein konsistentes Verhalten mit Proto-Enums sollte der zuerst deklarierte Enum-Wert ein Standardwert FOO_UNSPECIFIED sein und Tag 0 verwenden. Es kann verlockend sein, diesen Standard als semantisch bedeutungsvollen Wert zu deklarieren, aber als allgemeine Regel tun Sie dies nicht, um die Weiterentwicklung Ihres Protokolls zu unterstützen, wenn im Laufe der Zeit neue Enum-Werte hinzugefügt werden. Alle unter einer Container-Nachricht deklarierten Enum-Werte befinden sich im selben C++-Namespace, also präfixen Sie den unspezifizierten Wert mit dem Namen des Enums, um Kompilierungsfehler zu vermeiden. Wenn Sie niemals sprachübergreifende Konstanten benötigen, erhält ein int32 unbekannte Werte und generiert weniger Code. Beachten Sie, dass Proto-Enums den ersten Wert auf Null benötigen und unbekannte Enum-Werte (Roundtrip) serialisieren/deserialisieren können.

C/C++ Makrokonstanten für Enum-Werte nicht verwenden

Die Verwendung von Wörtern, die bereits von der C++-Sprache definiert wurden - insbesondere in ihren Header-Dateien wie math.h - kann zu Kompilierungsfehlern führen, wenn die #include-Anweisung für eine dieser Header-Dateien vor der für .proto.h steht. Vermeiden Sie die Verwendung von Makrokonstanten wie „NULL„, „NAN„ und „DOMAIN„ als Enum-Werte.

Bekannte und gemeinsame Typen verwenden

Die Verwendung der folgenden gängigen, gemeinsamen Typen wird dringend empfohlen. Verwenden Sie zum Beispiel nicht int32 timestamp_seconds_since_epoch oder int64 timeout_millis in Ihrem Code, wenn bereits ein perfekt geeigneter gemeinsamer Typ vorhanden ist!

  • duration ist eine signierte, feste Zeitspanne (z. B. 42s).
  • timestamp ist ein Zeitpunkt, unabhängig von Zeitzone oder Kalender (z. B. 2017-01-15T01:30:15.01Z).
  • interval ist ein Zeitintervall, unabhängig von Zeitzone oder Kalender (z. B. 2017-01-15T01:30:15.01Z - 2017-01-16T02:30:15.01Z).
  • date ist ein Kalenderdatum (z. B. 2005-09-19).
  • month ist ein Monat des Jahres (z. B. April).
  • dayofweek ist ein Wochentag (z. B. Montag).
  • timeofday ist eine Tageszeit (z. B. 10:42:23).
  • field_mask ist eine Menge von symbolischen Feldpfaden (z. B. f.b.d).
  • postal_address ist eine Postadresse (z. B. 1600 Amphitheatre Parkway Mountain View, CA 94043 USA).
  • money ist ein Geldbetrag mit seiner Währung (z. B. 42 USD).
  • latlng ist ein Breiten-/Längengradpaar (z. B. 37,386051 Längengrad und -122,083855 Breitengrad).
  • color ist eine Farbe im RGBA-Farbraum.

Hinweis: Während die „Well-Known Types“ (wie Duration und Timestamp) mit dem Protocol Buffers-Compiler geliefert werden, sind die „Common Types“ (wie Date und Money) dies nicht. Um die Common Types zu verwenden, müssen Sie möglicherweise eine Abhängigkeit zum googleapis-Repository hinzufügen.

Nachrichtentypen in separaten Dateien definieren

Beim Definieren eines Proto-Schemas sollten Sie eine einzelne Nachricht, ein Enum, eine Erweiterung, einen Dienst oder eine Gruppe zyklischer Abhängigkeiten pro Datei haben. Dies erleichtert das Refactoring. Das Verschieben von Dateien, wenn sie getrennt sind, ist viel einfacher, als Nachrichten aus einer Datei mit anderen Nachrichten zu extrahieren. Die Einhaltung dieser Praxis trägt auch dazu bei, die Proto-Schema-Dateien kleiner zu halten, was die Wartbarkeit verbessert.

Wenn sie außerhalb Ihres Projekts weit verbreitet sind, sollten Sie sie in eine eigene Datei ohne Abhängigkeiten legen. Dann ist es für jeden einfach, diese Typen zu verwenden, ohne die transitiven Abhängigkeiten in Ihren anderen Proto-Dateien einzuführen.

Weitere Informationen zu diesem Thema finden Sie unter 1-1-1 Regel.

Den Standardwert eines Feldes nicht ändern

Ändern Sie fast nie den Standardwert eines Proto-Feldes. Dies führt zu Versionskonflikten zwischen Clients und Servern. Ein Client, der einen nicht gesetzten Wert liest, erhält ein anderes Ergebnis als ein Server, der denselben nicht gesetzten Wert liest, wenn ihre Builds die Proto-Änderung überbrücken. Proto3 hat die Möglichkeit, Standardwerte festzulegen, entfernt.

Nicht von wiederholt zu skalar wechseln

Obwohl es keine Abstürze verursacht, verlieren Sie Daten. Für JSON geht ein Missverhältnis bei der Wiederholung der ganze Nachricht verloren. Bei numerischen proto3-Feldern und proto2 packed-Feldern geht beim Wechsel von wiederholt zu skalar der gesamte Datensatz in diesem Feld verloren. Bei nicht-numerischen proto3-Feldern und unkommentierten proto2-Feldern führt der Wechsel von wiederholt zu skalar dazu, dass der zuletzt deserialisierte Wert „gewinnt“.

Der Wechsel von skalar zu wiederholt ist in proto2 und in proto3 mit [packed=false] in Ordnung, da für die binäre Serialisierung der skalare Wert zu einer Liste mit einem Element wird.

Den Stilrichtlinien für generierten Code folgen

Proto-generierter Code wird in normalem Code referenziert. Stellen Sie sicher, dass Optionen in der .proto-Datei nicht zur Generierung von Code führen, der gegen den Stilrichtlinien verstößt. Zum Beispiel

Keine Textformatnachrichten für den Austausch verwenden

Textbasierte Serialisierungsformate wie Textformat und JSON stellen Felder und Enum-Werte als Zeichenketten dar. Infolgedessen schlägt die Deserialisierung von Protokollpuffern in diesen Formaten mit altem Code fehl, wenn ein Feld oder Enum-Wert umbenannt wird oder wenn ein neues Feld, ein neuer Enum-Wert oder eine neue Erweiterung hinzugefügt wird. Verwenden Sie nach Möglichkeit die binäre Serialisierung für den Datenaustausch und Textformat nur für die menschliche Bearbeitung und das Debugging.

Wenn Sie Protos, die in JSON konvertiert wurden, in Ihrer API oder zur Datenspeicherung verwenden, können Sie Felder oder Enums möglicherweise überhaupt nicht sicher umbenennen.

Niemals auf Serialisierungsstabilität über Builds hinweg vertrauen

Die Stabilität der Proto-Serialisierung ist über Binärdateien oder über Builds derselben Binärdatei hinweg nicht garantiert. Verlassen Sie sich nicht darauf, wenn Sie beispielsweise Cache-Schlüssel erstellen.

Java Protos nicht im selben Java-Paket wie anderer Code generieren

Generieren Sie Java-Proto-Quellen in einem separaten Paket von Ihren handgeschriebenen Java-Quellen. Die Optionen package, java_package und java_alt_api_package steuern, wo die generierten Java-Quellen ausgegeben werden. Stellen Sie sicher, dass handgeschriebener Java-Quellcode nicht auch in demselben Paket lebt. Eine gängige Praxis ist es, Ihre Protos in ein proto-Unterpaket in Ihrem Projekt zu generieren, das nur diese Protos enthält (d. h. kein handgeschriebener Quellcode).

Java-Paket aus dem .proto-Paket ableiten (falls überschrieben)

Das Setzen von java_package kann zu Kollisionen vollqualifizierter Namen im generierten Code führen, die in der .proto-Semantik nicht existierten. Zum Beispiel können diese beiden Dateien zu Kollisionen im generierten Code führen, obwohl die vollqualifizierten Namen im ursprünglichen Schema nicht kollidieren.

package x;
option java_package = "com.example.proto";
message Abc {}
package y;
option java_package = "com.example.proto";
message Abc {}

Um diese Probleme zu vermeiden, sollten Sie niemals dasselbe java_package in zwei Dateien festlegen, in denen unterschiedliche .proto-Pakete festgelegt sind.

Die beste Praxis ist, ein lokales Namensschema festzulegen, bei dem der Paketname vom .proto-Paket abgeleitet wird. Zum Beispiel könnte eine Best-Practice-Datei mit package y konsequent option java_package = "com.example.proto.y" festlegen.

Diese Anleitung gilt auch für alle anderen sprachspezifischen Optionen, bei denen Paketüberschreibungen möglich sind.

Vermeiden Sie die Verwendung von Sprachschlüsselwörtern für Feldnamen

Wenn der Name einer Nachricht, eines Feldes, eines Enums oder eines Enum-Wertes ein Schlüsselwort in der Sprache ist, die von diesem Feld liest/schreibt, kann Protobuf den Feldnamen ändern und andere Zugriffsmethoden als normale Felder haben. Sehen Sie sich zum Beispiel diese Warnung zu Python an.

Sie sollten auch die Verwendung von Schlüsselwörtern in Ihren Dateipfaden vermeiden, da dies ebenfalls zu Problemen führen kann.

Verschiedene Nachrichten für RPC-APIs und Speicher verwenden

Die Wiederverwendung derselben Nachrichten für APIs und Langzeitspeicher mag bequem erscheinen, reduziert Boilerplate-Code und den Aufwand für die Konvertierung zwischen Nachrichten.

Die Bedürfnisse des Langzeitspeichers und der Live-RPC-Dienste tendieren jedoch dazu, sich später zu unterscheiden. Die Verwendung separater Typen, auch wenn sie anfangs weitgehend dupliziert sind, gibt Ihnen die Freiheit, Ihr Speicherformat zu ändern, ohne Ihre externen Clients zu beeinträchtigen. Schichten Sie Ihren Code so, dass Module entweder mit Client-Protos, Speicher-Protos oder Übersetzung umgehen.

Es gibt Kosten für die Wartung der Übersetzungsschicht, aber diese zahlen sich schnell aus, sobald Sie Clients haben und Ihre ersten Speicheränderungen vornehmen müssen.

Keine Booleans für etwas verwenden, das jetzt zwei Zustände hat, aber später mehr haben könnte

Wenn Sie boolesche Werte für ein Feld verwenden, stellen Sie sicher, dass das Feld tatsächlich nur zwei mögliche Zustände beschreibt (für immer, nicht nur jetzt und die nahe Zukunft). Die zukünftige Flexibilität der Verwendung eines Enums ist oft lohnenswert, selbst wenn es bei der Einführung nur zwei Werte hat.

message Photo {
  // Bad: True if it's a GIF.
  optional bool gif;

  // Good: File format of the referenced photo (for example, GIF, WebP, PNG).
  optional PhotoType type;
}

`java_outer_classname` verwenden

Jede Proto-Schema-Definitionsdatei sollte die Option java_outer_classname auf den Dateinamen .proto festlegen, konvertiert in TitleCase mit entferntem „.“. Zum Beispiel sollte die Datei student_record_request.proto Folgendes festlegen

option java_outer_classname = "StudentRecordRequestProto";