مبدأ المسؤولية الفردية. ليس بهذه البساطة كما يبدو

صورة مبدأ المسؤولية الفردية ، هو مبدأ المسؤولية الفردية ،
هو مبدأ التباين الموحد - رجل زلق للغاية لفهم ومثل هذا السؤال العصبي في مقابلة مبرمج.


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


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


الحالة عندما كان حجم قائمة الانتظار مضاعفات ثلاثة ، وكان تنفيذ جيد لـ SRP.


تعريف 1. مسؤولية واحدة.


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


النظر في وجوه Tippler.
لتحقيق مبدأ SRP ، نقسم المسؤوليات إلى ثلاثة:


  • صب واحد ( PourOperation )
  • مشروبات واحدة ( DrinkUpOperation )
  • وجبة خفيفة واحدة ( TakeBiteOperation )

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


الشراب ، بدوره ، هو الواجهة لهذه العمليات:


lass Tippler { //... void Act(){ _pourOperation.Do() //  _drinkUpOperation.Do() //  _takeBiteOperation.Do() //  } } 

صورة

لماذا؟


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


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


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


لذلك ، يعد SRP مبدأً يشرح كيف تتحلل ، أي مكان رسم خط الفصل .


يقول إن التحلل يجب أن يستند إلى مبدأ الفصل بين "المسؤولية" ، أي وفقًا لمهام الأشياء المختلفة.


صورة

دعنا نرجع إلى الخليط والمزايا التي يحصل عليها شخص قرد عند التحلل:


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

وبالطبع ، فإن سلبيات:


  • سوف تضطر إلى إنشاء المزيد من الأنواع.
  • سوف يشرب الخاسر للمرة الأولى بعد ساعتين مما يستطيع

تعريف 2. تقلب موحد.


اسمح للسادة! يتحمل فصل الشرب مسؤولية واحدة - إنه يشرب! وبشكل عام ، فإن كلمة "مسؤولية" هي مفهوم غامض للغاية. شخص ما مسؤول عن مصير البشرية ، وشخص مسؤول عن تربية طيور البطريق المائلة للقطب.


النظر في اثنين من تطبيقات البنغو. الأول ، المذكور أعلاه ، يحتوي على ثلاثة فصول - صب ، وشرب وتعض.


تتم كتابة الطريقة الثانية من خلال منهجية Forward و Only Forward وتحتوي على كل المنطق في طريقة Act :


 //      .    lass BrutTippler { //... void Act(){ //  if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity)) throw new OverdrunkException(); //  if(!_hand.TryDrink(from: _glass, size: _glass.Capacity)) throw new OverdrunkException(); // for(int i = 0; i< 3; i++){ var food = _foodStore.TakeOrDefault(); if(food==null) throw new FoodIsOverException(); _hand.TryEat(food); } } } 

كلتا الفئتين ، من وجهة نظر مراقب خارجي ، تبدو متماثلة تمامًا وتضطلع بالمسؤولية الفردية المتمثلة في "الشرب".


الارتباك!


ثم نتصفح الإنترنت ونعرف تعريفًا آخر لـ SRP - مبدأ التباين الموحد.


ينص هذا التعريف على أن " الوحدة النمطية لها سبب واحد فقط للتغيير ". وهذا هو ، "المسؤولية مناسبة للتغيير".


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


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


التعريف 3. توطين التغييرات.


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


دعنا نبدأ تسجيل الدخول مع عملية صب:


 class PourOperation: IOperation{ PourOperation(ILogger log /*....*/){/*...*/} //... void Do(){ _log.Log($"Before pour with {_hand} and {_bottle}"); //Pour business logic ... _log.Log($"After pour with {_hand} and {_bottle}"); } } 

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


 interface IPourLogger{ void LogBefore(IHand, IBottle){} void LogAfter(IHand, IBottle){} void OnError(IHand, IBottle, Exception){} } class PourOperation: IOperation{ PourOperation(IPourLogger log /*....*/){/*...*/} //... void Do(){ _log.LogBefore(_hand, _bottle); try{ //... business logic _log.LogAfter(_hand, _bottle"); } catch(exception e){ _log.OnError(_hand, _bottle, e) } } } 

سوف يلاحظ قارئ دقيق أنه يمكن أيضًا تغيير LogAfter و LogBefore و OnError بشكل فردي ، ومن خلال القياس مع الخطوات السابقة ، سيتم إنشاء ثلاث فئات: PourLoggerBefore و PourLoggerAfter و PourErrorLogger .


وتذكر أن هناك ثلاث عمليات للنغمة - نحصل على تسع فصول تسجيل. نتيجة لذلك ، يتكون خمر كامل من 14 (!!!) فصول.


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


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


... لا تعرف أبدًا وجود التعريف الثالث لـ Srp:


" يجب تخزين الأشياء المشابهة للتغيير في مكان واحد ." أو " ما يتغير معًا يجب أن يبقى في مكان واحد "


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


هذه نقطة مهمة للغاية - حيث أن جميع تفسيرات SRP التي تم ذكرها أعلاه تقضي بأن الأنواع يجب تقسيمها أثناء تقسيمها ، أي فرض "قيود أعلى" على حجم الكائن ، والآن نحن نتحدث عن "حد أدنى" . بمعنى آخر ، لا يتطلب SRP فقط "التكسير أثناء التكسير" ، ولكن أيضًا لا يتطلب المبالغة في ذلك - "لا تسحق الأشياء المرتبطة" . لا تعقد دون داع. هذه هي معركة شائكة كبيرة مع الرجل القرد!


صورة

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


 class OperationLogger{ public OperationLogger(string operationName){/*..*/} public void LogBefore(object[] args){/*...*/} public void LogAfter(object[] args){/*..*/} public void LogError(object[] args, exception e){/*..*/} } 

وإذا تمت إضافة النوع الرابع من العمليات إلينا ، فسيكون التسجيل جاهزًا لذلك. ورمز العمليات نفسها نظيف وخالي من ضوضاء البنية التحتية.


نتيجة لذلك ، لدينا 5 فصول لحل مشكلة الشرب:


  • صب العملية
  • شرب العملية
  • عملية المربى
  • Logirovschik
  • واجهة Boolers

كل واحد منهم مسؤول تمامًا عن وظيفة واحدة ، وله سبب واحد للتغيير. تقع جميع القواعد المشابهة للتغييرات في مكان قريب.


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


التسلسل وإلغاء التسلسل

كجزء من تطوير بروتوكول نقل البيانات ، من الضروري إجراء تسلسل وتحرير نوع من "المستخدم" إلى سلسلة.


 User{ String Name; Int Age; } 

قد تعتقد أنه يلزم إجراء التسلسل وإلغاء التسلسل في فصول منفصلة:


 UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} } 

لأن كل واحد منهم لديه مسؤوليته الخاصة وسبب واحد للتغيير.


لكن لديهم سبب شائع للتغيير - "تغيير تنسيق تسلسل البيانات."
وعند تغيير هذا التنسيق ، ستتغير التسلسل وإلغاء التسلسل دائمًا.


وفقًا لمبدأ توطين التغييرات ، يجب علينا دمجها في فئة واحدة:


 UserSerializer{ String deserialize(User){...} User serialize(String){...} } 

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


عد وانقاذ

تحتاج إلى حساب الإيرادات السنوية للشركة وحفظها في الملف C: \ results.txt.


نحل هذه المشكلة بسرعة باستخدام طريقة واحدة:


 void SaveGain(Company company){ //     //   } 

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


لذلك ، تحتاج إلى تقسيم هذه الطريقة إلى قسمين:


 Gain CalcGain(Company company){..} void SaveGain(Gain gain){..} 

الايجابيات:


  • يمكن اختبارها بشكل منفصل CalcGain
  • أسهل لترجمة الأخطاء وإجراء التغييرات
  • زيادة قراءة التعليمات البرمجية
  • يتم تقليل خطر الخطأ في كل طريقة بسبب التبسيط

منطق الأعمال المتطور

بمجرد أن كتبنا خدمة للتسجيل التلقائي لعميل b2b. وكان هناك طريقة الله مع 200 خطوط من محتوى مماثل:


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

كان هناك حوالي 10 عمليات تجارية أخرى مع اتصال رهيب في هذه القائمة. كائن الحساب كان مطلوبًا من قِبل الجميع تقريبًا. هناك حاجة إلى معرف نقطة واسم العميل في نصف المكالمات.


بعد ساعة من إعادة البناء ، تمكنا من فصل رمز البنية التحتية وبعض الفروق الدقيقة في العمل مع الحساب في طرق / فصول منفصلة. أصبحت طريقة الله أسهل ، ولكن كان هناك 100 سطر من الشفرة تركت لا تريد أن تنهار.


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


الشكلية.


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


الشكلية 1. تعريف SRP


  1. افصل بين العناصر بحيث يكون كل واحد منهم مسؤولاً عن شيء واحد.
  2. المسؤولية تعني "سبب التغيير". أي أن لكل عنصر سبب واحد فقط للتغيير ، من حيث منطق العمل.
  3. التغييرات المحتملة في منطق الأعمال. يجب أن تكون مترجمة. يجب أن تكون العناصر القابلة للتغيير معًا قريبة.

الشكلية 2. المعايير اللازمة للفحص الذاتي.


لم أحقق المعايير الكافية لتنفيذ برنامج التقويم الاستراتيجي. ولكن هناك شروط ضرورية:


1) اسأل نفسك سؤالًا - ماذا تفعل هذه الفئة / الطريقة / الوحدة / الخدمة. يجب أن تجيب عليه بتعريف بسيط. (بفضل Brightori )


تفسيرات

ومع ذلك ، في بعض الأحيان يكون من الصعب للغاية العثور على تعريف بسيط


2) إصلاح الخلل أو إضافة ميزة جديدة يؤثر على الحد الأدنى لعدد الملفات / الفئات. من الناحية المثالية ، واحد.


