تتعلم من خلال المقارنة بما تعرفه بالفعل. لقد تعرضت مؤخرًا لمشكلة بسبب افتراضي أن Rust يعمل مثل Java فيما يتعلق بحل إصدار التبعيات العابرة. في هذا المنشور، أريد مقارنة الاثنين.
قبل الخوض في تفاصيل كل مجموعة، دعنا نصف المجال والمشكلات التي تأتي معه.
\ عند تطوير أي مشروع فوق مستوى Hello World، من المحتمل أن تواجه مشكلات واجهها الآخرون من قبل. إذا كانت المشكلة منتشرة، فإن احتمالية أن يكون شخص ما لطيفًا وذو عقلية مدنية كافية لحزم الكود الذي يحلها، ليعيد الآخرون استخدامه. الآن، يمكنك استخدام الحزمة والتركيز على حل مشكلتك الأساسية. هكذا تبني الصناعة معظم المشاريع اليوم، حتى لو جلبت مشاكل أخرى: أنت تجلس على أكتاف العمالقة.
\ تأتي اللغات مع أدوات بناء يمكنها إضافة مثل هذه الحزم إلى مشروعك. معظمها يشير إلى الحزم التي تضيفها إلى مشروعك باسم التبعيات. بدورها، يمكن أن يكون لتبعيات المشاريع تبعياتها الخاصة: وتسمى الأخيرة التبعيات العابرة.

في الرسم البياني أعلاه، C و D هما تبعيات عابرة.
\ التبعيات العابرة لها مشاكلها الخاصة. أكبرها هو عندما تكون التبعية العابرة مطلوبة من مسارات مختلفة، ولكن بإصدارات مختلفة. في الرسم البياني أدناه، كل من A و B يعتمدان على C، ولكن على إصدارات مختلفة منه.

أي إصدار من C يجب أن تتضمنه أداة البناء في مشروعك؟ لدى Java و Rust إجابات مختلفة. دعنا نصفها بالتناوب.
تذكير: يتم تجميع كود Java إلى bytecode، والذي يتم تفسيره بعد ذلك في وقت التشغيل (وأحيانًا يتم تجميعه إلى كود أصلي، ولكن هذا خارج مساحة مشكلتنا الحالية). سأصف أولاً حل التبعيات في وقت التشغيل وحل التبعيات في وقت البناء.
\ في وقت التشغيل، توفر آلة Java الافتراضية مفهوم classpath. عند الحاجة إلى تحميل فئة، يبحث وقت التشغيل عبر classpath المكون بالترتيب. تخيل الفئة التالية:
public static Main { public static void main(String[] args) { Class.forName("ch.frankel.Dep"); } }
\ دعنا نقوم بتجميعها وتنفيذها:
java -cp ./foo.jar:./bar.jar Main
\ سيبحث ما سبق أولاً في foo.jar عن فئة ch.frankel.Dep. إذا تم العثور عليها، فإنه يتوقف هناك ويحمل الفئة، بغض النظر عما إذا كانت موجودة أيضًا في bar.jar؛ إذا لم يتم العثور عليها، فإنه يبحث أكثر في فئة bar.jar. إذا لم يتم العثور عليها بعد، فإنه يفشل بـ ClassNotFoundException.
\ آلية حل التبعيات في وقت التشغيل في Java مرتبة ولها حبيبية لكل فئة. ينطبق ذلك سواء كنت تقوم بتشغيل فئة Java وتحدد classpath في سطر الأوامر كما هو موضح أعلاه، أو ما إذا كنت تقوم بتشغيل JAR يحدد classpath في بيانه.
\ دعنا نغير الكود أعلاه إلى ما يلي:
public static Main { public static void main(String[] args) { var dep = new ch.frankel.Dep(); } }
\ نظرًا لأن الكود الجديد يشير إلى Dep مباشرة، يتطلب الكود الجديد حل الفئة في وقت التجميع. يعمل حل classpath بنفس الطريقة:
javac -cp ./foo.jar:./bar.jar Main
\ يبحث المترجم عن Dep في foo.jar، ثم في bar.jar إذا لم يتم العثور عليه. ما سبق هو ما تتعلمه في بداية رحلة تعلم Java الخاصة بك.
\ بعد ذلك، تكون وحدة عملك هي أرشيف Java، المعروف باسم JAR، بدلاً من الفئة. JAR هو أرشيف ZIP مجيد، مع بيان داخلي يحدد إصداره.
\ الآن، تخيل أنك مستخدم لـ foo.jar. يقوم مطورو foo.jar بتعيين classpath محدد عند التجميع، ربما بما في ذلك JARs أخرى. ستحتاج إلى هذه المعلومات لتشغيل أمرك الخاص. كيف يمرر مطور المكتبة هذه المعرفة إلى المستخدمين النهائيين؟
\ توصل المجتمع إلى بعض الأفكار للإجابة على هذا السؤال: كانت الاستجابة الأولى التي استمرت هي Maven. لدى Maven مفهوم نموذج كائن المشروع، حيث تقوم بتعيين البيانات الوصفية لمشروعك، بالإضافة إلى التبعيات. يمكن لـ Maven حل التبعيات العابرة بسهولة لأنها تنشر أيضًا POM الخاص بها، مع تبعياتها الخاصة. وبالتالي، يمكن لـ Maven تتبع تبعيات كل تبعية وصولاً إلى التبعيات الورقية.
\ الآن، للعودة إلى بيان المشكلة: كيف يحل Maven تعارضات الإصدار؟ أي إصدار تبعية سيحل Maven لـ C، 1.0 أو 2.0؟
\ الوثائق واضحة: الأقرب.

