Полиморфизм в протоколе буферов 3
Текущий дизайн
Я рефакторинг некоторого существующего кода API, который возвращает поток событий для пользователя. API - это обычный RESTful API, и текущая реализация просто запрашивает БД и возвращает фид.
Код длинный и громоздкий, поэтому я решил переместить генерацию каналов на микросервис, который будет вызываться с сервера API.
Новый дизайн
Ради развязки я подумал, что данные могут перемещаться взад-вперед с сервера API на микросервис в виде объектов Protobuf. Таким образом, я могу сменить язык программирования на любом конце и при этом наслаждаться безопасностью типов и небольшим размером protobuf.
Эта проблема
Фид содержит несколько типов (например, лайки, изображения и голосовые сообщения). В будущем могут быть добавлены новые типы. Например, все они имеют несколько временных меток и заголовка, но в остальном они могут быть совершенно другими.
В классическом ООП решение простое - основа FeedItem
класс, от которого наследуются все элементы фида, и Feed
класс, который содержит последовательность FeedItem
классы.
Как мне выразить понятие полиморфизма в буферных протоколах 3 или, по крайней мере, включить разные типы сообщений в списке?
Что я проверил
Oneof
: "Одно нельзя повторить".Any
: Слишком широкий (как у JavaList<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 может фактически выдавать некоторые исключения или создавать полный мусор.