كيفية استخدام Control Inversion في JavaScript و Reactjs لتبسيط معالجة التعليمات البرمجية

كيفية استخدام Control Inversion في JavaScript و Reactjs لتبسيط معالجة التعليمات البرمجية


يعد "انعكاس التحكم" مبدأًا سهل الفهم إلى حد ما في البرمجة ، والذي في الوقت نفسه ، يمكنه تحسين التعليمات البرمجية الخاصة بك بشكل كبير. توضح هذه المقالة كيفية تطبيق Control Inversion في JavaScript وفي Reactjs.


إذا كنت قد كتبت بالفعل رمزًا يُستخدم في أكثر من مكان ، فأنت على دراية بهذا الموقف:


  1. يمكنك إنشاء جزء من التعليمات البرمجية القابلة لإعادة الاستخدام (يمكن أن تكون وظيفة أو مكون React أو React hook أو ما إلى ذلك) ومشاركتها (للتعاون أو النشر في مصدر مفتوح).
  2. شخص ما يطلب منك إضافة وظائف جديدة. لا يدعم الكود الخاص بك الوظيفة المقترحة ، لكن قد يحدث ذلك إذا قمت بإجراء تغيير بسيط.
  3. يمكنك إضافة وسيطة / دعم / خيار جديد إلى الكود والمنطق المقترن به للحفاظ على عمل هذه الميزة الجديدة.
  4. كرر الخطوتين 2 و 3 عدة مرات (أو عدة مرات).
  5. من الصعب الآن استخدام التعليمات البرمجية القابلة لإعادة الاستخدام وصيانتها.

ما الذي يجعل الكود كابوسًا للاستخدام والمحافظة عليه بالضبط؟ هناك العديد من الجوانب التي يمكن أن تجعل التعليمات البرمجية الخاصة بك إشكالية:


  1. حجم الحزمة و / أو الأداء: يمكن أن يؤدي المزيد من التعليمات البرمجية لتشغيلها على الأجهزة إلى ضعف الأداء. في بعض الأحيان ، قد يؤدي ذلك إلى رفض الأشخاص ببساطة استخدام التعليمات البرمجية الخاصة بك.
  2. من الصعب الحفاظ عليها: في السابق ، كان أمامك الكود القابل لإعادة الاستخدام بضعة خيارات فقط ، وكان يركز على القيام بشيء واحد جيدًا ، لكن الآن يمكنه أن يفعل مجموعة من الأشياء المختلفة ، وتحتاج إلى توثيقها كلها. بالإضافة إلى ذلك ، سيبدأ الأشخاص في طرح الأسئلة عليك حول كيفية استخدام التعليمات البرمجية الخاصة بك لحالات استخدام معينة قد تكون أو لا تكون قابلة للمقارنة مع حالات الاستخدام التي أضفت دعمًا لها بالفعل. قد يكون لديك حتى حالتان متطابقتان تقريبًا لاستخدامات مختلفة بعض الشيء ، لذلك سيكون عليك الإجابة على أسئلة حول ما هو الأفضل للاستخدام في موقف معين.
  3. تعقيد التنفيذ : في كل مرة لا يكون ذلك مجرد if ، يتعايش كل فرع من منطق الشفرة مع فروع المنطق الموجودة. في الواقع ، تكون المواقف ممكنة عندما تحاول الاحتفاظ بمجموعة من الحجج / الخيارات / الدعائم التي لا يستخدمها أي شخص حتى الآن ، لكنك لا تزال بحاجة إلى التفكير في أي خيارات ممكنة ، لأنك لا تعرف بالضبط ما إذا كان أي شخص سيستخدم هذه المجموعات أم لا.
  4. واجهة برمجة تطبيقات متطورة : كل ​​وسيطة / خيار / دعامة جديدة تضيفها إلى التعليمات البرمجية القابلة لإعادة الاستخدام تجعل من الصعب استخدامها ، لأن لديك الآن README ضخم أو موقع حيث تم توثيق جميع الوظائف المتاحة ، ويتعين على الأشخاص تعلم كل هذا للاستخدام الفعال كودك استخدامه غير مناسب ، لأن تعقيد واجهة برمجة التطبيقات الخاصة بك يخترق رمز المطور الذي يستخدمه ، مما يعقد التعليمات البرمجية الخاصة به.

