سعر التركيب في العالم جافا سكريبت

فكرة أنه عند تطوير أي منطق أعمال أكثر أو أقل تعقيدًا ، يجب إعطاء الأولوية لتكوين الكائنات ، بدلاً من الميراث شائع بين مطوري البرامج من أنواع مختلفة. في الموجة التالية من شعبية نموذج البرمجة الوظيفية التي أطلقها نجاح ReactJS ، وصل الحديث عن فوائد الحلول التركيبية إلى الواجهة الأمامية. يوجد في هذا المنشور تخطيط بسيط على أرفف نظرية تكوين الأجسام في Javascript ، وهو مثال محدد وتحليله والإجابة على سؤال عن مقدار تكلفة الأناقة الدلالية للمستخدم (المفسد: الكثير).

خامسا Kandinsky - التكوين X
فاسيلي كاندينسكي - "التكوين X"

سنوات من التطوير الناجح لمقاربة موجهة نحو التنمية ، معظمها في المجال الأكاديمي ، أدت إلى اختلال ملحوظ في ذهن المطور العادي. لذلك ، في معظم الحالات ، فإن الفكرة الأولى ، إذا لزم الأمر ، لتعميم سلوك لعدد من الكيانات المتنوعة هي إنشاء فئة رئيسية ورث هذا السلوك. يؤدي نهج إساءة الاستخدام هذا إلى العديد من المشكلات التي تعقد التصميم وتثبط عملية التطوير.

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

الصورة
mail.mozilla.org/pipermail/es-discuss/2013- June/031614.html

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

أعتقد أن عدم قابلية إعادة الاستخدام يأتي بلغات موجهة للكائنات ، وليس لغات وظيفية. نظرًا لأن مشكلة اللغات الموجهة للكائنات هي أنها تمتلك كل هذه البيئة الضمنية التي يحملونها معهم. أردت موزة ولكن ما حصلت عليه كان غوريلا تحمل الموز والغابة بأكملها.

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

بنقل التعريفات من "أنماط التصميم" إلى عالم Javascript الحيوي ، يمكننا تلخيص ثلاثة أنواع من تكوين الكائن: التجميع ، التسلسل ، والتفويض . تجدر الإشارة إلى أن هذا الفصل ومفهوم تكوين الكائن بشكل عام له طابع تقني بحت ، في حين أن معنى هذه المصطلحات له تقاطعات ، مما يؤدي إلى حدوث تشويش. لذلك ، على سبيل المثال ، يتم تطبيق الميراث الطبقي في Javascript على أساس التفويض (الميراث النموذج الأولي). لذلك ، من الأفضل عمل نسخة احتياطية من أمثلة التعليمات البرمجية المباشرة في كل حالة.

التجميع هو اتحاد لا حصر له من الكائنات ، يمكن الحصول على كل منها باستخدام معرف وصول فريد. وتشمل الأمثلة المصفوفات والأشجار والرسوم البيانية. مثال جيد من عالم تطوير الويب هو شجرة DOM. الجودة الأساسية لهذا النوع من التكوين والسبب في إنشائه هي القدرة على تطبيق بعض المعالج بشكل مريح على كل طفل من التكوين.

مثال اصطناعي هو مجموعة من الكائنات التي تتناوب تعيين نمط لعنصر مرئي تعسفي.

const styles = [  { fontSize: '12px', fontFamily: 'Arial' },  { fontFamily: 'Verdana', fontStyle: 'italic', fontWeight: 'bold' },  { fontFamily: 'Tahoma', fontStyle: 'normal'} ]; 

يمكن استخراج كل كائن من الكائنات من خلال فهرسه دون فقد المعلومات. بالإضافة إلى ذلك ، باستخدام Array.prototype.map () ، يمكنك معالجة جميع القيم المخزنة بطريقة معينة.

 const getFontFamily = s => s.fontFamily; styles.map(getFontFamily) //["Arial","Verdana","Tahoma"] 

يتضمن Concatenation توسيع وظيفة كائن موجود عن طريق إضافة خصائص جديدة إليه. وهكذا ، على سبيل المثال ، مخفضات الحالة في عمل Redux. تتم كتابة البيانات الواردة للتحديث إلى كائن الحالة ، وتوسيعه. البيانات الموجودة على الحالة الحالية للكائن ، على عكس التجميع ، يتم فقدها إذا لم يتم حفظها.