تفسيرات

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


مثال آخر هو إضافة عنصر تحكم واجهة مستخدم جديد مشابه لتلك السابقة. إذا كان هذا يفرض عليك إضافة 10 كيانات مختلفة و 15 محولًا مختلفًا - يبدو أنك قد "كسرت".


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


تفسيرات

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


4) عند توضيح سؤال حول منطق العمل (من مطور أو مدير) ، يمكنك الصعود بدقة إلى فئة / ملف واحد وتلقي المعلومات من هناك فقط.


تفسيرات

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


5) التسمية واضحة.


تفسيرات

فئة أو طريقة لدينا هي المسؤولة عن شيء واحد ، وينعكس المسؤولية في اسمها.


AllManagersManagerService - على الأرجح ، من الدرجة الله
LocalPayment - ربما لا


الشكلية 3. طريقة تطوير اوكام الأولى.


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


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

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


لقد حان الوقت للالتفاف


لا يقتصر نطاق SRP على OOP و SOLID. هذا ينطبق على الطرق والوظائف والفئات والوحدات والخدمات الميكروية والخدمات. ينطبق ذلك على كل من "figax-figax-in-in-prod" و "الصواريخ sainz" ، مما يجعل العالم أفضل قليلاً في كل مكان. إذا فكرت في الأمر ، فهذا هو المبدأ الأساسي لكل الهندسة تقريبًا. الهندسة الميكانيكية ، وأنظمة التحكم ، وبالفعل جميع الأنظمة المعقدة مبنية من مكونات ، و "تفتيت غير مكتمل" يحرم المصممين من المرونة ، و "التشرذم" - من الكفاءة ، والحدود غير الصحيحة - للعقل وراحة البال.


صورة

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

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


All Articles