protobuf что это такое
Структурированный протокол обмена данных Protobuf или JSON во фронтенде?
В новом проекте в нашей команде мы выбрали frontend framework VUE для нового продукта, бэкенд написан на PHP, и уже как 17 лет успешно работает.
Когда код начал разрастаться, нужно было думать над упрощением обмена данных с сервером, об этом я и расскажу.
Про бэкенд
Проект достаточно большой, и функционал очень замороченный, следовательно код написанный на DDD имел определенные структуры данных, они были сложные и объемные для некой универсальности в проекте в целом.
Про фронтенд
4мес. разработки фронта мы использовали JSON в качестве ответа от сервера, мапили в State Vuex в удобном нам формате. Но для отдачи на сервер нам требовалось преобразовывать в обратную сторону, чтобы сервер смог прочитать и замапить свои DTO объекты (может показаться странным, но так надо 🙂 )
Проблемы
Вроде бы ничего, работали с тем что есть, состояние разрасталось до объектов больших размеров. Начали разбивать на еще меньшие модули в каждом из которых были свои состояния, мутации и т.п… API стало меняться вслед новым задачам от менеджеров, и все сложнее стало управлять всем этим, то там замапили не так, то поля изменились…
И тут мы начали думать об универсальных структурах данных на сервере и фронте чтобы исключить ошибки в парсингах, мапингах и т.п.
После некоторых поисков, мы пришли к двум вариантам:
Хватит болтовни, давайте посмотрим как все это выглядит
Как это выглядит на стороне PHP я не буду описывать, там примерно все тоже самое, объекты те же.
Покажу на примере простого клиентского JS и мини сервера на Node.js.
Для начала описываем структуры данных которые нам потребуется. Дока.
Поясню немного про сервис, зачем он нужен, если даже не используется. Сервис описывается только ради документации в нашем случае, что принимает и что отдает, чтобы мы могли подставлять нужные объекты. Он нужен только для gRPC.
Далее скачивается генератор кода на основании структур.
И запускается команда генерации под JS.
После генерации появляется 3 JS файла, в которых уже все приведено к объектам, с функционалом сериализации в буфер и десериализации из буфера.
price_pb.js
product_pb.js
service_pb.js
Далее описываем уже JS код.
В принципе клиент готов.
На сервере заюзаем Express
Что мы имеем в итоге
Я взял lorem ipsum на 10 абзацев, получилось 5.5кб данных с учетом заполненных объектов Price, Product. И погонял данные по Protobuf и JSON (все тоже самое только заполненные JSON схемы, вместо Protobuf объектов)
Здравствуйте, а что такое Protocol Buffers?
Почему мне стоит использовать эту библиотеку вместо встроенных средств?
Так вы утверждаете, что protobuf не уступает бинарной сериализации и к тому же переносим?
namespace Proto.Sample
<
public enum TaskPriority
<
Low,
Medium,
High
>
[ Serializable ] //
[ProtoContract]
public class Task
<
[ProtoMember(1)]
public int Id
[ProtoMember(2)]
public DateTime CreatedAt
[ProtoMember(3)]
public string CreatedBy
[ProtoMember(4)]
public TaskPriority Priority
using System;
using System.Collections. Generic ;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using ProtoBuf;
const string file1 = «tasks1.bin» ;
TestBinaryFormatter(tasks, file1, 1000);
TestBinaryFormatter(tasks, file1, 2000);
TestBinaryFormatter(tasks, file1, 3000);
TestBinaryFormatter(tasks, file1, 4000);
TestBinaryFormatter(tasks, file1, 5000);
const string file2 = «tasks2.bin» ;
TestProtoBuf(tasks, file2, 1000);
TestProtoBuf(tasks, file2, 2000);
TestProtoBuf(tasks, file2, 3000);
TestProtoBuf(tasks, file2, 4000);
TestProtoBuf(tasks, file2, 5000);
for ( var i = 0; i var restoredTasks = ( List )formatter.Deserialize(file);
>
for ( var i = 0; i var restoredTasks = Serializer.Deserialize List >(file);
>
The test of binary formatter:
1000 iterations in 423 ms
2000 iterations in 381 ms
3000 iterations in 532 ms
4000 iterations in 660 ms
5000 iterations in 814 ms
The test of protobuf-net:
1000 iterations in 1056 ms
2000 iterations in 76 ms
3000 iterations in 129 ms
4000 iterations in 152 ms
5000 iterations in 202 ms
The comparision of file size:
The size of tasks1.bin is 710 bytes
The size of tasks2.bin is 101 bytes
var model = TypeModel.Create();
model.Add( typeof (Task), true );
var compiledModel = model.Compile(path);
compiledModel.Serialize(file, tasks);
Остальные тесты и результаты можно посмотреть здесь.
Ок. Относительно скорости и сжатия вы меня убедили, но как решается проблема переносимости?
Понимаете, если существуют реализация для нужной вам платформы, вопрос переносимости в большинстве случаев снимается. А реализациии protobuf существует для более чем 20 языков. Полный список можно увидеть здесь. Отмечу только, что для некоторых языков существует более одной реализации. Так что у вас всегда есть выбор.
Использование Google Protocol Buffers (protobuf) в Java
Привет, хабровчане. В рамках курса «Java Developer. Professional» подготовили для вас перевод полезного материала.
Недавно вышло третье издание книги «Effective Java» («Java: эффективное программирование»), и мне было интересно, что появилось нового в этой классической книге по Java, так как предыдущее издание охватывало только Java 6. Очевидно, что появились совершенно новые разделы, связанные с Java 7, Java 8 и Java 9, такие как глава 7 «Lambdas and Streams» («Лямбда-выражения и потоки»), раздел 9 «Prefer try-with-resources to try-finally» (в русском издании «2.9. Предпочитайте try-с-ресурсами использованию try-finally») и раздел 55 «Return optionals judiciously» (в русском издании «8.7. Возвращайте Optional с осторожностью»). Но я был слегка удивлен, когда обнаружил новый раздел, не связанный с нововведениями в Java, а обусловленный изменениями в мире разработки программного обеспечения. Именно этот раздел 85 «Prefer alternatives to Java Serialization» (в русском издании «12.1 Предпочитайте альтернативы сериализации Java») и побудил меня написать данную статью об использовании Google Protocol Buffers в Java.
В разделе 85 «Prefer alternatives to Java Serialization» (12.1 «Предпочитайте альтернативы сериализации Java») Джошуа Блох (Josh Bloch) выделяет жирным шрифтом следующие два утверждения, связанные с сериализацией в Java:
«Лучший способ избежать проблем, связанных с сериализацией, — никогда ничего не десериализовать».
«Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».
После описания в общих чертах проблем с десериализацией в Java и, сделав эти смелые заявления, Блох рекомендует использовать то, что он называет «кроссплатформенным представлением структурированных данных» (чтобы избежать путаницы, связанной с термином «сериализация» при обсуждении Java). Блох говорит, что основными решениями здесь являются JSON (JavaScript Object Notation) и Protocol Buffers (protobuf). Мне показалось интересным упоминание о Protocol Buffers, так как в последнее время я немного читал о них и игрался с ними. В интернете есть довольно много материалов по использованию JSON (даже в Java), в то время как осведомленность о Protocol Buffers среди java-разработчиков гораздо меньше. Поэтому я думаю, что статья об использовании Protocol Buffers в Java будет полезной.
На странице проекта Google Protocol Buffers описывается как «не зависящий от языка и платформы расширяемый механизм для сериализации структурированных данных». Также там есть пояснение: «Как XML, но меньше, быстрее и проще». И хотя одним из преимуществ Protocol Buffers является поддержка различных языков программирования, в этой статье речь пойдет исключительно про использование Protocol Buffers в Java.
album.proto
Несмотря на простоту приведенного выше определения формата протокола, в нем присутствует довольно много информации. В первой строке явно указано, что используется proto3 вместо proto2, используемого по умолчанию, если явно ничего не указано. Две строки, начинающиеся с option, указывают параметры генерации Java-кода (имя генерируемого класса и пакет этого класса) и они нужны только при использовании Java.
Ключевое слово «message» определяет структуру «Album», которую мы хотим представить. В ней есть четыре поля, три из которых строки (string), а одно — целое число (int32). Два из них могут присутствовать в сообщении более одного раза, так как для них указано зарезервированное слово repeated. Обратите внимание, что формат сообщения определяется независимо от Java за исключением двух option, которые определяют детали генерации Java-классов по данной спецификации.
Сгенерированный Java-класс AlbumProtos.java содержит более 1000 строк, и я не буду приводить его здесь, он доступен на GitHub. Среди нескольких интересных моментов относительно сгенерированного кода я хотел бы отметить отсутствие выражений import (вместо них используются полные имена классов с пакетами). Более подробная информация об исходном коде Java, сгенерированном protoc, доступна в руководстве Java Generated Code. Важно отметить, что данный сгенерированный класс AlbumProtos пока никак не связан с моим Java-приложением, и сгенерирован исключительно из текстового файла album.proto, приведенного ранее.
Прежде чем двигаться дальше, нам понадобится простой Java-класс для демонстрации Protocol Buffers. Для этого я буду использовать класс Album, который приведен ниже (код на GitHub).
Album.java
Создадим экземпляр Album с помощью следующего кода:
Чтение массива byte[] обратно в экземпляр Album может быть выполнено следующим образом:
Здесь мы видим, что в обоих экземплярах соответствующие поля одинаковы и эти два экземпляра действительно разные. При использовании Protocol Buffers, действительно, нужно сделать немного больше работы, чем при «почти автоматическом» механизме сериализации Java, когда надо просто наследоваться от интерфейса Serializable, но есть важные преимущества, которые оправдывают затраты. В третьем издании книги Effective Java («Java: эффективное программирование») Джошуа Блох обсуждает уязвимости безопасности, связанные со стандартной десериализацией в Java, и утверждает, что «Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».
Смотреть открытый вебинар на тему «gRPC для микросервисов или не REST-ом единым».
Темная сторона protobuf
В среде разработчиков часто бытует мнение, что протокол сериализации protobuf и его реализация — это особая, выдающаяся технология, способная решить все реальные и потенциальные проблемы с производительность одним фактом своего применения в проекте. Возможно на такое восприятие влияет простота применения этой технологии и авторитет самой компании Google.
К сожалению, на одном из проектов мне пришлось вплотную столкнуться с некоторыми особенностями, которые никак не упоминаются в рекламной документации, однако сильно влияют на технические характеристики проекта.
Все последующее изложение касается только реализации protobuf на платформе Java. Также в основном описана версия 2.6.1, хотя в уже выпущенной версии 3.0.0 принципиальных изменений я также не увидел.
Также обращаю факт, что статья не претендует на полноту обзора. Про хорошие стороны технологии (например, это мультиязычность и отличная документация) можно почитать на официальном сайте. Эта статья рассказывает только про проблемы и, возможно, позволит принять более взвешенное решение. Одна часть проблем относится к самому формату, другая часть проблем относится к реализации. Также нужно уточнить, что большинство упомянутых тут проблем проявляются при определённых условиях.
maven-проект с уже подключенными зависимостями для самостоятельного исследования можно взять на github.
0. Необходимость препроцессинга
Это наименьшая проблема, даже не хотел включать ее в перечень, но для полноты пусть будет упомянута. Для того чтобы получить java-код необходимо запустить компилятор protoc. Некоторая проблема есть в том, что этот компилятор представляет собой нативное приложение и на каждой из платформ исполняемый файл будет своим, поэтому обойтись простым подключением maven-плагина не получится. Как минимум нужна переменная окружения на машинах разработчиков и на CI-сервере, которая будет указывать на исполняемый файл, и после этого его уже можно запускать из maven/ant сценария.
Как вариант, можно сделать maven-pluging, который держит в ресурсах все бинарники и распаковывает из себя нужный под текущую платформу в временную папку, откуда и запускает его. Не знаю, может такой кто-то уже и сделал.
В общем, невелик грех, поэтому простим.
1. Непрактичный код
К сожалению, для платформы Java генератор protoc производит очень непрактичный код. Вместо того, чтобы сгенерировать чистенькие anemic-контейнеры и отдельно сериализаторы к ним, генератор упихивает все в один большой класс с подклассами. Генерируемые бины нельзя ни внедрить в свою иерархию, ни даже банально заимплементировать интерфейс java.util.Serializable для спихивания бинов на куда-нибудь сторону. В общем они годятся только в качестве узкоспециализированных DTO. Если вас это устраивает — то это и не проблема вовсе, только не заглядывайте внутрь.
2. Излишнее копирование — низкая производительность
Собственно вот тут у меня начались уже совершенно объективные проблемы. Генерируемый код для каждой описываемой сущности (назовем ее «Bean») создает два класса (и один интерфейс, но он не важен в данном контексте). Первый класс — это immutable Bean который представляет собой read-only слепок данных, второй класс — это mutable Bean.Builder, который уже можно править и устанавливать значения.
Зачем так сделано, осталось непонятным. Кто-то говорит, что авторы входят в секту адептов ФП; кто-то утверждает что так они пытались избавится от циклических зависимостей при сериализации (как это им помогло?); кто-то говорит, что protobuf первой версии работал только с mutable-классами, а глупые люди стреляли при этом себе в ноги.
Можно было бы сказать, что на вкус и цвет архитектуры разные, но при таком дизайне для того чтобы получить байтовое представление вам нужно создать Bean.Builder, заполнить его, затем вызвать метод build(). Для того чтобы изменить бин, нужно создать его билдер через метод toBuilder(), изменить значение и затем вызвать build().
И все ничего, только при каждом вызове build() и toBuilder() происходит копирование всех полей из экземпляра одного класса в экземпляр другого класса. Если все что вам нужно — это получить байтовый массив для сериализации или изменить пару полей, то это копирование сильно мешает. Кроме того, в этом методе похоже (я сейчас выясняю) присутствует многолетняя проблема, которая приводит к тому, что копируются даже те поля, значения которых даже не были установлены в билдере.
Вы вряд ли заметите это, если у вас мелкие бины с небольшим количеством полей. Однако мне в наследство досталась целая библиотека, количество полей в отдельных бинах которой достигало трех сотен. Вызов метода build() для такого бина занимает около 50мкс в моем случае, что позволяет обработать не более 20000 бинов в секунду.
Ирония в том, что в моем случае другие тесты показывают, что сохранение подобного бина через Jackson/JSON в два-три раза быстрее (в случае если проинициализированы не все поля и большую часть полей можно не сериализовать).
3. Потеря ссылочности
Если у вас есть графоподобная структура, в которой бины ссылаются друг на друга, то у меня для вас плохая новость — protobuf не подходит для сериализации таких структур. Он сохраняет бины по-значению, не отслеживая факт того, что этот бин уже был сериализован.
Другими словами если у вас есть bean1 и bean2, которые ссылаются друг на друга, то при сериализации-десериализации вы получите bean1, который ссылается на бин bean3; а также bean2, который ссылается на бин bean4.
Уверен, что в подавляющем большинстве случаев такая функциональность не нужна и даже противопоказана в простых DTO. Однако эта проблема проявляется и в более естественных случаях. Например, если вы добавите один и тот же бин в коллекцию 100 раз, он будет сохранен все 100 раз, а не одиножды. Или вы сериализуете список лотов (товаров). Каждый из лотов представляет собой мелкий бин с описанием (количество, цена, дата), а также со ссылкой на развесистое описание продукта. Если сохранять в лоб, то описание продукта будет сериализовано столько раз, сколько существует лотов, даже если все лоты указывают на один и тот же продукт. Решением этой проблемы будет отдельное сохранение продуктов в виде словаря, но это уже дополнительные действия — и при сериализации, и при десереализации.
Описанное поведение является абсолютно ожидаемым и естественным для текстовых форматов типа JSON/XML. Но вот от бинарного формата ожидаешь несколько другого, тем более, что штатная сериализация Java в этом плане работает ровно так, как и ожидается.
4. Компактность под вопросом
Бытует мнение, что protobuf является суперкомпактным форматом. На самом деле компактность сериализации обеспечивается всего несколькими факторами:
Предположим, приложение генерирует такие данные, что в процентном соотношении в байтовом представлении строки занимают 75%, а примитивы занимают 25%. В таком случае, даже если наш алгоритм оптимизации примитивов сократит необходимое для их хранения место до нуля, мы получим экономию всего в 1/4.
В некоторых случаях компактность сериализация является весьма критичной, например для мобильных приложений в условиях плохой/дорогой связи. В таких случаях без дополнительной компрессии поверх protobuf не обойтись, иначе мы будем впустую гонять избыточные данные в строках. Но тогда вдруг выясняется, что аналогичный комплект [JSON+GZIP] при сериализации дает несильно больший размер по сравнению с [PROTOBUF+ZIP]. Конечно, вариант [JSON+GZIP] будет также потреблять больше ресурсов CPU при работе, но в тоже время, он зачастую также является еще и более удобным.
protoc v3
В protobuf третьей версии появился новый режим генерации «Java Nano». Его еще нет в документации, а runtime этого режима еще в стадии alpha, но пользоваться им можно уже сейчас при помощи переключателя «—javanano_out».
В этом режиме генератор создает анемичные бины с публичными полями (без сеттеров и без геттеров) и с простыми методами сериализации. Лишнего копирования нет, поэтому проблема #2 решена. Остальные проблемы остались, более того при наличии циклических ссылок сериализатор выпадает в StackOverflowError.
Принятие решения о сериализации каждого поля производится на основании его текущего значения, а не отдельной битовой маски, что несколько упрощает сами бины.
protostuff
Альтернативная реализация протокола protobuf. В бою не испытывал, но на первый взгляд выглядит очень добротно. Не требует proto-файлов (однако умеет с ними работать, если это необходимо), поэтому решены проблемы #0, #1 и #2. Кроме этого умеет сохранять в свой собственный формат, а также в JSON, XML и YAML. Также интересной является возможность перегонять данные из одного формата в другой потоком, без необходимости полной десериализации в промежуточный бин.
К сожалению, если отдать на сериализацию обычный POJO без схемы, аннотаций и без proto-файлов (так тоже можно), protostuff будет сохранять все поля объекта подряд, в независимости от того были они проинициализированы значением или нет, а это снова сильно бьет по компактности в случае, когда заполнены не все поля. Но насколько я вижу, такое поведение при желании можно подправить, переопределив пару классов.
Protobuffers — это неправильно
Значительную часть своей профессиональной жизни я выступаю против использования Protocol Buffers. Они явно написаны любителями, невероятно узкоспециализированы, страдают от множества подводных камней, сложно компилируются и решают проблему, которой на самом деле нет ни у кого, кроме Google. Если бы эти проблемы протобуферов остались в карантине абстракций сериализации, то мои претензии на этом и закончились бы. Но, к сожалению, плохой дизайн Protobuffers настолько навязчив, что эти проблемы могут просочиться и в ваш код.
Узкая специализация и разработка любителями
Остановитесь. Закройте свой почтовый клиент, где уже написали мне полписьма о том, что «в Google работают лучшие в мире инженеры», что «их разработки по определению не могут быть созданы любителями». Не хочу этого слышать.
Давай просто не будем обсуждать эту тему. Полное раскрытие: мне доводилось работать в Google. Это было первое (но, к сожалению, не последнее) место, где я когда-либо использовал Protobuffers. Все проблемы, о которых я хочу поговорить, существуют в кодовой базе Google; это не просто «неправильное использование протобуферов» и тому подобная ерунда.
Безусловно, самая большая проблема с Protobuffers — ужасная система типов. Поклонники Java должны чувствовать себя здесь как дома, но, к сожалению, буквально никто не считает Java хорошо спроектированной системой типов. Ребята из лагеря динамической типизации жалуются на излишние ограничения, в то время как представители лагеря статической типизации, вроде меня, жалуются на излишние ограничения и отсутствие всего того, что вы на самом деле хотите от системы типов. Проигрыш в обоих случаях.
Узкая специализация и разработка любителями идут рука об руку. Многое в спецификациях словно прикручено в последний момент — и оно явно было прикручено в последний момент. Некоторые ограничения заставят вас остановиться, почесать голову и спросить: «Какого чёрта?» Но это всего лишь симптомы более глубокой проблемы:
Очевидно, протобуферы созданы любителями, потому что предлагают плохие решения широко известных и уже решённых проблем.
Отсутствие композиционности
Protobuffers предлагают несколько «фич», которые не работают друг с другом. Например, посмотрите на список ортогональных, но в то же время ограниченных функций типизации, которые я нашёл в документации.
Ваши догадки о проблеме с enum так же верны, как и мои.
Что так расстраивает во всём этом, так это слабое понимание, как работают современные системы типов. Это понимание позволило бы кардинально упростить спецификацию Protobuffers и одновременно удалить все произвольные ограничения.
Решение заключается в следующем:
Например, можно переделать поля optional :
Создание полей repeated тоже просто:
Конечно, реальная логика сериализации позволяет делать что-то умнее, чем пушить связанные списки по сети — в конце концов, реализация и семантика не обязательно должны соответствовать друг другу.
Сомнительный выбор
Конечно же, в двух разновидностях типов совершенно разная семантика.
Невозможно отличить поле, которое отсутствовало в протобуфере, от поля, которому присвоено значение по умолчанию. Предположительно, это решение сделано для оптимизации, чтобы не пересылать скалярные значения по умолчанию. Это лишь предположение, потому что в документации не упоминается эта оптимизация, так что ваше предположение будет не хуже моего.
Когда будем обсуждать претензии Protobuffers на идеальное решение для обратной и будущей совместимости с API, мы увидим, что эта неспособность различать неустановленные значения и значения по умолчанию — настоящий кошмар. Особенно если это действительно сознательное решение, чтобы сохранить один бит (установлено или нет) для поля.
Сравните это поведение с типами сообщений. В то время как скалярные поля являются «тупыми», поведение полей сообщений совершенно безумно. Внутренне, поля сообщений либо есть, либо их нет, но поведение сумасшедшее. Небольшой псевдокод для их аксессора стоит тысячи слов. Представьте такое в Java или где-то ещё:
Такое поведение особенно вопиюще, потому что оно нарушает закон! Мы ожидаем, что задание msg.foo = msg.foo; не будет работать. Вместо этого реализация фактически втихаря изменяет msg на копию foo с инициализацией нулями, если её раньше не было.
(но макросы препроцессора запрещены руководством по стилю Google).
Это не очень приятно слышать, тем более тем из нас, кто любит параметрический полиморфизм, который обещает в точности противоположное.
Ложь обратной и будущей совместимости
Одна из часто упоминаемых «киллер-фич» Protobuffers — их «беспроблемная способность писать обратно- и вперёд-совместимые API». Это утверждение повесили у вас перед глазами, чтобы заслонить правду.
Что Protobuffers являются разрешительными. Им удаётся справиться с сообщениями из прошлого или будущего, потому что они не дают абсолютно никаких обещаний, как будут выглядеть ваши данные. Всё опционально! Но если вам это нужно, Protobuffers с удовольствием приготовит и подаст вам что-то с проверкой типов, независимо от того, имеет ли это смысл.
Это означает, что Protobuffers выполняют обещанные «путешествия во времени», втихую делая неправильные вещи по умолчанию. Конечно, осторожный программист может (и должен) написать код, выполняющий проверку корректности полученных протобуферов. Но если на каждом сайте писать защитные проверки корректности, может, это просто означает, что шаг десериализации был слишком разрешительным. Всё, что вам удалось сделать, это децентрализовать логику проверки корректности с чётко определённой границы и размазать её по всей кодовой базе.
Один из возможных аргументов — что протобуферы сохранят в сообщении любую информацию, которую не понимают. В принципе, это означает неразрушающую передачу сообщения через посредника, который не понимает эту версию схемы. Это же явная победа, не так ли?
Конечно, на бумаге это классная функция. Но я ни разу не видел приложения, где действительно сохраняется это свойство. За исключением программного обеспечения для маршрутизации, ни одна программа не хочет проверять только некоторые биты сообщения, а затем пересылать его в неизменном виде. Подавляющее большинство программ на протобуферах будут декодировать сообщение, трансформировать его в другое и отправлять в другое место. Увы, эти преобразования делаются на заказ и кодируются вручную. И ручные преобразования из одного протобуфера в другой не сохраняют неизвестные поля, потому что это буквально бессмысленно.
Это повсеместное отношение к протобуферам как универсально совместимым проявляется и другими уродливыми способами. Руководства по стилю для Protobuffers активно выступают против DRY и предлагают по возможности встраивать определения в код. Они аргументируют тем, что это позволит в будущем использовать отдельные сообщения, если определения разойдутся. Подчеркну, они предлагают отказаться от 60-летней практики хорошего программирования на всякий случай, вдруг когда-то в будущем вам потребуется что-то изменить.
Корень проблемы в том, что Google объединяет значение данных с их физическим представлением. Когда вы находитесь в масштабе Google, такое имеет смысл. В конце концов, у них есть внутренний инструмент, который сравнивает почасовую оплату программиста с использованием сети, стоимостью хранения X байтов и другими вещами. В отличие от большинства технологических компаний, зарплата программистов — одна из самых маленьких статей расходов Google. Финансово для них имеет смысл тратить время программистов, чтобы сэкономить пару байтов.
Кроме пяти ведущих технологических компаний, больше никто не находится в пределах пяти порядков масштаба Google. Ваш стартап не может позволить тратить инженерные часы на экономию байтов. Но экономия байтов и трата времени программистов в процессе — это именно то, для чего оптимизированы Protobuffers.
Давайте посмотрим правде в глаза. Вы не соответствуете масштабу Google, и никогда не будете соответствовать. Прекратите карго-культ использования технологии только потому, что «Google использует её», и потому что «это лучшие отраслевые практики».
Protobuffers загрязняет кодовые базы
Если бы можно было ограничить использование Protobuffers только сетью, я бы не высказывался так жёстко об этой технологии. К сожалению, хотя в принципе существует несколько решений, ни одно из них не достаточно хорошо, чтобы фактически использоваться в реальном программном обеспечении.
Protobuffers соответствуют данным, которые вы хотите отправить по каналу связи. Они часто соответствуют, но не идентичны фактическим данным, с которыми приложение хотело бы работать. Это ставит нас в неудобное положение, необходимо выбирать между одним из трёх плохих вариантов:
Вместо этого код, использующий протобуферы, позволяет им распространяться по всей кодовой базе. Это реальность. Моим основным проектом в Google был компилятор, который брал «программу», написанную на одной разновидности Protobuffers, и выдавал эквивалентную «программу» на другой. Форматы ввода и вывода достаточно отличались, чтобы их правильные параллельные версии C++ никогда не работали. В результате мой код не мог использовать ни одну из богатых техник написания компиляторов, потому что данные Protobuffers (и сгенерированный код) были слишком жёстким, чтобы сделать с ними что-нибудь интересное.
В результате вместо 50 строк схем рекурсии использовались 10 000 строк специального тасования буфера. Код, который я хотел написать, был буквально невозможен при наличии протобуферов.
Хотя это один случай, он не уникален. В силу жёсткой природы генерации кода, проявления протобуферов в языках никогда не будут идиоматическими, и их невозможно сделать такими — разве что переписать генератор кода.
Но даже тогда у вас останется проблема встроить дерьмовую систему типов в целевой язык. Поскольку большинство функций Protobuffers плохо продуманы, эти сомнительные свойства просачиваются в наши кодовые базы. Это означает, что мы вынуждены не только реализовывать, но и использовать эти плохие идеи в любом проекте, который надеется взаимодействовать с Protobuffers.
На прочной основе легко реализовать бессмысленные вещи, но если пойти в другом направлении, в лучшем вы столкнётесь со сложностями, а в худшем — с настоящим древним ужасом.
В общем, оставь надежду каждый, кто внедрит Protobuffers в свои проекты.