في الرسم البياني أعلاه، المسار إلى v1 له مسافة اثنين، واحدة إلى B، ثم واحدة إلى C؛ في الوقت نفسه، المسار إلى v2 له مسافة ثلاثة، واحدة إلى A، ثم واحدة إلى D، ثم أخيرًا واحدة إلى C. وبالتالي، يشير أقصر مسار إلى v1.
\ ومع ذلك، في الرسم البياني الأولي، كلا إصداري C على نفس المسافة من الأداة الجذرية. لا تقدم الوثائق إجابة. إذا كنت مهتمًا بذلك، فإنه يعتمد على ترتيب إعلان A و B في POM! باختصار، يعيد Maven إصدارًا واحدًا من تبعية مكررة لتضمينه في classpath التجميع.
\ إذا كان A يمكن أن يعمل مع C v2.0 أو B مع C 1.0، رائع! إذا لم يكن كذلك، فمن المحتمل أن تحتاج إلى ترقية إصدار A أو خفض إصدار B، بحيث يعمل إصدار C المحلول مع كليهما. إنها عملية يدوية مؤلمة - اسألني كيف أعرف. الأسوأ من ذلك، قد تكتشف أنه لا يوجد إصدار C يعمل مع كل من A و B. حان الوقت لاستبدال A أو B.
يختلف Rust عن Java في عدة جوانب، ولكن أعتقد أن ما يلي هو الأكثر صلة لغرض مناقشتنا:
\ دعنا نفحصها واحدة تلو الأخرى.
\ يتم تجميع Java إلى bytecode، ثم تقوم بتشغيل الأخير. تحتاج إلى تعيين classpath في وقت التجميع ووقت التشغيل. يمكن أن يؤدي التجميع باستخدام classpath محدد والتشغيل بآخر مختلف إلى أخطاء. على سبيل المثال، تخيل أنك تقوم بالتجميع مع فئة تعتمد عليها، ولكن الفئة غائبة في وقت التشغيل. أو بدلاً من ذلك، موجودة، ولكن في إصدار غير متوافق.
\ على عكس هذا النهج المعياري، يقوم Rust بتجميع حزمة أصلية فريدة من نوعها لكود الصندوق وكل تبعية. علاوة على ذلك، يوفر Rust أداة البناء الخاصة به أيضًا، وبالتالي تجنب الاضطرار إلى تذكر غرائب الأدوات المختلفة. ذكرت Maven، ولكن من المحتمل أن يكون لأدوات البناء الأخرى قواعد مختلفة لحل الإصدار في حالة الاستخدام أعلاه.
\ أخيرًا، يحل Java التبعيات من الثنائيات: JARs. على العكس من ذلك، يحل Rust التبعيات من المصادر. في وقت البناء، يحل Cargo شجرة التبعيات بأكملها، ويقوم بتنزيل جميع المصادر المطلوبة، ويقوم بتجميعها بالترتيب الصحيح.
\ مع وضع هذا في الاعتبار، كيف يحل Rust إصدار تبعية C في المشكل


