قبل التاريخ
على مدى العامين الماضيين ، شاركت في عدد كبير من المقابلات. في كل واحد منهم ، سألت المتقدمين عن مبدأ المسؤولية الفردية (المشار إليها فيما يلي بـ SRP). ومعظم الناس لا يعرفون أي شيء عن المبدأ. وحتى بالنسبة لأولئك الذين يستطيعون قراءة التعريف ، لا يمكن لأحد أن يقول كيف يستخدمون هذا المبدأ في عملهم. لم يتمكنوا من تحديد كيف يؤثر SRP على التعليمات البرمجية التي يكتبونها أو مراجعة التعليمات البرمجية للزملاء. كان لدى البعض منهم أيضًا اعتقاد خاطئ بأن SRP ، مثل SOLID بأكمله ، له صلة فقط بالبرمجة الموجهة للكائنات. وأيضًا ، لم يتمكن الأشخاص في كثير من الأحيان من التعرف على حالات انتهاك واضحة لهذا المبدأ ، وذلك ببساطة لأن الشفرة كانت مكتوبة بالأسلوب الموصى به في إطار العمل المعروف.
تعتبر Redux مثالًا رئيسيًا على إطار عمل ينتهك دليله التوجيهي SRP.
المسائل SRP
أريد أن أبدأ بقيمة هذا المبدأ ، مع الفوائد التي يحققها. وأريد أيضًا أن أشير إلى أن المبدأ لا ينطبق فقط على OOP ، ولكن أيضًا على البرمجة الإجرائية والوظيفية والإعلانية. HTML ، كممثل للأخيرة ، يمكن ويجب أيضًا أن تتحلل ، خاصةً الآن عندما يتم التحكم فيه بواسطة أطر عمل واجهة المستخدم مثل React أو Angular. بالإضافة إلى ذلك ، ينطبق المبدأ على المجالات الهندسية الأخرى. وليس الهندسة فقط ، كان هناك مثل هذا التعبير في الموضوعات العسكرية: "فرق تسد" ، والتي هي إلى حد كبير تجسيد لنفس المبدأ. يقتل التعقيد ويقسمه إلى أجزاء وستفوز.
فيما يتعلق بالمجالات الهندسية الأخرى ، هنا ، على المحور ، كان هناك مقال مثير للاهتمام حول كيفية فشل محركات الطائرات المتقدمة ، ولم يتحول إلى عكس اتجاه قائد الطيار. كانت المشكلة أنهم أساءوا تفسير حالة الهيكل المعدني. بدلاً من الاعتماد على الأنظمة التي تتحكم في الهيكل المعدني ، تقوم وحدة التحكم في المحرك بقراءة المستشعرات مباشرة ، ومفاتيح الحد ، وما إلى ذلك الموجودة في الهيكل المعدني. وقد ذكر أيضًا في المقال أن المحرك يجب أن يخضع لشهادة مطولة قبل وضعه حتى على طائرة النموذج الأولي. وانتهاك SRP في هذه الحالة أدى بوضوح إلى حقيقة أنه عند تغيير تصميم الهيكل ، فإن الشفرة في وحدة التحكم في المحرك تحتاج إلى تعديل وإعادة التصديق. والأسوأ من ذلك ، كان انتهاك هذا المبدأ يكاد يستحق الطائرة وحياة الطيار. لحسن الحظ ، لا تهدد برامجنا اليومية مثل هذه العواقب ، ومع ذلك ، لا يزال يتعين عليك عدم إهمال مبادئ كتابة التعليمات البرمجية الجيدة. وهنا السبب:
- تحلل الكود يقلل من تعقيده. على سبيل المثال ، إذا كان حل مشكلة يتطلب منك كتابة التعليمات البرمجية مع تعقيد cyclomatic من أربعة ، فإن الطريقة المسؤولة عن حل مشكلتين في نفس الوقت سوف تتطلب رمز مع تعقيد 16. إذا كانت مقسمة إلى طريقتين ، فإن التعقيد الكلي سيكون 8. بالطبع ، هذا ليس دائمًا ينخفض إلى حد ما مقابل العمل ، ولكن الاتجاه سوف يكون هو نفسه تقريبا على أي حال.
- اختبار وحدة من رمز متحللة مبسطة وأكثر كفاءة.
- رمز المتحللة يخلق مقاومة أقل للتغيير. عند إجراء التغييرات ، يكون من غير المحتمل ارتكاب خطأ.
- رمز هو الحصول على أفضل منظم. البحث عن شيء في التعليمات البرمجية مرتبة في الملفات والمجلدات هو أسهل بكثير من في قدم واحدة كبيرة.
- يؤدي الفصل بين الشفرة النقطية ومنطق العمل إلى حقيقة أنه يمكن تطبيق إنشاء الشفرة في المشروع.
وكل هذه العلامات تسير معًا ، هذه علامات على نفس الكود. ليس عليك الاختيار بين ، على سبيل المثال ، رمز تم اختباره جيدًا ورمز منظم جيدًا.
التعاريف الموجودة لا تعمل
أحد التعاريف هو: "يجب أن يكون هناك سبب واحد فقط لتغيير الكود (الفئة أو الوظيفة)". المشكلة في هذا التعريف هي أنه يتعارض مع مبدأ الإغلاق المفتوح ، والثاني لمجموعة مبادئ سوليد. تعريفه: "يجب أن يكون الرمز مفتوحًا للتمديد ومغلقًا للتغيير." أحد أسباب التغيير مقابل الحظر الكامل للتغيير. إذا كشفنا بمزيد من التفصيل عما هو المقصود هنا ، اتضح أنه لا يوجد تعارض بين المبادئ ، ولكن هناك بالتأكيد تعارض بين التعاريف الغامضة.
التعريف الثاني الأكثر مباشرة هو: "يجب أن يكون للرمز مسؤولية واحدة فقط." المشكلة في هذا التعريف هي أنه من الطبيعة البشرية تعميم كل شيء.
على سبيل المثال ، هناك مزرعة تزرع الدجاج ، وفي تلك اللحظة تقع على المزرعة مسؤولية واحدة فقط. وهكذا يتم اتخاذ القرار لتربية البط هناك أيضًا. غريزي ، سوف نسمي هذه مزرعة دواجن ، بدلاً من الاعتراف بأن هناك الآن مسؤوليتين. أضف خروفًا ، وهذه الآن مزرعة للحيوانات الأليفة. ثم نريد أن نزرع الطماطم أو الفطر هناك ، ونأتي بالاسم الأكثر عمومية التالي. الأمر نفسه ينطبق على "سبب واحد" للتغيير. يمكن تعميم هذا السبب كما يكفي الخيال.
مثال آخر هو فئة مدير المحطة الفضائية. إنه لا يفعل شيئًا آخر ، فهو يدير المحطة الفضائية فقط. كيف تحب هذه الفئة مع مسؤولية واحدة؟
وبما أنني ذكرت Redux عندما يكون مقدم الطلب على دراية بهذه التقنية ، فأنا أطرح أيضًا السؤال ، هل ينتهك مخفض SRP النموذجي؟
المخفض ، على ما أذكر ، يتضمن بيان التبديل ، ويحدث أن ينمو إلى عشرات أو حتى مئات الحالات. والمسؤولية الوحيدة للمخفض هي إدارة التحولات في حالة طلبك. هذا هو ، حرفيا ، أجاب بعض المتقدمين. ولا يمكن لأي تلميحات أن تنقل هذا الرأي عن الأرض.
إجمالًا ، إذا بدا أن هناك نوعًا من الشفرات يفي بمبدأ SRP ، ولكن في نفس الوقت ، تنبعث منه رائحة كريهة - تعرف لماذا يحدث هذا. لأن تعريف "الكود يجب أن يتحمل مسؤولية واحدة" ببساطة لا يعمل.
تعريف أكثر ملاءمة
من التجربة والخطأ ، كان لدي تعريف أفضل:
يجب ألا تكون مسؤولية التعليمات البرمجية كبيرة جدًانعم ، أنت الآن بحاجة إلى "قياس" مسؤولية فئة أو وظيفة. وإذا كانت كبيرة جدًا ، فأنت بحاجة إلى تقسيم هذه المسؤولية الكبيرة إلى عدة مسؤوليات أصغر. بالعودة إلى مثال المزرعة ، حتى مسؤولية تربية الدجاج يمكن أن تكون كبيرة للغاية ومن المنطقي أن تفصل دجاج الشوايات بطريقة ما عن الدجاج البياض ، على سبيل المثال.
ولكن كيف يمكن قياسها ، وكيفية تحديد أن مسؤولية هذا الرمز كبيرة جدًا؟
لسوء الحظ ، ليس لدي طرق دقيقة رياضيا ، ولكن فقط الأساليب التجريبية. والأهم من ذلك كله هو أن التجربة تأتي ، فالمطورين المبتدئين ليسوا قادرين على الإطلاق على تحليل الشفرة ، والمطورين الأكثر تقدماً هم الأفضل في امتلاكها ، على الرغم من أنهم لا يستطيعون دائمًا وصف سبب قيامهم بذلك وكيف تقع في نظريات مثل SRP.
- متري التعقيد cyclomatic. لسوء الحظ ، هناك طرق لإخفاء هذا المقياس ، ولكن إذا قمت بجمعه ، فهناك فرصة لإظهار الأماكن الأكثر ضعفًا في تطبيقك.
- حجم الوظائف والفئات. لا يلزم قراءة وظيفة 800 خط لفهم أن هناك خطأ ما في ذلك.
- الكثير من الواردات. بمجرد أن فتحت ملفًا في مشروع فريق مجاور ورأيت شاشة كاملة للواردات ، ضغطت الصفحة لأسفل ومرة أخرى لم تكن هناك سوى واردات على الشاشة. فقط بعد الضغط الثاني رأيت بداية الكود. يمكنك أن تقول أن جميع IDEs الحديثة يمكن أن تخفي الواردات تحت "علامة الجمع" ، لكنني أقول أن الرمز الجيد لا يحتاج إلى إخفاء "الروائح". بالإضافة إلى ذلك ، كنت بحاجة إلى إعادة استخدام قطعة صغيرة من التعليمات البرمجية وأزلتها من هذا الملف إلى ملف آخر ، وتم نقل ربع أو حتى ثلث الواردات خلف هذه القطعة. من الواضح أن هذا الرمز لا ينتمي إلى هناك.
- اختبارات الوحدة. إذا كنت لا تزال تواجه صعوبة في تحديد مقدار المسؤولية ، فاجبر نفسك على كتابة الاختبارات. إذا كنت بحاجة إلى كتابة أكثر من عشرين اختبارًا حول الغرض الرئيسي من الوظيفة ، دون حساب حالات الشريط الحدودي ، وما إلى ذلك ، عندئذٍ يلزم التحلل.
- الأمر نفسه ينطبق على الكثير من الخطوات التحضيرية في بداية الاختبار والتحقق في النهاية. على شبكة الإنترنت ، بالمناسبة ، يمكنك العثور على بيان الطوباوي الذي يسمى يجب أن يكون هناك تأكيد واحد فقط في الاختبار. أعتقد أن أي فكرة جيدة بشكل تعسفي ، يتم طرحها على المطلق ، يمكن أن تصبح غير عملية بشكل سخيف.
- لا يجب أن يعتمد منطق الأعمال بشكل مباشر على الأدوات الخارجية. من المستحسن فصل برنامج تشغيل Oracle ، مسارات Express ، كل هذا عن منطق العمل و / أو الاختباء وراء الواجهات.
بضع نقاط:
بالطبع ، كما ذكرت بالفعل ، يوجد جانب للعملة ، وقد لا تكون 800 طريقة في سطر واحد أفضل من طريقة واحدة على 800 خط ، يجب أن يكون هناك توازن في كل شيء.
الثاني - أنا لا أتناول مسألة أين أضع هذا الكود أو ذاك وفقًا لمسؤوليته. على سبيل المثال ، في بعض الأحيان يواجه المطورون أيضًا صعوبات في جذب الكثير من المنطق إلى طبقة DAL.
ثالثًا ، لا أقترح أي حدود ثابتة محددة مثل "لا يزيد عن 50 سطرًا لكل وظيفة". ينطوي هذا النهج فقط على اتجاه لتطوير المطورين ، وربما فرق. إنه يعمل من أجلي ، يجب عليه كسب المال من أجل الآخرين.
والشيء الأخير ، إذا ذهبت عبر TDD ، فإن هذا وحده سيجعلك بالتأكيد تتحلل الشفرة قبل وقت طويل من كتابة تلك الاختبارات العشرين مع 20 تأكيدًا لكل منها.
فصل منطق العمل عن كود النمذجة
الحديث عن قواعد رمز جيد ، لا يمكنك الاستغناء عن الأمثلة. المثال الأول يدور حول فصل كود النمطي.

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

