أربع قواعد محسنة لتصميم البرمجيات

مرحبا يا هبر! أقدم إليكم مقالة "أربع قواعد أفضل لتصميم البرامج" بقلم ديفيد براينت كوبلاند. ديفيد براينت كوبلاند هو مهندس برمجيات و CTO لـ Stitch Fix. لديه مدونة وهو مؤلف لعدة كتب .


قام مارتن فاولر بالتغريد مؤخرًا باستخدام رابط إلى مدونته حول أربع قواعد تصميم بسيطة من Kent Beck ، والتي أعتقد أنه يمكن تحسينها أكثر (والتي يمكن أن ترسل المبرمج في بعض الأحيان على المسار الخطأ):


شرح قواعد Kent من البرمجة المتطرفة :


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

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


  • يتم تغطيتها بشكل جيد من قبل الاختبارات وتمريرها بنجاح.
  • لا يحتوي على ملفات تجريدية لا يحتاجها البرنامج مباشرة.
  • لديها سلوك لا لبس فيه.
  • يتطلب أقل قدر من المفاهيم.

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


إذن ماذا نفعل مع برنامجنا؟


لا يمكننا التحدث عن تصميم البرنامج دون التحدث أولاً عما نعتزم القيام به.


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


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


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


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


يتطلب القضاء على ازدواجية التعليمات البرمجية التجريدات ، وتؤدي التجريدات إلى التعقيد


لا تكرر نفسك أو تستخدم DRY لتبرير قرارات التصميم المثيرة للجدل. هل سبق لك أن رأيت كودًا مشابهًا؟


ZERO = BigDecimal.new(0) 

بالإضافة إلى ذلك ، ربما رأيت شيئًا كهذا:


 public void call(Map payload, boolean async, int errorStrategy) { // ... } 

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


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


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


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


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


هذا يقودنا إلى النقطة التالية - النية مقابل السلوك.


نية المبرمج لا معنى لها - السلوك يعني كل شيء


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


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


 function LastModified(props) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } 

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


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


 function LastModified(props) { if (!props.date) { throw "LastModified requires a date to be passed"; } return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } 

أو حتى مثل هذا:


 function LastModified(props) { if (props.date) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } else { return <div>Never modified</div>; } } 

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


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


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


التكاليف المفاهيمية تساهم في الارتباك والتعقيد


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


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


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


عندما نكتب برامج الكود أو التصميم ، يجب أن نتوقف عن التفكير في الأناقة أو الجمال أو أي تدبير شخصي آخر في الكود. بدلاً من ذلك ، يجب أن نتذكر دائمًا ما سنفعله مع البرنامج.


أنت لا تعلق الكود على الحائط - يمكنك تغييره


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


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


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


حاول دائمًا إبقاء السلوك بسيطًا للعرض والتنبؤ والفهم ، والحفاظ على عدد المفاهيم إلى الحد الأدنى المطلق.

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


All Articles