نحن نستخدم محددات إعادة التكرار أكثر من اللازم

عندما أنظر إلى ملف {domain} /selectors.js في مشاريع React / Redux الكبيرة التي أعمل معها ، أرى غالبًا قائمة ضخمة من محددات الاسترجاع من هذا النوع:


getUsers(state) getUser(id)(state) getUserId(id)(state) getUserFirstName(id)(state) getUserLastName(id)(state) getUserEmailSelector(id)(state) getUserFullName(id)(state) … 

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


صورة

استرجاع والمحددات


دعنا ننظر في الإعادة. ما هو ، لماذا؟ بعد قراءة redux.js.org ، نفهم أن Redux عبارة عن "حاوية يمكن التنبؤ بها لتخزين حالة تطبيق JavaScript"


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


منغمسين بعمق في محددات


قائمة المحددات التي أشرت إليها في مقدمة هذه المقالة هي سمة من الرموز التي تم إنشاؤها على عجل.


تخيل أن لدينا نموذج مستخدم ونريد إضافة حقل بريد إلكتروني جديد إليه. لدينا مكون كان يتوقع firstName واسم lastName ، والآن سينتظر email آخر. باتباع المنطق في الكود مع المحددات ، وتقديم حقل بريد إلكتروني جديد ، يجب على المؤلف إضافة محدد getUserEmailSelector واستخدامه لتمرير هذا الحقل إلى المكون. البنغو!


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


 const getUsers = (state) => state.users; const getUser = (id) => (state) => getUsers(state)[id]; const getUserEmailSelector = (id) => (state) => getUser(id)(state).email; 

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


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


لنفترض أننا كتبنا getUserEmailSelector محدد getUserEmailSelector كما هو موضح أعلاه. نستخدمها ونربط المكون بالحالة:


 const mapStateToProps = (state, ownProps) => ({ firstName: getUserFirstName(ownProps.userId)(state), lastName: getUserLastName(ownProps.userId)(state), //   email: getUserEmailName(ownProps.userId)(state), }) 

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


أو ربما يجب عليك الوصول إلى حقول الكيان مباشرة؟


إذا كانت المشكلة هي أن لدينا العديد من المحددات - ربما نستخدم فقط getUser ونصل إلى خصائص الكيان التي نحتاجها مباشرة؟


 const user = getUser(id)(state); const email = user.email; 

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


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


صورة

يرأس نموذج المجال. الإعادة - الثانوية


يمكنك الوصول إلى هذه الصورة عن طريق الإجابة على سؤالين:


  • كيف نحدد نموذج مجالنا؟
  • كيف سنخزن البيانات؟ (إدارة الحالة ، لذلك نستخدم redux * translator's note * التي تسمى طبقة الثبات في DDD)

الإجابة على السؤال "كيف يمكننا تعريف نموذج المجال" (في حالتنا ، المستخدم) ، دعنا نستخلص من الإعادة ونقرر ما هو "المستخدم" وما هي واجهة برمجة التطبيقات اللازمة للتفاعل معها؟


 // api.ts type User = { id: string, firstName: string, lastName: string, email: string, ... } const getFirstName = (user: User) => user.firstName; const getLastName = (user: User) => user.lastName; const getFullName = (user: User) => `${user.firstName} ${user.lastName}`; const getEmail = (user: User) => user.email; ... const createUser = (id: string, firstName: string, ...) => User; 

سيكون من الجيد إذا استخدمنا دائمًا واجهة برمجة التطبيقات هذه ونظرنا في أن نموذج المستخدم لا يمكن الوصول إليه خارج ملف api.ts. هذا يعني أننا لن ننتقل مباشرة إلى حقول الكيان منذ ذلك الحين التعليمة البرمجية التي تستخدم API لا تعرف حتى الكيان الذي يحتوي على حقول.


الآن يمكننا العودة إلى Redux وحل المشكلات المتعلقة بالحالة فقط:


  • ما المكان الذي يشغله المستخدمون في مقالتنا؟
  • كيف ينبغي لنا تخزين المستخدمين؟ قائمة؟ القاموس (مفتاح القيمة)؟ كيف؟
  • كيف سنحصل على مثيل المستخدم من الدولة؟ هل يجب استخدام الحفظ؟ (في سياق محدد getUser)

API الصغيرة مع فوائد كبيرة


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


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


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


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


أصبحت الدعائم عنصر أنظف. بدلاً من انتظار إدخال الاسم الأول واسم العائلة وخصائص البريد الإلكتروني ، يتلقى المكون مثيل المستخدم ويستخدم داخليًا API للوصول إلى البيانات الضرورية. اتضح أننا بحاجة إلى محدد واحد فقط - getUser.


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


استنتاج


مع النوايا الحسنة (اقرأ هنا - محددات) الطريق إلى الجحيم معبّد: لم نرغب في الوصول إلى حقول الكيان مباشرةً وجعلنا محددات منفصلة لذلك.


على الرغم من أن فكرة المحددات نفسها جيدة ، إلا أن الإفراط في استخدامها يجعل من الصعب الحفاظ على الكود.


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


ملاحظات المترجم


  1. لقد استخدمت كلمة الحالة ، حيث يبدو أنها دخلت بقوة في مفردات المطورين الناطقين بالروسية.
  2. يستخدم المؤلف الكلمات upstream / downstream ليعني "الكود العالي المستوى / المنخفض المستوى" (إذا كان وفقًا لمارتن) أو "الكود المستخدم أدناه / الكود أدناه يستخدم ما هو مكتوب أعلاه" ، لكن ليس من الصحيح معرفة كيفية استخدام هذا في الترجمة لذلك ، يمكنني أن أتعاطف مع نفسي عن طريق محاولة عدم إزعاج الشعور العام.

سأقبل بكل سرور التعليقات والاقتراحات الخاصة بالتصحيحات في PM وتصحيحها.

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


All Articles