Комментарии 32
Вопрос, а какие есть альтернативы, которые обеспечат такую же статическую типизацию (на основе кодогенерации) и производительность?
Смотря для каких целей. И если есть поддержка языков.
Гонять данные в/из браузера — отлично.
Между системами — есть поддержка множества типов, можно добавить свои,
передал дату-время — получил дату-время.
Вы серьезно?))
А в чем вопрос?
А тем кому надо передать дату-время туда-сюда protobuf не нужен.
Еще и мою любимую Java обидел )) Она же никого не заставляет писать на ней, не нравиться не пиши ))
Это же относится к следующему пассажу.
Вариант 1 — однозначно «правильное» решение, но оно непригодно для Protobuffers. Язык недостаточно мощный для кодирования типов, которые могут выполнять двойную работу в двух форматах. Это означает, что вам придётся написать совершенно отдельный тип данных, развивать его синхронно с Protobuffers и специально писать код сериализации для них. Но поскольку большинство людей, кажется, используют Protobuffers, чтобы не писать код сериализации, такой вариант, очевидно, никогда не реализуется.
В зависимости от приложения и ситуации вариант №1 может оказаться единственно возможным решением, а где-то применим вариант №2 или №3.
Теперь касательно «большинство людей, кажется, используют Protobuffers, чтобы не писать код сериализации».
Проблема не в том чтобы написать код сериализации. Написать код сериализации сама по себе простая задача, тем более если весь код заключается в копировании данных из «обыкновенного» класса в сгенерированный протобуфом. Основная проблема сериализации заключается как раз в поддержании совместимости между старыми и новыми версиями классов и данных. И здесь протобуф справляется очень хорошо, пусть и путём ввода некоторых ограничений и фишек вроде required и optional.
Например, boost::serialization справляется с задачей обратной совместимости, но поддержки прямой совместимости нет, хотя соответствующие багрепорты открыты давно. Мне пришлось написать специальную библиотеку для того чтобы реализовать прямую совместимость хотя бы отчасти.
пусть и путём ввода некоторых ограничений и фишек вроде required и optional.Ващет выпилили. Теперь все филды всегда optional.
Есть еще древнючий XDR, но при этом весьма адекватный.
https://ru.wikipedia.org/wiki/External_Data_Representation
Пользую protobuf 2 и 3, как и позднее grpc, много лет в разных проектах на С++, меньше на python. Проблемы автора, признаться, не очень понятны.
Поле не может быть repeated? Оберни его в message, и оно сможет, оверхед в С++ будет примерно нулевой.
Значения по умолчанию? 0, и ничего другого, RTFM.
Не идеально, конечно же, в том же С++ коде профайлер показывает огромное, по сравнению на пример с flatbuffers, количество выделений памяти. Но покажите мне что-нибудь получше, чтоб из коробки понимало хотя бы С++, java, python? Go, С# и Rust желательны, но сейчас не обязательны :)
Смешаны в кучу претензии к спецификации схемы и к reference implementation кодогенератора. Никто не заставляет использовать protoc от Google.
Лично моё мнение:
- Заточенность системы типов на типичные случаи использования — это не так уж и плохо. Почти все описанные проблемы решаются обёртыванием в отдельный тип.
- Работа с опциональными типами в сгенерированном гугловым компилятором коде действительно ужасна: эти hasFoo() и getFoo() с дефолтными значениями — прямой путь к неожиданному поведению кода вместо вылета NullPointerException. Значение по умолчанию практически никогда не имеет смысла — какие полезные операции можно сделать с объектом, у которого во всех полях нули, пустые строки и вложенные такие же пустые объекты? Это выглядит дико даже в Java, не говоря о языках со встроенными средствами работы с опциональными значениями.
- Proto3 пошёл ещё дальше и теперь такая же ситуация в спецификации, так как required полей больше нет. А уж что там происходит с enum — это вообще нонсенс. Не указал значение — получаешь первое объявленное. И нет способа узнать, было ли оно установлено или это "дефолтное". В результате десериализации можно получить всё что угодно — любое поле могло быть не задано и код будет по-тихому работать не так, как задумано.
По мне так такая "схема данных" — это просто мусор.
То, что из-под крыла Google выходит нечто сомнительного качества, я вижу не в первый раз. Впрочем, что ещё можно ожидать от большой корпорации, в которой разными продуктами занимаются совершенно разные люди с разными целями, умениями и бюджетами.
Конечно, реальная логика сериализации позволяет делать что-то умнее, чем пушить связанные списки по сети — в конце концов, реализация и семантика не обязательно должны соответствовать друг другу.
Вот только в protobuf-описании задается именно реализация, а не семантика! Одно из требований к библиотеке для сериализации — это бинарная совместимость с другими библиотеками сериализации работающими с тем же самым форматом.
А значит, если определить список через сопродукт — то он будет передан по сети именно как сопродукт. То есть без указания количества элементов и с обязательным разделителем между элементами. И никакой оптимизации тут добавить нельзя, потому что формат сообщений не должен зависеть от настроек оптимизации.
Невозможно отличить поле, которое отсутствовало в протобуфере, от поля, которому присвоено значение по умолчанию.
Так ведь так и задумано же. И это сделано не для хитрых оптимизаций, а ради возможности обновлять протокол оставаясь совместимым со старой версией.
Protobuffers в духе Java различает скалярные типы и типы сообщений. Конечно же, в двух разновидностях типов совершенно разная семантика.
Вот как автор это углядел? Я, напротив, вижу что приведенный псевдокод написан как раз из желания придать сообщениям скалярную семантику. Именно потому null "втихую" и заменяется на пустое сообщение.
Но если вы измените foo, он также изменит своего родителя!
Обычное поведение ссылочных типов данных в императивных языках. При чем тут вообще protobuf?
Мы ожидаем, что задание msg.foo = msg.foo; не будет работать.
Так оно и не работает...
Обратите внимание, что, по крайней мере, в языках со статической типизацией, этот шаблон нельзя абстрагировать из-за номинальной связи между методами foo(), set_foo() и has_foo()
И чем же Reflection и FieldDescriptor — не абстракция? А ведь можно еще и свои кодогенераторы подключать...
Чтобы сменить тему, поговорим о другом сомнительном решении. Хотя вы можете в протобуферах определить поля oneof, их семантика не соответствует типу сопродукта! Ошибка новичка, парни! Вместо этого вы получаете опциональное поле для каждого случая oneof и магический код в сеттерах, который просто отменит любое другое поле, если это установлено.
Интересно, а какая еще возможна нормальная реализация сопродукта на C++? std::variant
, к примеру, при некорректном обращении кидает исключение — то-то радости будет программисту, который не может уследить за тем, какие свойства он читает...
Мы сейчас будем писать интеграцию где вынуждены использовать Protobuf - эта статья конечно подкинула дров в топку сомнения )) Спасибо за то что поделился горьким опытом, но за что вы так с Java ? Все там хорошо со строгой типизацией )) И динамическая типизация вроде бы с 17 Java уже вполне приличная))
Protobuffers — это неправильно