نتيجة لذلك ، الجميع يعاني. تجدر الإشارة إلى أن تنفيذ البرنامج النهائي هو جزء أساسي من التنمية. ولكن سيكون أمراً رائعاً إذا فكرنا أكثر في تنفيذ أعمالنا التجريبية (اقرأ عن "برمجة AHA" ). هل هناك طريقة يمكننا من خلالها تقليل المشاكل المتعلقة بالكود القابل لإعادة الاستخدام وما زلت نجني فوائد استخدام التجريد؟


التحكم في الانقلاب


انقلاب التحكم هو مبدأ يبسط حقًا إنشاء التجريد واستخدامه. إليك ما تقول ويكيبيديا عنه:


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

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


ما هو انعكاس الإدارة في الكود؟


للبدء ، إليك مثال مفتعل للغاية:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

الآن ، دعونا نلعب "دورة حياة تجريدية" نموذجية عن طريق إضافة حالات استخدام جديدة لهذا التجريد و "تحسينها بلا مبالاة" لدعم حالات الاستخدام الجديدة هذه:


 //   Array.prototype.filter   function filter( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if ( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ) { continue } newArray[newArray.length] = element } return newArray } filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterNull: false}) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterUndefined: false}) // [0, 1, 2, undefined, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterZero: true}) // [1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], {filterEmptyString: true}) // [0, 1, 2, 3, 'four'] 

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


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


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


حسنًا ، دعنا الآن نكتب تجريدًا أكثر تفصيلًا لهذه الوظيفة ونطبق طريقة انعكاس التحكم لدعم جميع حالات الاستخدام التي نحتاجها:


 //   Array.prototype.filter   function filter(array, filterFn) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (filterFn(element)) { newArray[newArray.length] = element } } return newArray } filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) // [0, 1, 2, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined) // [0, 1, 2, null, 3, 'four', ''] filter([0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null) // [0, 1, 2, undefined, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== 0, ) // [1, 2, 3, 'four', ''] filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== undefined && el !== null && el !== '', ) // [0, 1, 2, 3, 'four'] 

! ممتاز اتضح أسهل بكثير. لقد أدارنا التحكم في الوظيفة ، وننقل المسؤولية عن تحديد أي عنصر يقع في الصفيف الجديد ، من وظيفة filter إلى الوظيفة التي تستدعي وظيفة المرشح. لاحظ أن وظيفة filter لا تزال مجردة مفيدة في حد ذاتها ، لكنها الآن أكثر مرونة.


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


 filter( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], animal => animal.legs === 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

فقط تخيل ما إذا كنت بحاجة إلى إضافة دعم لحالة الاستخدام هذه دون تطبيق عكس التحكم؟ نعم ، سيكون ذلك مجرد سخيف.


واجهة برمجة تطبيقات سيئة؟


واحدة من أكثر الشكاوى شيوعًا التي أسمعها من الناس بشأن واجهات برمجة التطبيقات التي تستخدم انقلاب التحكم هي: "نعم ، ولكن الآن أصبح من الصعب استخدامها أكثر من ذي قبل." خذ هذا المثال:


 //  filter([0, 1, undefined, 2, null, 3, 'four', '']) //  filter( [0, 1, undefined, 2, null, 3, 'four', ''], el => el !== null && el !== undefined, ) 

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


 function filterWithOptions( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { return filter( array, element => !( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ), ) } 

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


و فقط للمروحة:


 function filterByLegCount(array, legCount) { return filter(array, animal => animal.legs === legCount) } filterByLegCount( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], 0, ) // [ // {name: 'dolphin', legs: 0, mammal: true}, // {name: 'salmon', legs: 0, mammal: false}, // ] 

يمكنك إنشاء وظائف خاصة لأي موقف يحدث لك غالبًا.


أمثلة الحياة الحقيقية


لذلك ، هذا يعمل في حالات بسيطة ، ولكن هل هذا المفهوم مناسب للحياة الحقيقية؟ حسنا ، على الأرجح كنت تستخدم باستمرار انقلاب السيطرة. على سبيل المثال ، تقوم الدالة Array.prototype.filter بتطبيق التحكم العكسي. مثل وظيفة Array.prototype.map .