بالرجوع إلى المثال ، وتطبيق الإعدادات أعلاه بالتناوب على العنصر المرئي ، يمكنك إنشاء النتيجة النهائية عن طريق وصل معلمات الكائنات.

 const concatenate = (a, s) => ({…a, …s}); styles.reduce(concatenate, {}) //{fontSize:"12px",fontFamily:"Tahoma",fontStyle:"normal",fontWeight:"bold"} 

قيم النمط الأكثر تحديدًا ستحل في النهاية محل الحالات السابقة.

عند التفويض ، كما تعتقد ، يتم تفويض كائن إلى آخر. المندوبين ، على سبيل المثال ، نماذج أولية في Javascript. مثيلات الكائنات المشتقة إعادة توجيه المكالمات إلى الأساليب الأصل. إذا لم تكن هناك خاصية أو طريقة مطلوبة في مثيل الصفيف ، فستعيد توجيه هذه المكالمة إلى Array.prototype ، وإذا لزم الأمر - إلى Object.prototype. وبالتالي ، تستند آلية الميراث في Javascript إلى سلسلة تفويض النموذج الأولي ، والتي تعد من الناحية الفنية إصدارًا (مفاجئًا) للتكوين.

يمكن دمج مجموعة من كائنات النمط حسب التفويض على النحو التالي.

 const delegate = (a, b) => Object.assign(Object.create(a), b); styles.reduceRight(delegate, {}) //{"fontSize":"12px","fontFamily":"Arial"} styles.reduceRight(delegate, {}).fontWeight //bold 

كما ترى ، لا يمكن الوصول إلى خصائص المفوض عن طريق التعداد (على سبيل المثال ، باستخدام Object.keys ()) ، ولكن يمكن الوصول إليها فقط عن طريق الوصول الصريح. حقيقة أن هذا يعطينا هو في نهاية هذا المنصب.

الآن إلى التفاصيل. من الأمثلة الجيدة على الحالة التي تشجع المطور على استخدام التكوين بدلاً من الميراث في " تكوين كائن Michael Rise" في Javascript . هنا ، ينظر المؤلف في عملية إنشاء تسلسل هرمي لشخصيات لعب الأدوار. في البداية ، هناك حاجة إلى نوعين من الشخصيات - محارب وساحر ، لكل منهما احتياطي صحي معين وله اسم. هذه الخصائص شائعة ويمكن نقلها إلى حرف الفئة الأصل.

 class Character { constructor(name) { this.name = name; this.health = 100; } } 

يتميز المحارب بحقيقة أنه يعرف كيف يضرب ، بينما يقضي قدرته على التحمل ، والساحر - القدرة على إلقاء تعويذات ، وتقليل كمية المانا.

 class Fighter extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina -  ; } } class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana -  ; } } 

بعد إنشاء فئتي Fighter و Mage ، أحفاد الحرف ، يواجه المطور مشكلة غير متوقعة عندما تكون هناك حاجة لإنشاء فئة بالادين. تتميز الشخصية الجديدة بالقدرة المحسوسة على القتال والاستحضار. مرتجلاً يمكنك رؤية حلين مختلفين في نفس النعمة.

  1. يمكنك جعل بالادين سليلًا للشخصية وتنفيذ كل من القتال () و cast () فيه من البداية. في هذه الحالة ، يتم انتهاك مبدأ DRY بشكل صارخ ، لأنه سيتم تكرار كل طريقة من الطرق أثناء الإنشاء وبالتالي ستحتاج إلى تزامن مستمر مع أساليب فئتي Mage و Fighter لتتبع التغييرات.
  2. يمكن تطبيق أساليب القتال () و cast () على مستوى فئة Charater بحيث تمتلك جميع الأنواع الثلاثة من الشخصيات. هذا حل أكثر متعة ، ومع ذلك ، في هذه الحالة ، يجب على المطور إعادة تعريف طريقة fight () للساحر وطريقة cast () الخاصة بالمحارب ، واستبدالها بكعوب فارغة.

