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

تتمثل الطريقة الثانية للتعقيد في الوصول إلى الكود في الإضافة الإضافية: عند إرسال سطر أو سطرين إلى الوظيفة الحالية. ومن الصعب للغاية ملاحظة أن وظيفتك كانت على ما يرام منذ وقت واحد ، لكنها الآن معقدة للغاية. يستغرق جزءًا كبيرًا من التركيز ، ومهارة المراجعة ، وممارسة الملاحة الجيدة بالشفرة لتحديد موقعها فعليًا. معظم الناس (مثلي!) يفتقرون إلى هذه المهارات والسماح للتعقيد بالدخول إلى قاعدة البيانات بانتظام.
لذلك ، ما الذي يمكن عمله لمنع التعليمات البرمجية الخاصة بك من التعقيد؟ نحن بحاجة إلى استخدام الأتمتة! لنجعل الغوص عميقًا في تعقيد الشفرة وطرق العثور عليها وحلها أخيرًا.
في هذه المقالة ، سأوجهك خلال الأماكن التي يعيش فيها التعقيد وكيفية محاربته هناك. بعد ذلك سنناقش كيف تمكن الشفرة والأتمتة البسيطة المكتوبة جيدًا من إتاحة الفرصة لأنماط تطوير "إعادة البناء المستمر" و "الهندسة المعمارية حسب الطلب".
وأوضح تعقيد
قد يتساءل المرء: ما هو بالضبط "تعقيد الرمز"؟ وبينما يبدو مألوفًا ، هناك عقبات خفية في فهم موقع التعقيد الدقيق. لنبدأ بالأجزاء الأكثر بدائية ثم ننتقل إلى الكيانات الأعلى مستوى.
تذكر ، أن هذه المقالة يدعى "شلال التعقيد"؟ سأريكم كيف يتدفق التعقيد من أبسط الأوليات إلى أعلى التجريدات.
سوف أستخدم python
كلغة رئيسية لأمثلة wemake-python-styleguide
style كأداة الفحص الرئيسية للعثور على الانتهاكات في الكود الخاص بي وتوضيح وجهة نظري.
التعبير
كل ما تبذلونه من رمز يتكون من التعبيرات البسيطة مثل a + 1
print(x)
. على الرغم من أن التعبيرات نفسها بسيطة ، فإنها قد تتفوق بشكل غير ملحوظ على الكود الخاص بك مع التعقيد في مرحلة ما. مثال: تخيل أن لديك قاموسًا يمثل بعض نماذج User
وأنك تستخدمه كما يلي:
def format_username(user) -> str: if not user['username']: return user['email'] elif len(user['username']) > 12: return user['username'][:12] + '...' return '@' + user['username']
تبدو بسيطة جدا ، أليس كذلك؟ في الواقع ، فإنه يحتوي على مسألتين على أساس التعقيد. إنها overuses 'username'
سلسلة overuses 'username'
وتستخدم الرقم السحري 12
(لماذا نستخدم هذا الرقم في المقام الأول ، ولماذا لا 13
أو 10
؟). من الصعب العثور على هذه الأنواع من الأشياء بنفسك. إليك كيفية ظهور الإصدار الأفضل:
هناك مشاكل مختلفة مع التعبير كذلك. يمكننا أيضًا استخدام تعبيرات مفرطة في الاستخدام : عند استخدام سمة some_object.some_attr
كل مكان بدلاً من إنشاء متغير محلي جديد. يمكن أن يكون لدينا أيضًا ظروف منطقية معقدة للغاية أو الوصول إلى نقطة عميقة جدًا .
الحل : قم بإنشاء متغيرات أو وسيطات أو ثوابت جديدة. قم بإنشاء واستخدام وظائف أو أساليب مساعدة جديدة إذا كان عليك ذلك.
خطوط
تشكل التعبيرات أسطرًا برمجية (من فضلك ، لا تخلط بين السطور والعبارات: عبارة واحدة يمكن أن تأخذ أسطر متعددة وقد توجد عبارات متعددة في سطر واحد)
المقياس الأول والأكثر وضوحًا لتعقيد الخط هو طوله. نعم ، سمعت ذلك بشكل صحيح. لهذا السبب (نحن المبرمجون) نفضل التمسك بـ 80
حرفًا لكل سطر وليس لأنه كان يستخدم من قبل في آلات الكتابة اليدوية. هناك الكثير من الشائعات حول هذا الأمر مؤخرًا ، قائلة إنه لا يوجد أي معنى لاستخدام 80
حرفًا لرمزك في 2k19. لكن هذا ليس صحيحًا بالتأكيد.
الفكرة بسيطة. يمكن أن يكون لديك ضعف المنطق في سطر يحتوي على 160
حرفًا مقارنةً بـ 80
حرفًا فقط. لهذا السبب يجب تعيين هذا الحد وإنفاذه. تذكر ، هذا ليس اختيار الأسلوب . وهو متري التعقيد!
مقياس التعقيد في الخط الرئيسي الثاني أقل شهرة وأقل استخدامًا. ويسمى جونز التعقيد . الفكرة وراء ذلك بسيطة: نحسب العقد الشفرة (أو ast
) في سطر واحد للحصول على تعقيدها. دعونا نلقي نظرة على المثال. يختلف هذان الخطان اختلافًا جوهريًا من حيث التعقيد ولكنهما لهما نفس العرض بالحروف:
print(first_long_name_with_meaning, second_very_long_name_with_meaning, third) print(first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2))
دعونا نحسب العقد في أول واحد: مكالمة واحدة ، ثلاثة أسماء. أربع عقد تماما. الثاني لديه واحد وعشرون العقد. حسنا ، الفرق واضح. لهذا السبب نستخدم مقياس جونز المعقدة للسماح للخط الطويل الأول وعدم السماح للخط الثاني على أساس التعقيد الداخلي ، وليس فقط على طول الخام.
ما يجب القيام به مع خطوط درجة جونز عالية التعقيد؟
الحل : قم بتقسيمها إلى عدة أسطر أو إنشاء متغيرات وسيطة جديدة ووظائف الأداة وفئات جديدة وما إلى ذلك.
print( first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2), )
الآن هو الطريق أكثر قابلية للقراءة!
الهياكل
تتمثل الخطوة التالية في تحليل بنيات اللغة مثل ، if
، for
، with
، إلخ ، والتي تم تشكيلها من خطوط وتعبيرات. يجب أن أقول إن هذه النقطة خاصة باللغة. سأعرض عدة قواعد من هذه الفئة باستخدام python
كذلك.
سنبدأ مع if
. ماذا يمكن أن يكون أسهل من حسن البالغ من العمر if
؟ في الواقع ، if
بدأت في الحصول على صعبة حقا صعبة. فيما يلي مثال على كيفية reimplement switch
مع:
if isinstance(some, int): ... elif isinstance(some, float): ... elif isinstance(some, complex): ... elif isinstance(some, str): ... elif isinstance(some, bytes): ... elif isinstance(some, list): ...
ما هي المشكلة مع هذا الرمز؟ حسنًا ، تخيل أن لدينا عشرات أنواع البيانات التي يجب تغطيتها بما في ذلك الأنواع الجمركية التي لم نعلم بها بعد. ثم هذا الرمز المركب هو مؤشر على أننا نختار نمطًا خاطئًا هنا. نحتاج إلى إعادة تفعيل كودنا لإصلاح هذه المشكلة. على سبيل المثال ، يمكن للمرء استخدام es typeclass
أو typeclass
. هم نفس الوظيفة ، ولكن أجمل.
python
لا يتوقف أبدا لتسلية لنا. على سبيل المثال ، يمكنك الكتابة with
عدد كبير من الحالات ، وهو أمر معقد للغاية ومربك:
with first(), second(), third(), fourth(): ...
يمكنك أيضًا كتابة فهم مع أي عدد من التعبيرات " if
أو "للتعبير" ، مما قد يؤدي إلى رمز معقد وغير قابل للقراءة:
[ (x, y, z) for x in x_coords for y in y_coords for z in z_coords if x > 0 if y > 0 if z > 0 if x + y <= z if x + z <= y if y + z <= x ]
قارنه بإصدار بسيط وقابل للقراءة:
[ (x, y, z) for x, y, x in itertools.product(x_coords, y_coords, z_coords) if valid_coordinates(x, y, z) ]
يمكنك أيضًا تضمين multiple statements inside a try
بطريق الخطأ multiple statements inside a try
قضية multiple statements inside a try
، وهي غير آمنة لأنها يمكن أن تثير استثناءً وتعالجه في مكان متوقع:
try: user = fetch_user()
وهذه ليست حتى 10 ٪ من الحالات التي يمكن أن تسوء مع رمز python
الخاص بك. هناك الكثير والكثير من الحالات التي يجب تتبعها وتحليلها.
الحل : الحل الوحيد الممكن هو استخدام لغة جيدة للغة التي تختارها. ومجمع refactor الأماكن التي يسلط الضوء على هذا linter. خلاف ذلك ، سوف تضطر إلى إعادة اختراع العجلة وتعيين سياسات مخصصة لنفس المشاكل بالضبط.
وظائف
أشكال التعبير والعبارات والهياكل تشكل وظائف. التعقيد من هذه الكيانات يتدفق إلى وظائف. وهنا تبدأ الأمور بالفضول. لأن الوظائف تحتوي بالفعل على عشرات مقاييس التعقيد: جيدة وأخرى سيئة.
سنبدأ بالأكثر شهرة: التعقيد السيكلومي وطول الوظيفة المقاس بخطوط الشفرة. يشير التعقيد الحلقي إلى عدد الدورات التي يمكن أن يستغرقها تدفق التنفيذ الخاص بك: فهو يساوي تقريبًا عدد اختبارات الوحدات المطلوبة لتغطية شفرة المصدر بالكامل. إنه مقياس جيد لأنه يحترم الدلالي ويساعد المطور على القيام بإعادة بيع المباني. من ناحية أخرى ، طول الوظيفة هو مقياس سيء. لا تتوافق مع مقياس جونز المعقّد الذي سبق شرحه لأننا نعرف بالفعل: الأسطر المتعددة أسهل في القراءة من سطر واحد كبير مع كل شيء بداخله. سنركز على المقاييس الجيدة فقط ونتجاهل المقاييس السيئة.
بناءً على تجربتي ، يجب حساب مقاييس التعقيد المفيدة المتعددة بدلاً من طول الوظيفة العادية:
- عدد من الديكور وظيفة. أقل هو أفضل
- عدد الحجج أقل هو أفضل
- عدد التعليقات التوضيحية ؛ أعلى هو أفضل
- عدد المتغيرات المحلية ؛ أقل هو أفضل
- عدد العوائد ، الغلة ، ينتظر. أقل هو أفضل
- عدد البيانات والعبارات ؛ أقل هو أفضل
يتيح لك الجمع بين كل هذه الاختبارات حقًا كتابة وظائف بسيطة (يتم تطبيق جميع القواعد أيضًا على الطرق أيضًا).
عندما تحاول القيام ببعض الأشياء السيئة من خلال وظيفتك ، فإنك بالتأكيد ستكسر مقياسًا واحدًا على الأقل. وهذا سيخيب linter لدينا وتفجير البناء الخاص بك. نتيجة لذلك ، سيتم حفظ وظيفتك.
الحل : عندما تكون إحدى الوظائف معقدة للغاية ، فإن الحل الوحيد لديك هو تقسيم هذه الوظيفة إلى عدة وظائف.
فصول
المستوى التالي من التجريد بعد الدوال عبارة عن فصول. وكما خمنت بالفعل ، فهي أكثر تعقيدًا وسيولة من الوظائف. لأن الفصول قد تحتوي على وظائف متعددة في الداخل (تسمى الأسلوب) ولديها ميزات فريدة أخرى مثل الوراثة والخلطات ، والسمات على مستوى الفصل والديكور على مستوى الفصل. لذلك ، علينا أن نتحقق من كل الطرق كوظائف وجسم الفصل نفسه.
بالنسبة للفصول الدراسية ، علينا قياس المقاييس التالية:
- عدد من الديكور على مستوى الصف. أقل هو أفضل
- عدد الطبقات الأساسية ؛ أقل هو أفضل
- عدد الصفات العامة على مستوى الصف ؛ أقل هو أفضل
- عدد الصفات العامة على مستوى المثيل ؛ أقل هو أفضل
- عدد الطرق أقل هو أفضل
عندما يكون أي من هذه الأمور معقدًا للغاية - علينا أن ندق ناقوس الخطر ونفشل في الإنشاء!
الحل : refactor فصلك الفاشلة! قسّم فئة معقدة واحدة إلى عدة فئات بسيطة أو قم بإنشاء وظائف فائدة جديدة واستخدم التكوين.
ذكر جدير بالذكر: يمكن للمرء أيضًا تتبع مقاييس التماسك والاقتران للتحقق من تعقيد تصميم OOP الخاص بك.
وحدات
تحتوي الوحدات النمطية على عبارات ووظائف وفئات متعددة. وكما ذكرتم بالفعل ، فإننا ننصح عادةً بتقسيم الوظائف والفئات إلى وظائف جديدة. لهذا السبب يتعين علينا الحفاظ على تعقيدات الوحدة ومراقبتها: فهو يتدفق حرفيًا إلى وحدات من الفصول والوظائف.
لتحليل مدى تعقيد الوحدة ، يتعين علينا التحقق منها:
- عدد الواردات والأسماء المستوردة ؛ أقل هو أفضل
- عدد الطبقات والوظائف ؛ أقل هو أفضل
- متوسط تعقيد الوظائف والفئات داخل ؛ أقل هو أفضل
ماذا نفعل في حالة وجود وحدة معقدة؟
الحل : نعم ، لقد فهمت ذلك بشكل صحيح. نحن تقسيم وحدة واحدة إلى عدة منها.
حزم
الحزم تحتوي على وحدات متعددة. لحسن الحظ ، هذا كل ما يفعلونه.
لذلك ، يمكن أن يبدأ عدد الوحدات في حزمة قريبًا أن يكون كبيرًا جدًا ، لذلك سينتهي بك الأمر مع الكثير منها. وهذا هو التعقيد الوحيد الذي يمكن العثور عليه مع الحزم.
الحل : عليك تقسيم الحزم إلى حزم فرعية وحزم بمستويات مختلفة.
تأثير شلال التعقيد
لقد قمنا الآن بتغطية جميع أنواع التجريدات الممكنة تقريبًا في قاعدة بياناتك. ما الذي تعلمناه منه؟ الوجبات السريعة الرئيسية ، في الوقت الحالي ، هي أن معظم المشاكل يمكن حلها عن طريق إخراج التعقيد إلى مستوى التجريد نفسه أو العلوي.