في هذا المثال ، كل التفاعل مع Express موجود في ملف منفصل.
للوهلة الأولى ، قد يبدو أن المثال الثاني لم يحقق تحسينات ، فقد كان هناك ملفان بدلاً من واحد ، ظهرت خطوط إضافية لم تكن موجودة من قبل - اسم الفئة وتوقيع الطريقة. ثم ماذا يعطي هذا الفصل من الكود؟ بادئ ذي بدء ، "نقطة إدخال التطبيق" لم تعد Express. الآن هذه هي وظيفة Typescript العادية. أو وظيفة جافا سكريبت ، سواء C # ، الذي يكتب WebAPI على ما.
هذا ، بدوره ، يسمح لك بتنفيذ مختلف الإجراءات التي لا تتوفر في المثال الأول. على سبيل المثال ، يمكنك كتابة اختبارات السلوك دون الحاجة إلى رفع Express ، دون استخدام طلبات http داخل الاختبار. وحتى لا تكون هناك حاجة إلى إجراء أي نوع من التبول ، استبدل كائن Router بكائن "اختبار" ، والآن يمكن ببساطة استدعاء رمز التطبيق مباشرة من الاختبار.
ميزة أخرى مثيرة للاهتمام التي يوفرها هذا التحلل هي أنه يمكنك الآن كتابة منشئ رمز يقوم بتحليل userApiService واستخدامه لإنشاء رمز يربط هذه الخدمة مع Express. أخطط في منشوراتي المستقبلية للإشارة إلى ما يلي: لن يؤدي توفير الشفرة إلى توفير الوقت في عملية كتابة التعليمات البرمجية. لن يتم سداد تكاليف مولد الشفرة بحقيقة أنك الآن لا تحتاج إلى نسخ هذا الملف. إن إنشاء الشفرة سيؤتي ثماره بحقيقة أن الشفرة التي تنتجها لا تحتاج إلى دعم ، مما سيوفر الوقت ، والأهم من ذلك ، أعصاب المطورين على المدى الطويل.
فرق تسد
طريقة كتابة الكود هذه موجودة منذ وقت طويل ، لم أخترعها بنفسي. لقد توصلت للتو إلى استنتاج مفاده أنها مريحة للغاية عند كتابة منطق العمل. ولهذا السبب ، توصلت إلى مثال وهمي آخر ، أظهر كيف يمكنك بسرعة وسهولة كتابة التعليمات البرمجية التي تتحلل بشكل جيد فورًا وأيضًا توثيقها ذاتيًا باستخدام طرق التسمية.
لنفترض أنك حصلت على مهمة من محلل أعمال لإنشاء طريقة ترسل تقرير الموظف إلى شركة تأمين. للقيام بذلك:
- يجب أن تؤخذ البيانات من قاعدة البيانات
- تحويل إلى الشكل المطلوب
- إرسال التقرير الناتج
لا تتم كتابة هذه المتطلبات دائمًا بشكل صريح ، وأحيانًا قد يكون هذا التسلسل ضمنيًا أو موضحًا من محادثة مع المحلل. في عملية تنفيذ هذه الطريقة ، لا تتعجل لفتح الاتصالات بقاعدة البيانات أو الشبكة ، بل حاول ترجمة هذه الخوارزمية البسيطة إلى الكود "كما هي". شيء مثل هذا:
async function sendEmployeeReportToProvider(reportId){ const data = await dal.getEmployeeReportData(reportId); const formatted = reportDataService.prepareEmployeeReport(data); await networkService.sendReport(formatted); }
من خلال هذا النهج ، اتضح أنه رمز بسيط إلى حد ما وسهل القراءة والاختبار ، على الرغم من أنني أعتقد أن هذا الرمز تافه ولا يحتاج إلى اختبار. وكانت مسؤولية هذه الطريقة هي عدم إرسال تقرير ، وكانت مسؤوليتها تقسيم هذه المهمة المعقدة إلى ثلاث مهام فرعية.
بعد ذلك ، نعود إلى المتطلبات ونكتشف أن التقرير يجب أن يتكون من قسم الرواتب وقسم مع ساعات العمل.
function prepareEmployeeReport(reportData){ const salarySection = prepareSalarySection(reportData); const workHoursSection = prepareWorkHoursSection(reportData); return { salarySection, workHoursSection }; }
وما إلى ذلك ، وما زلنا نواصل تقسيم المهمة حتى يتم تنفيذ أساليب صغيرة قريبة من بقايا تافهة.
التفاعل مع مبدأ الإغلاق المفتوح
قلت في بداية المقال إن تعاريف مبادئ برنامج التقويم الاستراتيجي والإغلاق المفتوح تتناقض مع بعضها البعض. الأول يقول أنه يجب أن يكون هناك سبب واحد للتغيير ، والثاني يقول أنه يجب إغلاق الشفرة للتغيير. والمبادئ نفسها ، لا تتناقض مع بعضها البعض فقط ، بل على العكس ، إنها تعمل بتآزر مع بعضها البعض. تهدف جميع مبادئ SOLID الخمسة إلى هدف واحد جيد - لإخبار المطور بالرمز "السيئ" وكيفية تغييره بحيث يصبح "جيدًا". المفارقة - لقد استبدلت للتو 5 مسؤوليات بمسؤولية أخرى واحدة.
لذلك ، بالإضافة إلى المثال السابق مع إرسال التقرير إلى شركة التأمين ، تخيل أن محلل أعمال يأتي إلينا ويقول إننا بحاجة الآن إلى إضافة وظيفة ثانية للمشروع. يجب طباعة نفس التقرير.
تخيل أن هناك مطورًا يعتقد أن برنامج التقويم الاستراتيجي "لا يتعلق بالتحلل".
وفقًا لذلك ، لم يوضح هذا المبدأ له الحاجة إلى التحلل ، وأدرك المهمة الأولى بأكملها في وظيفة واحدة. بعد أن جاءت المهمة إليه ، يجمع بين المسؤوليتين في واحدة ، لأنه لديهم الكثير من القواسم المشتركة ويعمم اسمه. الآن تسمى هذه المسؤولية "تقرير الخدمة". تنفيذ هذا يبدو مثل هذا:
async function serveEmployeeReportToProvider(reportId, serveMethod){ switch(serveMethod) { case sendToProvider: case print: default: throw; } }
تذكر بعض الكود في مشروعك؟ كما قلت ، لا يعمل كلا التعاريف المباشرة لـ SRP. إنهم لا ينقلون المعلومات إلى المطور بأن هذه الشفرة لا يمكن كتابتها. وما رمز يمكن كتابتها. لا يزال هناك سبب واحد فقط للمطور لتغيير هذا الرمز. انه ببساطة إعادة تسمية السبب السابق ، وأضاف التبديل والهدوء. وهنا يأتي مبدأ مبدأ الإغلاق المفتوح إلى الموقع ، والذي يقول مباشرة إنه كان من المستحيل تعديل ملف موجود. كان من الضروري كتابة التعليمات البرمجية بحيث عند إضافة وظيفة جديدة ، كان من الضروري إضافة ملف جديد ، وليس تحرير ملف موجود. وهذا هو ، مثل هذا الرمز هو سيء من وجهة نظر مبدأين في وقت واحد. وإذا لم يساعد الأول في رؤيته ، فيجب أن يساعد الثاني.
وكيف تحل طريقة فرق تسد نفس المشكلة:
async function printEmployeeReport(reportId){ const data = await dal.getEmployeeReportData(reportId); const formatted = reportDataService.prepareEmployeeReport(data); await printService.printReport(formatted); }
إضافة وظيفة جديدة. أحيانًا أسمىهم "وظيفة البرنامج النصي" لأنهم لا يحملون تطبيقات ؛ فهم يحددون تسلسل استدعاء الأجزاء المتحللة من مسؤوليتنا. من الواضح أن أول سطرين ، يتزامن المسؤولان الأولان المتحللان مع أول سطرين من الوظيفة المنفذة سابقًا. تماما مثل الخطوات الأولى والثانية من مهمتين وصفها محلل أعمال يتزامن.
وبالتالي ، لإضافة وظائف جديدة إلى المشروع ، أضفنا طريقة جديدة للبرنامج النصي و printService جديد. الملفات القديمة لم تتغير. أي أن طريقة كتابة الكود جيدة من وجهة نظر مبدأين. و SRP وفتح إغلاق
البديل
أردت أيضًا أن أذكر طريقة بديلة منافسة للحصول على رمز متحلل جيدًا يبدو كهذا - أولاً نكتب الكود "على الجبهة" ، ثم نعيد تشكيله باستخدام تقنيات مختلفة ، على سبيل المثال ، وفقًا لكتاب فاولر "Refactoring". ذكّرتني هذه الطرق بالنهج الرياضي في لعبة الشطرنج ، حيث لا تفهم بالضبط ما تفعله من حيث الاستراتيجية ، فأنت فقط تحسب "وزن" مركزك وتحاول تعظيمه عن طريق اتخاذ خطوات. لم يعجبني هذا النهج لسبب واحد بسيط - من الصعب تسمية الأساليب والمتغيرات بالفعل ، وعندما لا يكون لها قيمة أعمال ، يصبح ذلك مستحيلًا. على سبيل المثال ، إذا كانت هذه التقنيات تشير إلى أنك تحتاج إلى تحديد 6 خطوط متطابقة من هنا ومن هناك ، ثم تسليط الضوء عليها ، ما الذي يجب أن نسميه هذه الطريقة؟ someSixIdenticalLines ()؟
أريد أن أبدي تحفظًا - لا أعتقد أن هذه الطريقة سيئة ، لم أستطع تعلم كيفية استخدامها.
في المجموع
في اتباع هذا المبدأ ، يمكنك أن تجد الفوائد.
تعريف "يجب أن تكون هناك مسؤولية واحدة" لا يعمل.
هناك تعريف أفضل وعدد من الميزات غير المباشرة ، ما يسمى رمز رائحة تشير إلى الحاجة إلى تتحلل.
يتيح لك نهج "الفجوة والقهر" أن تكتب فورًا كودًا جيد التنظيم وموثقًا ذاتيًا.