في أي من الخيارات ، يجب أن يواجه المرء عاجلاً أم آجلاً مشاكل الميراث التي يتم التعبير عنها في بداية المنشور. يمكن حلها من خلال نهج وظيفي لتنفيذ الشخصيات. يكفي الابتعاد عن أنواعهم ، ولكن عن وظائفهم. في الخلاصة ، لدينا اثنين من الميزات الرئيسية التي تحدد قدرة الشخصيات - القدرة على القتال والقدرة على استحضار. يمكن تعيين هذه الميزات باستخدام وظائف المصنع التي تعمل على توسيع الحالة التي تحدد الشخصية (مثال على التكوين هو تسلسل).

 const canCast = (state) => ({ cast: (spell) => { console.log(`${state.name} casts ${spell}!`); state.mana -  ; } }) const canFight = (state) => ({ fight: () => { console.log(`${state.name} slashes at the foe!`); state.stamina -  ; } }) 

وبالتالي ، يتم تحديد الشخصية من خلال مجموعة من هذه الميزات ، والخصائص الأولية ، العامة (الاسم والصحة) ، والخاصة (القدرة على التحمل ومانا).

 const fighter = (name) => { let state = { name, health: 100, stamina: 100 } return Object.assign(state, canFight(state)); } const mage = (name) => { let state = { name, health: 100, mana: 100 } return Object.assign(state, canCast(state)); } const paladin = (name) => { let state = { name, health: 100, mana: 100, stamina: 100 } return Object.assign(state, canCast(state), canFight(state)); } 

كل شيء جميل - إعادة استخدام رمز الإجراء ، يمكنك إضافة أي شخصية جديدة بسهولة ، دون لمس الشخصيات السابقة ودون تضخيم وظيفة أي كائن واحد. للعثور على ذبابة في المرهم في الحل المقترح ، يكفي مقارنة أداء الحل على أساس الميراث (قراءة التفويض) والحل استنادًا إلى تسلسل. إنشاء جيش المليون من الحالات من الشخصيات التي تم إنشاؤها.

 var inheritanceArmy = []; for (var i = 0; i < 1000000; i++) { inheritanceArmy.push(new Fighter('Fighter' + i)); inheritanceArmy.push(new Mage('Mage' + i)); } var compositionArmy = []; for (var i = 0; i < 1000000; i++) { compositionArmy.push(fighter('Fighter' + i)); compositionArmy.push(mage('Mage' + i)); } 

وقارن بين الذاكرة والتكاليف الحسابية بين الميراث والتكوين اللازم لإنشاء الكائنات.

الصورة

في المتوسط ​​، يتطلب الحل الذي يستخدم التركيبة بالتسلسل موارد أكثر بنسبة 100-150٪. تم الحصول على النتائج المقدمة في بيئة NodeJS ، ويمكنك رؤية النتائج لمحرك المتصفح من خلال تشغيل هذا الاختبار .

يمكن شرح ميزة الحل القائم على تفويض الميراث عن طريق حفظ الذاكرة بسبب نقص الوصول الضمني إلى خصائص المفوض ، وكذلك تعطيل بعض تحسينات المحرك للمندوبين الديناميين. بدوره ، يستخدم الحل القائم على تسلسل الأسلوب Object.assign () باهظ الثمن ، والذي يؤثر بشكل كبير على أدائه. ومن المثير للاهتمام ، أن Firefox Quantum يعرض نتائج متناقضة تمامًا مع Chromium - يعمل الحل الثاني بشكل أسرع في Gecko.

بالطبع ، من المفيد الاعتماد على نتائج اختبارات الأداء فقط عند حل المهام الشاقة المرتبطة بإنشاء عدد كبير من الكائنات ذات البنية التحتية المعقدة - على سبيل المثال ، عند العمل مع شجرة افتراضية للعناصر أو تطوير مكتبة رسومية. في معظم الحالات ، يصبح الجمال الهيكلي للحل وموثوقيته وبساطته أكثر أهمية ، ولا يلعب اختلاف بسيط في الأداء دورًا كبيرًا (ستستهلك العمليات مع عناصر DOM موارد أكثر بكثير).

في الختام ، تجدر الإشارة إلى أن أنواع التكوين المعتبرة ليست فريدة ومتميزة. يمكن تنفيذ التفويض باستخدام التجميع ، ووراثة الفصل باستخدام التفويض (كما هو الحال في JavaScript). في جوهرها ، سيكون أي مجموعة من الكائنات شكلاً أو بآخر من التكوين ، وفي نهاية المطاف فقط أهمية البساطة والمرونة في الحل الناتج.

Source: https://habr.com/ru/post/ar438404/


All Articles