هذا يقودنا إلى الفكرة الأكثر أهمية في هذه المقالة: لا تدع كودك مليء بالتعقيد. سأقدم عدة أمثلة لكيفية حدوث ذلك عادة.
تخيل أنك تنفذ ميزة جديدة. وهذا هو التغيير الوحيد الذي تقوم به:
يبدو حسنا ، أود تمرير هذا الرمز في المراجعة. ولن يحدث شيء سيء. لكن النقطة التي أفتقدها هي أن التعقيد فاض في هذا الخط! هذا ما wemake-python-styleguide
:

حسنًا ، علينا الآن حل هذا التعقيد. لنصنع متغيرًا جديدًا:
class Product(object): ... def can_be_purchased(self, user_id) -> bool: ... is_sub_paid = sub.is_due(tz.now() + delta) if user.is_active and user.has_sub() and is_sub_paid: ... ... ...
الآن ، يتم حل تعقيد الخط. لكن انتظر لحظة. ماذا لو كانت وظيفتنا بها الكثير من المتغيرات الآن؟ لأننا قمنا بإنشاء متغير جديد دون التحقق من عددهم داخل الوظيفة أولاً. في هذه الحالة ، سيتعين علينا تقسيم هذه الطريقة إلى عدة طرق مثل:
class Product(object): ... def can_be_purchased(self, user_id) -> bool: ... if self._has_paid_sub(user, sub, delta): ... ... def _has_paid_sub(self, user, sub, delta) -> bool: is_sub_paid = sub.is_due(tz.now() + delta) return user.is_active and user.has_sub() and is_sub_paid ...
الآن انتهينا! أليس كذلك؟ لا ، لأنه يتعين علينا الآن التحقق من تعقيد فئة Product
. تخيل أنه يحتوي الآن على العديد من الطرق لأننا أنشأنا _has_paid_sub
جديدة.
حسنا ، نحن ندير linter للتحقق من التعقيد مرة أخرى. وتبين أن فئة منتجاتنا معقدة للغاية في الوقت الحالي. أفعالنا؟ نحن تقسيمها إلى عدة فصول!
class Policy(object): ... class SubcsriptionPolicy(Policy): ... def can_be_purchased(self, user_id) -> bool: ... if self._has_paid_sub(user, sub, delta): ... ... def _has_paid_sub(self, user, sub, delta) -> bool: is_sub_paid = sub.is_due(tz.now() + delta) return user.is_active and user.has_sub() and is_sub_paid class Product(object): _purchasing_policy: Policy ... ...
من فضلك قل لي أنه هو التكرار الأخير! حسنًا ، أنا آسف ، لكن علينا الآن التحقق من تعقيد الوحدة. وتخمين ماذا؟ لدينا الآن الكثير من أعضاء الوحدة. لذلك ، علينا تقسيم الوحدات إلى وحدات منفصلة! ثم نتحقق من تعقيد الحزمة. وربما أيضا تقسيمها إلى عدة مجموعات فرعية.
هل رأيت ذلك؟ نظرًا لقواعد التعقيد المحددة جيدًا ، تحول تعديل سطر واحد الخاص بنا إلى جلسة إعادة هيكلة ضخمة تضم عدة وحدات وفئات جديدة. ولم نتخذ قرارًا واحدًا بأنفسنا: فكل أهداف إعادة التوطين كانت مدفوعة بالتعقيد الداخلي واللون الذي يكشفها.
هذا ما أسميه عملية "إعادة التسكين المستمر". أنت مجبر على القيام بإعادة بيع المساكن. دائما.
هذه العملية لديها أيضا نتيجة واحدة مثيرة للاهتمام. انها تسمح لك أن يكون "الهندسة المعمارية على الطلب". اسمحوا لي أن أشرح. مع فلسفة "Architecture on Demand" ، يمكنك دائمًا أن تبدأ صغيرة. على سبيل المثال مع ملف logic/domains/user.py
ملف واحد. وتبدأ في وضع كل ما يتعلق User
هناك. لأنه في هذه اللحظة من المحتمل أنك لا تعرف الشكل الذي ستبدو عليه العمارة. وأنت لا تهتم. لديك فقط مثل ثلاث وظائف.
بعض الناس يندرجون في مصيدة التعقيد في مقابل الهندسة المعمارية. يمكنهم زيادة تعقيد بنيتهم المعمارية من البداية مع طبقات مستودع / خدمة / مجال كاملة. أو يمكنهم التعقيد المفرط للكود المصدري بدون فصل واضح. كافح وعيش هكذا لسنوات (إذا كان بإمكانها العيش لسنوات باستخدام الكود هكذا!).
"العمارة عند الطلب" مفهوم يحل هذه المشاكل. تبدأ صغيرًا عندما يحين الوقت - تنقسم وتعيد قراءة الأشياء:
- عليك أن تبدأ
logic/domains/user.py
وتضع كل شيء هناك - بعد ذلك تقوم بإنشاء
logic/domains/user/repository.py
عندما يكون لديك ما يكفي من الأشياء المتعلقة بقاعدة البيانات - ثم تقوم بتقسيمه إلى
logic/domains/user/repository/queries.py
logic/domains/user/repository/commands.py
عندما يخبرك التعقيد بذلك - ثم تقوم بإنشاء
logic/domains/user/services.py
باستخدام الأشياء المتعلقة بـ http
- ثم تقوم بإنشاء وحدة نمطية جديدة تسمى
logic/domains/order.py
- وهلم جرا وهلم جرا
هذا كل شيء. إنها أداة مثالية لتحقيق التوازن بين الهندسة المعمارية الخاصة بك وتعقيد التعليمات البرمجية. احصل على قدر ما تحتاج إليه من الهندسة المعمارية في الوقت الحالي.
استنتاج
جيد linter يفعل أكثر بكثير من العثور على الفواصل المفقودة ونقلت سيئة. يتيح لك تطبيق Linter الجيد الاعتماد عليه من خلال قرارات الهندسة ومساعدتك في عملية إعادة البناء.
على سبيل المثال ، قد يساعدك wemake-python-styleguide
في تعقيد شفرة مصدر python
، على:
- محاربة التعقيد بنجاح على جميع المستويات
- فرض المقدار الهائل من معايير التسمية ، وأفضل الممارسات ، والتحقق من التناسق
- يمكنك دمجه بسهولة في قاعدة الشفرة القديمة بمساعدة خيار
diff
أو أداة flakehell
، لذلك سيتم flakehell
الانتهاك القديم ، لكن لن يتم السماح بمخالفات جديدة - قم بتمكينه في [CI] () ، حتى كعمل جيثب
لا تدع تعقيد لتجاوز التعليمات البرمجية الخاصة بك ، واستخدام جيد linter !