Полиморфизм в протоколе буферов 3

Текущий дизайн

Я рефакторинг некоторого существующего кода API, который возвращает поток событий для пользователя. API - это обычный RESTful API, и текущая реализация просто запрашивает БД и возвращает фид.

Код длинный и громоздкий, поэтому я решил переместить генерацию каналов на микросервис, который будет вызываться с сервера API.

Новый дизайн

Ради развязки я подумал, что данные могут перемещаться взад-вперед с сервера API на микросервис в виде объектов Protobuf. Таким образом, я могу сменить язык программирования на любом конце и при этом наслаждаться безопасностью типов и небольшим размером protobuf.

Эта проблема

Фид содержит несколько типов (например, лайки, изображения и голосовые сообщения). В будущем могут быть добавлены новые типы. Например, все они имеют несколько временных меток и заголовка, но в остальном они могут быть совершенно другими.

В классическом ООП решение простое - основа FeedItem класс, от которого наследуются все элементы фида, и Feed класс, который содержит последовательность FeedItem классы.

Как мне выразить понятие полиморфизма в буферных протоколах 3 или, по крайней мере, включить разные типы сообщений в списке?

Что я проверил

  • Oneof: "Одно нельзя повторить".
  • Any: Слишком широкий (как у Java List<Object>,

0 ответов

Ответом для протоколов сериализации является использование полиморфизма на основе дискриминатора. Традиционное объектно-ориентированное наследование - это форма с некоторыми очень плохими характеристиками. В более новых протоколах, таких как OpenAPI, концепция немного чище.

Позвольте мне объяснить, как это работает с proto3

Сначала вам нужно объявить ваши полиморфные типы. Предположим, мы идем к классической проблеме видов животных, где разные виды имеют разные свойства. Сначала нам нужно определить тип корня для всех животных, которые будут идентифицировать вид. Затем мы объявляем сообщения Cat и Dog, которые расширяют базовый тип. Обратите внимание, что дискриминатор species прогнозируется во всех 3:

 message BaseAnimal {
   string species = 1;
 }

 message Cat {
   string species = 1;
   string coloring = 10;
 }

 message Dog {
   string species = 1;
   int64 weight = 10;
 }

Вот простой тест Java, чтобы продемонстрировать, как все работает на практике

    ByteArrayOutputStream os = new ByteArrayOutputStream(1024);

    // Create a cat we want to persist or send over the wire
    Cat cat = Cat.newBuilder().setSpecies("CAT").setColoring("spotted")
            .build();

    // Since our transport or database works for animals we need to "cast"
    // or rather convert the cat to BaseAnimal
    cat.writeTo(os);
    byte[] catSerialized = os.toByteArray();
    BaseAnimal forWire = BaseAnimal.parseFrom(catSerialized);
    // Let's assert before we serialize that the species of the cat is
    // preserved
    assertEquals("CAT", forWire.getSpecies());

    // Here is the BaseAnimal serialization code we can share for all
    // animals
    os = new ByteArrayOutputStream(1024);
    forWire.writeTo(os);
    byte[] wireData = os.toByteArray();

    // Here we read back the animal from the wire data
    BaseAnimal fromWire = BaseAnimal.parseFrom(wireData);
    // If the animal is a cat then we need to read it again as a cat and
    // process the cat going forward
    assertEquals("CAT", fromWire.getSpecies());
    Cat deserializedCat = Cat.parseFrom(wireData);

    // Check that our cat has come in tact out of the serialization
    // infrastructure
    assertEquals("CAT", deserializedCat.getSpecies());
    assertEquals("spotted", deserializedCat.getColoring());

Весь трюк в том, что привязки proto3 сохраняют свойства, которые они не понимают, и сериализуют их по мере необходимости. Таким образом можно реализовать преобразование proto3 (преобразование), которое изменяет тип объекта без потери данных.

Обратите внимание, что "proto3 cast" - очень небезопасная операция, и ее следует применять только после того, как будут выполнены надлежащие проверки для дискриминатора. Вы можете без проблем выставить кошку на собаку в моем примере. Код ниже не работает

    try {
        Dog d = Dog.parseFrom(wireData);
        fail();
    } catch(Exception e) {
        // All is fine cat cannot be cast to dog
    }

Когда типы свойств в одном и том же индексе совпадают, возможно, что будут семантические ошибки. В приведенном мной примере, где индекс 10 - это int64 в собаке или строка в cat proto3, они обрабатываются как разные поля, так как их код типа на проводе отличается. В некоторых случаях, когда тип может быть строкой, а структура proto3 может фактически выдавать некоторые исключения или создавать полный мусор.

Другие вопросы по тегам