بالنسبة لمعظم حياتي المهنية ، أنا أعارض استخدام المخازن المؤقتة للبروتوكول. لقد كتبوا بوضوح من قبل الهواة ، المتخصصين بشكل لا يصدق ، يعانون من العديد من المزالق ، ويصعب تجميعها وحل مشكلة لا يوجد لدى أي شخص سوى Google. إذا بقيت مشاكل المخازن المؤقتة الأولية في الحجر الصحي للتسلسلات التجريدية ، فستنتهي ادعاءاتي عند هذا الحد. ولكن لسوء الحظ ، فإن التصميم الضعيف لـ Protobuffers متطفل للغاية بحيث يمكن لهذه المشاكل أن تتسرب إلى التعليمات البرمجية الخاصة بك.
التخصص والتنمية الضيقة من قبل الهواةتوقف أغلق برنامج البريد الإلكتروني الخاص بك ، حيث كتبت لي رسالة تقول فيها "أفضل المهندسين في العالم يعملون في Google" ، "إن تصميماتهم ، بحكم تعريفها ، لا يمكن إنشاؤها بواسطة الهواة". لا اريد سماع ذلك
دعنا لا نناقش هذا الموضوع. الإفشاء الكامل: كنت أعمل في Google. كان هذا هو المكان الأول (ولكن للأسف ليس الأخير) الذي استخدمته على الإطلاق Protobuffers. جميع المشاكل التي أريد التحدث عنها موجودة في Google codebase ؛ انها ليست مجرد "إساءة استخدام protobuffers" وما شابه ذلك.
إلى حد بعيد ، أكبر مشكلة مع Protobuffers هي نظام النوع المروع. يجب أن يشعر محبو جافا بأنهم في المنزل هنا ، ولكن للأسف لا أحد يعتقد أن جافا هو نظام نوع مصمم جيدًا. يشكو الرجال من معسكر الكتابة الديناميكي من القيود غير الضرورية ، بينما يشكو ممثلو معسكر الكتابة الثابتة ، مثلي ، من القيود غير الضرورية ونقص كل ما تريده حقًا من نظام النوع. الخسارة في الحالتين.
التخصص الضيق والتطوير من قبل الهواة يسيران جنبًا إلى جنب. يبدو أن معظم المواصفات تم تثبيتها في اللحظة الأخيرة - ومن الواضح أنها كانت مثبتة في اللحظة الأخيرة. سوف تجبرك بعض القيود على التوقف ، وتخدش رأسك وتسأل: "ماذا بحق الجحيم؟" لكن هذه مجرد أعراض لمشكلة أعمق:
من الواضح أن هواة صنع النماذج الأولية هم من صنع الهواة لأنهم يقدمون حلولاً ضعيفة للمشكلات المعروفة والتي تم حلها بالفعل.
نقص التكوين
تقدم Protobuffers العديد من الميزات التي لا تعمل مع بعضها البعض. على سبيل المثال ، انظر إلى قائمة التعامد ، ولكن في نفس الوقت وظائف الكتابة المحدودة التي وجدتها في الوثائق.
- لا يمكن
repeated
حقول oneof
. - تحتوي
map<k,v>
الحقول map<k,v>
على بناء جملة خاص للمفاتيح والقيم ، ولكن لا يتم استخدامه في أي أنواع أخرى. - على الرغم من إمكانية
map
معلمات حقول map
، لم يعد مسموحًا بأي نوع معرف من قبل المستخدم. هذا يعني أنك عالق في تحديد تخصصاتك يدويًا في هياكل البيانات المشتركة. - لا يمكن
repeated
حقول map
. - يمكن أن تكون مفاتيح
map
string
، ولكن ليس bytes
. التعداد ممنوع أيضًا ، على الرغم من أن الأخير يعتبر معادلاً للأعداد الصحيحة في جميع الأجزاء الأخرى من مواصفات Protobuffers. - لا يمكن أن تكون قيم
map
أخرى.
هذه القائمة المجنونة من القيود هي نتيجة اختيار غير مبدئي لوظائف التصميم والشد في اللحظة الأخيرة. على سبيل المثال ، لا يمكن
repeated
حقول
oneof
، لأنه بدلاً من نوع جانبي ، سينتج منشئ الشفرة حقول اختيارية حصرية متبادلة. مثل هذا التحول صالح فقط لحقل فردي (وكما سنرى لاحقًا ، فإنه لا يعمل حتى بالنسبة له).
يقتصر تقييد حقول
map
، التي لا يمكن
repeated
، على نفس الأوبرا تقريبًا ، ولكنه يُظهر تقييدًا مختلفًا لنظام النوع. خلف الكواليس ، تتحول
map<k,v>
إلى شيء مشابه
repeated Pair<k,v>
. وبما أن الكلمة
repeated
هي الكلمة السحرية للغة ، وليست النوع العادي ، فهي لا تتحد مع نفسها.
إن تخميناتك حول مشكلة
enum
صحيحة كما هي
enum
.
ما هو محبط للغاية في كل هذا هو فهم ضعيف لكيفية عمل أنظمة الكتابة الحديثة. هذا الفهم سوف
يبسط بشكل كبير مواصفات Protobuffers وفي نفس الوقت
إزالة جميع القيود التعسفية .
الحل كما يلي:
- قم بعمل جميع الحقول في الرسالة
required
. هذا يجعل كل رسالة نوع منتج. - رفع قيمة الحقل
oneof
لأنواع البيانات المستقلة. سيكون هذا نوع منتج مشترك. - لتمكين تحديد أنواع المنتجات والمنتجات المشتركة من أنواع أخرى.
هذا كل شيء! هذه التغييرات الثلاثة هي كل ما تحتاجه لتحديد أي بيانات محتملة. مع هذا النظام البسيط ، يمكنك إعادة جميع مواصفات Protobuffers الأخرى.
على سبيل المثال ، يمكنك إعادة الحقول
optional
:
product Unit { // no fields } coproduct Optional<t> { t value = 0; Unit unset = 1; }
إنشاء الحقول
repeated
أمر بسيط أيضًا:
coproduct List<t> { Unit empty = 0; Pair<t, List<t>> cons = 1; }
بالطبع ، يسمح لك المنطق الحقيقي للتسلسل أن تفعل شيئًا أكثر ذكاءً من دفع القوائم المرتبطة عبر الشبكة - بعد كل شيء ،
لا يجب أن يتوافق التطبيق والدلالات مع بعضها البعض .
خيار مريب
تميز برامج protobuffers بنمط Java بين الأنواع
العددية وأنواع
الرسائل . تتوافق المقاييس بشكل أو بآخر مع البدائية الآلية - أشياء مثل
int32
و
bool
و
string
. أنواع الرسائل ، من ناحية أخرى ، هي الباقي. جميع أنواع المكتبة والمستخدمين هي رسائل.
بالطبع ، لهذين النوعين دلالات مختلفة تمامًا.
دائمًا ما توجد الحقول ذات الأنواع العددية. حتى لو لم تقم بتثبيتها. سبق أن قلت ذلك (على الأقل في proto3
1 ) هل تم تهيئة جميع المخازن المؤقتة الأولية على أصفار ، حتى إذا لم يكن لديهم بيانات على الإطلاق؟ تحصل الحقول العددية على قيم مزيفة: على سبيل المثال ،
uint32
تهيئة
uint32
إلى
0
،
string
تهيئة
string
إلى
""
.
لا يمكن تمييز حقل لم يكن في المخزن المؤقت الأولي من حقل تم تعيينه على قيمة افتراضية. من المفترض أن هذا القرار تم اتخاذه من أجل التحسين حتى لا يتم توجيه التخلفيات العددية. هذا مجرد افتراض ، لأن الوثائق لا تذكر هذا التحسين ، لذلك لن يكون افتراضك أسوأ من تقديري.
عندما نناقش مطالبات Protobuffers للحل المثالي للتوافق مع الإصدارات السابقة والمستقبلية من واجهة برمجة التطبيقات ، سنرى أن عدم القدرة على التمييز بين القيم غير المحددة والافتراضية هو كابوس حقيقي. خاصة إذا كان قرارًا واعيًا حقًا توفير جزء واحد (تم تعيينه أم لا) للحقل.
قارن هذا السلوك بأنواع الرسائل. في حين أن الحقول العددية "غبية" ، فإن سلوك حقول الرسائل
مجنون تمامًا. داخليا ، حقول الرسالة موجودة أم لا ، لكن السلوك مجنون. يساوي رمز زائف صغير لموصلهم ألف كلمة. تخيل هذا في جافا أو في مكان آخر:
private Foo m_foo; public Foo foo { // only if `foo` is used as an expression get { if (m_foo != null) return m_foo; else return new Foo(); } // instead if `foo` is used as an lvalue mutable get { if (m_foo = null) m_foo = new Foo(); return m_foo; } }
نظريًا ، إذا لم يتم تعيين حقل
foo
، فسترى نسخة مبدئية افتراضية ، سواء طلبت ذلك أم لا ، ولكن لا يمكنك تغيير الحاوية. ولكن إذا قمت بتغيير
foo
، فإنه سيتم تغيير الأصل أيضًا! كل هذا فقط لتجنب استخدام نوع
Maybe Foo
و "الصداع" المرتبط به لمعرفة ما يجب أن تعنيه قيمة غير محددة.
مثل هذا السلوك فاضح بشكل خاص لأنه ينتهك القانون! نتوقع الوظيفة
msg.foo = msg.foo;
لن يعمل. بدلاً من ذلك ، يغير التطبيق في الواقع بهدوء
msg
إلى نسخة من
foo
مع التهيئة الصفرية إذا لم تكن موجودة من قبل.
على عكس الحقول العددية ، يمكنك على الأقل تحديد عدم تعيين حقل الرسالة. تقدم الارتباطات اللغوية لـ protobuffers شيئًا مثل طريقة
bool has_foo()
إنشاؤها
bool has_foo()
. إذا كان موجودًا ، في حالة النسخ المتكرر لحقل الرسالة من protobuffer واحد إلى آخر ، يجب عليك كتابة الرمز التالي:
if (src.has_foo(src)) { dst.set_foo(src.foo()); }
يرجى ملاحظة أنه ، على الأقل في اللغات ذات الكتابة الثابتة ،
لا يمكن تجريد هذا القالب بسبب العلاقة الاسمية بين
has_foo()
foo()
و
set_foo()
و
has_foo()
. نظرًا لأن جميع هذه الوظائف هي معرفاتها الخاصة ، فليس لدينا الوسائل اللازمة لتوليدها برمجيًا ، باستثناء الماكرو المعالج المسبق:
#define COPY_IFF_SET(src, dst, field) \ if (src.has_##field(src)) { \ dst.set_##field(src.field()); \ }
(ولكن تحظر وحدات ماكرو المعالجات المسبقة بواسطة دليل أنماط Google).
بدلاً من ذلك ، إذا تم تنفيذ جميع الحقول الإضافية كـ
Maybe
، يمكنك تعيين نظير الاتصال المستخلص بأمان.
لتغيير الموضوع ، دعونا نتحدث عن قرار مشبوه آخر. على الرغم من أنه يمكنك تحديد حقل واحد في
oneof
، فإن دلالاتها
لا تتطابق مع نوع المنتج المشترك! خطأ مبتدئ الرجال! بدلاً من ذلك ، تحصل على حقل اختياري لكل حالة من الحالات ورمز سحري في وحدات التوطين ، والذي سيؤدي ببساطة إلى التراجع عن أي حقل آخر إذا تم تعيينه.
للوهلة الأولى ، يبدو أن هذا يجب أن يكون معادلاً لغويًا للنوع الصحيح من النقابات. ولكن بدلاً من ذلك ، نحصل على مصدر خطأ مثير للاشمئزاز لا يوصف! عندما يتم دمج هذا السلوك مع تنفيذ غير قانوني
msg.foo = msg.foo;
، مثل هذه المهمة التي تبدو طبيعية تحذف بصمت كميات عشوائية من البيانات!
نتيجة لذلك ، هذا يعني أن أحد الحقول لا يشكل
Prism
يحترم القانون ، ولا تشكل الرسائل
Lens
ملتزمة بالقانون. حظًا موفقًا في محاولاتك لكتابة تلاعبات بروتوبوفير غير بسيطة دون أخطاء.
من المستحيل حرفيًا كتابة رمز متعدد الأشكال عالمي خالٍ من الأخطاء على أدوات الانتقاء الأولية .
هذا ليس لطيفًا جدًا لسماعه ، خاصة لأولئك منا الذين يحبون تعدد الأشكال المعياري ، والذي
يعد بالعكس تمامًا .
التوافق مع الإصدارات السابقة والمستقبلية
واحدة من "الميزات القاتلة" التي يتم ذكرها في كثير من الأحيان في Protobuffers هي "قدرتها على التخلص من المتاعب لكتابة واجهات برمجة التطبيقات المتوافقة مع الإصدارات السابقة والأمامية." تم تعليق هذا البيان أمام عينيك لإخفاء الحقيقة.
أن البدائل
متساهلة . تمكنوا من التعامل مع رسائل من الماضي أو المستقبل ، لأنهم لا يقدمون وعودًا على الإطلاق حول كيفية ظهور بياناتك. كل شيء اختياري! ولكن إذا كنت في حاجة إليها ، سيسعد Protobuffers بإعدادها وإعطائك شيئًا مع فحص النوع ، بغض النظر عما إذا كان ذلك منطقيًا.
هذا يعني أن Protobuffers تنفذ "السفر عبر الزمن" الموعود بينما
تفعل الشيء الخطأ بهدوء بشكل افتراضي . بالطبع ، يمكن للمبرمج الدقيق (ويجب) كتابة رمز للتحقق من صحة المواد الأولية المستلمة. ولكن إذا كتبت فحوصات صحة وقائية في كل موقع ، فربما يعني ذلك أن خطوة إزالة الترسيم كانت متساهلة للغاية. كل ما تمكنت من القيام به كان لا مركزية منطق التحقق من حدود محددة جيدًا وطمسها في جميع أنحاء قاعدة التعليمات البرمجية.
إحدى الحجج المحتملة هي أن protobuffers ستحفظ أي معلومات لا يفهمونها في الرسالة. من حيث المبدأ ، هذا يعني الإرسال غير المدمر للرسالة من خلال وسيط لا يفهم هذا الإصدار من المخطط. هذا انتصار واضح ، أليس كذلك؟
بالطبع ، على الورق هذه ميزة رائعة. لكنني لم أر تطبيقًا حيث يتم تخزين هذه الخاصية حقًا. باستثناء برنامج التوجيه ، لا يوجد برنامج يريد التحقق من أجزاء معينة من الرسالة ثم إعادة توجيهها دون تغيير. ستقوم الغالبية العظمى من البرامج في برامج الترقيم الأولية بفك تشفير الرسالة وتحويلها إلى أخرى وإرسالها إلى مكان آخر. للأسف ، يتم إجراء هذه التحويلات للطلب وترميزها يدويًا. والتحويلات اليدوية من منتج أولي إلى آخر لا تحتفظ بحقول مجهولة ، لأنها غير مجدية بالمعنى الحرفي.
يتجلى هذا الموقف المنتشر في كل مكان تجاه المركبات الأولية باعتبارها متوافقة عالميًا أيضًا بطرق قبيحة أخرى. يعارض دليل الأنماط لـ Protobuffers بنشاط DRY ويقترح تضمين التعريفات في التعليمات البرمجية كلما أمكن ذلك. يجادلون بأن هذا سيسمح باستخدام رسائل منفصلة في المستقبل إذا اختلفت التعريفات. أؤكد أنهم يعرضون التخلي عن ممارسة البرمجة الجيدة لمدة 60 عامًا
في حالة ، فجأة ، في وقت ما في المستقبل ، ستحتاج إلى تغيير شيء ما.
جذر المشكلة هو أن Google تجمع بين معنى البيانات وتمثيلها المادي. عندما تكون على مقياس Google ، فهذا منطقي. في النهاية ، لديهم أداة داخلية تقارن أجر المبرمج لكل ساعة باستخدام الشبكة ، وتكلفة تخزين X بايت وأشياء أخرى. على عكس معظم شركات التكنولوجيا ، فإن راتب المبرمجين هو أحد أصغر عناصر حساب Google. من الناحية المالية ، من المنطقي بالنسبة لهم قضاء وقت المبرمجين لإنقاذ بايتين.
بالإضافة إلى شركات التكنولوجيا الخمس الرائدة ، لا يوجد أي شخص آخر ضمن خمسة طلبات من حجم Google.
لا يمكن لشركتك الناشئة
أن تمضي ساعات هندسية في توفير وحدات البايت. لكن توفير وحدات البايت وإضاعة وقت المبرمجين في العملية هو بالضبط ما تم تحسينه من أجل Protobuffers.
دعونا نواجه الأمر. أنت لا تتناسب مع مقياس Google ، ولن تتناسب أبدًا. توقف عن استخدام عبادة الشحنات التكنولوجية لمجرد أن "Google تستخدمها" ولأن "هذه أفضل الممارسات في المجال".
الملوثات الأولية تلوث قواعد البرمجة
إذا كان من الممكن قصر استخدام Protobuffers على الشبكة فقط ، فلن أتحدث بشدة عن هذه التكنولوجيا. لسوء الحظ ، على الرغم من وجود العديد من الحلول من حيث المبدأ ، إلا أن أيا منها لا يكفي لاستخدامه في البرامج الحقيقية.
تتطابق Protobuffers مع البيانات التي تريد إرسالها عبر قناة الاتصال. غالبًا ما تكون
متسقة ، ولكن غير
متطابقة ، مع البيانات الفعلية التي يرغب التطبيق في العمل معها. هذا يضعنا في وضع غير مريح ، يجب عليك الاختيار من بين ثلاثة خيارات سيئة:
- احتفظ بنوع منفصل يصف البيانات التي تحتاجها حقًا ، وتأكد من دعم كلا النوعين في نفس الوقت.
- قم بتعبئة البيانات الكاملة بتنسيق لإرسالها واستخدامها بواسطة التطبيق.
- استرجاع البيانات الكاملة في كل مرة تكون هناك حاجة إليها من الصيغة القصيرة للإرسال.
من الواضح أن الخيار 1 هو الحل "الصحيح" ، ولكنه غير مناسب لـ Protobuffers. اللغة ليست قوية بما يكفي لترميز الأنواع التي يمكنها القيام بعمل مزدوج بتنسيقين. هذا يعني أنه يجب عليك كتابة نوع بيانات منفصل تمامًا ، وتطويره بشكل متزامن مع Protobuffers ،
وكتابة رمز تسلسل لهم تحديدًا . ولكن نظرًا لأن معظم الأشخاص يبدو أنهم يستخدمون Protobuffers لعدم كتابة رمز التسلسل ، فمن الواضح أنه لم يتم تنفيذ هذا الخيار أبدًا.
وبدلاً من ذلك ، تسمح التعليمات البرمجية باستخدام protobuffers لتوزيعها في جميع أنحاء قاعدة التعليمات البرمجية. إنها حقيقة. كان مشروعي الرئيسي في Google هو مترجم أخذ "برنامجًا" مكتوبًا في شكل واحد من أشكال Protobuffers وأنتج "برنامجًا" مكافئًا في برنامج آخر. كانت تنسيقات الإدخال والإخراج مختلفة تمامًا بحيث لم تعمل إصداراتها المتوازية الصحيحة من C ++ مطلقًا. نتيجة لذلك ، لم يستطع الكود الخاص بي استخدام أي من تقنيات كتابة المترجم الثرية ، لأن بيانات Protobuffers (والشفرة التي تم إنشاؤها) كانت صعبة للغاية للقيام بأي شيء مثير للاهتمام بها.
ونتيجة لذلك ، بدلاً من 50 سطرًا من
مخططات العودية ، تم استخدام 10000 سطر من خلط العازلة الخاص. كان الرمز الذي أردت كتابته مستحيلًا حرفياً مع المخازن المؤقتة الأولية.
على الرغم من أن هذه حالة واحدة ، إلا أنها ليست فريدة من نوعها. نظرًا للطبيعة القاسية لتوليد الشفرات ، لن تكون مظاهر المخازن المؤقتة الأولية في اللغات أبداً ، ولا يمكن إجراؤها - ما لم تقم بإعادة كتابة مولد الشفرة.
ولكن حتى ذلك الحين ، لا تزال لديك مشكلة في تضمين نظام نوع تافه في لغتك المستهدفة. نظرًا لأن معظم وظائف Protobuffers غير مدروسة جيدًا ، فإن هذه الخصائص المريبة تتسرب إلى ملفات التعليمات البرمجية الخاصة بنا. هذا يعني أننا مضطرون ليس فقط للتنفيذ ، ولكن أيضًا استخدام هذه الأفكار السيئة في أي مشروع يأمل في التفاعل مع Protobuffers.
على أساس متين ، من السهل إدراك أشياء لا معنى لها ، ولكن إذا ذهبت في اتجاه آخر ، فستواجه في أحسن الأحوال صعوبات ، وفي أسوأ الأحوال ، مع رعب قديم حقيقي.
بشكل عام ، أعط الأمل لأي شخص يقوم بتنفيذ Protobuffers في مشاريعهم.
1. حتى يومنا هذا ، هناك نقاش ساخن على Google حول proto2 وما إذا كان يجب وضع علامة على الحقول على أنها
required
. البيانان "
optional
يعتبر ضار"
و "
required
يعتبر ضار" يتم توزيعهما في نفس الوقت. حظا سعيدا معرفة ذلك يا رفاق.
↑