هناك العديد من الأنماط التي قد تكون على دراية بها ، والتي هي مجرد شكل واحد من أشكال انقلاب التحكم.


فيما يلي نوعان من الأنماط المفضلة لدي والتي تعرض هذه "المكونات المركبة" و "مخفضات الحالة" . فيما يلي أمثلة مختصرة عن كيفية تطبيق هذه الأنماط.


مكونات مركبة


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


 function App() { return ( <Menu buttonContents={ <> Actions <span aria-hidden></span> </> } items={[ {contents: 'Download', onSelect: () => alert('Download')}, {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')}, {contents: 'Delete', onSelect: () => alert('Delete')}, ]} /> ) } 

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


ربما قم بإنشاء نوع خاص من عنصر القائمة ، على سبيل المثال {contents: <hr />} . أعتقد أنه onSelect ، ولكن بعد ذلك سيتعين علينا التعامل مع الحالات التي لا يوجد فيها onSelect . ولكي نكون صادقين ، هذا هو API محرج للغاية.


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


 function App() { return ( <Menu> <MenuButton> Actions <span aria-hidden></span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert('Download')}>Download</MenuItem> <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem> <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem> </MenuList> </Menu> ) } 

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


يمكنك قراءة المزيد عن هذا النمط هنا . بفضل ريان فلورنس ، الذي علمني هذا.


الدولة المخفض


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


اقترح المنطق وراء Downshift أنه بعد اتخاذ الاختيار ، يجب إغلاق القائمة. اقترح مستخدم المكتبة الذي يحتاج إلى تغيير وظائفه إضافة prop closeOnSelection . لقد رفضت هذا العرض ، لأنني كنت قد قطعت المسار المؤدي إلى apropalapse ، وأردت تجنبه.


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


مثال على استخدام مكتبة Downshift بحيث لا تغلق القائمة بعد أن ينقر المستخدم على العنصر المحدد:


 function stateReducer(state, changes) { switch (changes.type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes, //     Downshift   //       isOpen  highlightedIndex //      isOpen: state.isOpen, highlightedIndex: state.highlightedIndex, } default: return changes } } // ,   // <Downshift stateReducer={stateReducer} {...restOfTheProps} /> 

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


تقديم الدعائم


الجدير بالذكر هو نمط "تقديم الدعائم" . هذا النموذج هو مثال مثالي لاستخدام انقلاب التحكم ، لكننا لسنا بحاجة إليه بشكل خاص. اقرأ المزيد عنها هنا: لماذا لا نحتاج إلى تقديم Render Props أكثر من ذلك .


تحذير


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


دعنا نعود إلى مثالنا بعيد المنال:


 //   Array.prototype.filter   function filter(array) { let newArray = [] for (let index = 0; index < array.length; index++) { const element = array[index] if (element !== null && element !== undefined) { newArray[newArray.length] = element } } return newArray } // : filter([0, 1, undefined, 2, null, 3, 'four', '']) // [0, 1, 2, 3, 'four', ''] 

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


كما هو الحال مع أي تجريد ، كن حذرًا في تطبيق مبدأ AHA Programming وتجنب التجريد المتعجل!


النتائج


آمل أن تكون المقالة مفيدة لك. لقد أوضحت كيف يمكنك تطبيق مفهوم Control Inversion في رد فعل. هذا المفهوم ، بالطبع ، لا ينطبق فقط على React (كما رأينا مع وظيفة filter ). في المرة التالية التي تلاحظ فيها أنك تضيف if آخر لوظيفة coreBusinessLogic لوظيفة تطبيقك ، فكر في الطريقة التي يمكنك بها عكس عنصر التحكم ونقل المنطق إلى حيث يتم استخدامه (أو ، إذا تم استخدامه في عدة أماكن ، فيمكنك إنشاء تجريد أكثر تخصصًا لهذا حالة محددة).


إذا كنت تريد ، يمكنك اللعب بمثال من مقال في CodeSandbox .


حظا سعيدا وشكرا لاهتمامكم!


PS. إذا كنت قد استمتعت بهذا المقال ، فقد تستمتع بهذا الكلام: youtube Kent C Dodds - Simply React

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


All Articles