مقاييس جودة الكود

مقياس CRAP: التعقيد والتغطية في رقم واحد

CRAP اختصار لـ Change Risk Anti-Patterns. يضرب المقياس مدى تشابك الدالة في مدى افتقارها للاختبارات، والنتيجة تجيب على سؤال لا يجيب عنه أي مقياس منفرد: أي كود يكون تغييره خطرًا؟

أداة خشنة بشكل متعمد

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

الاسم يقوم بعمل متعمد. يُناقَش مقياس اسمه Change Risk Anti-Patterns مرة واحدة، أما مقياس يخبرك بأن هذه الدالة سيئة فيُصلَح.

الصيغة، ولماذا الأُسُس غير متساوية

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

CRAP(m) = CC² × (1 − cov)³ + CC
  • CC هو التعقيد الدوري للدالة، أي عدد المسارات المستقلة التي تمر بها
  • cov هو تغطية الدالة كنسبة من 0 إلى 1، فيكون (1 − cov) هو الجزء غير المُختبَر
  • رفع الجزء غير المُختبَر للأس ثلاثة يجعل الحد الأول ينهار سريعًا كلما ارتفعت التغطية، فيعود اختبار دالة قبيحة بمردود فوري
  • عند التغطية الكاملة يصبح الحد الأول صفرًا ويساوي CRAP قيمة CC، وهي المخاطرة المتبقية التي لا تستطيع الاختبارات إزالتها
  • وجود ‎+ CC‎ في النهاية هو ما يمنع المقياس من التظاهر بأن التعقيد بلا ثمن

ما تطلبه الأرقام فعلًا

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

CC 0% cov 50% cov 100% cov To clear 30
5 30.0 8.1 5 لا شيء. الكود البسيط يمرّ بلا اختبارات.
10 110.0 22.5 10 نحو 42٪
15 240.0 43.1 15 نحو 60٪
20 410.0 70.0 20 نحو 71٪
25 650.0 103.1 25 80٪ بالضبط
30 930.0 142.5 30 100٪، وتحلّ على الخط تمامًا
31+ 961.0 155.2 31 غير قابل للتحقق. الاختبارات لا تنقذ هذه.

ما يقوله لك المنحنى

بعد التعقيد 30 تتوقف التغطية عن المساعدة

بعتبة 30، تبقى دالة تعقيدها 31 فوق الخط حتى بتغطية 100٪. هذا ليس خللًا في الصيغة بل رسالتها: الخطوة الوحيدة المتبقية هي تقسيم الدالة.

عند التغطية الكاملة يصبح CRAP هو التعقيد نفسه

الاختبارات لا تغفر التعقيد أبدًا، بل تكتفي بوقف تضخيمه. فالدالة المعقدة المغطاة جيدًا لا تزال تحمل تعقيدها كمخاطرة معروفة ومُدارة لا مخاطرة مخفية.

الكود البسيط يُترك عن قصد

دالة بتعقيد 5 تقع عند 30 بالضبط بلا أي اختبار. المقياس يوجّه الانتباه إلى الكود المتشابك بدلًا من توليد عمل بلا قيمة على الـ getters والـ mappers.

التغطية هي الساق الضعيفة

يقيس cov التنفيذ لا التأكيد. ومجموعة اختبارات بلا تأكيدات ترفع التغطية وتخفض CRAP دون تغيير المخاطرة الحقيقية. وهذه الثغرة بالتحديد هي ما وُجد اختبار التطفير لسدّه.

العمل به

الصيغة سطران من الكود، وهذا معظم سبب إعادة تنفيذها مرارًا في بيئات جديدة.

المقياس نفسه

js

أي تقرير تغطية مع أي أداة تعقيد يعطي كل ما تحتاجه الصيغة.

// crap.js — coverage is a fraction, 0..1
export function crap(complexity, coverage) {
	const untested = 1 - coverage;
	return complexity ** 2 * untested ** 3 + complexity;
}

crap(15, 0);    // 240
crap(15, 0.5);  // 43.125
crap(15, 1);    // 15

ضع البوابة على التغيير لا على قاعدة الكود

sh

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

# CI: score only the methods this branch touched
git diff --name-only origin/main... -- '*.js' \
  | xargs node ./tools/crap-report.mjs --threshold 30 --changed-only

# exit non-zero when a touched method crosses the line;
# print the untouched offenders as a report, not a failure

إعادة الهيكلة التي تطلبها الدرجة

js

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

// Before: complexity climbs with every rule anyone adds.
function validate(order) {
	if (!order.id) return 'missing id';
	if (order.items.length === 0) return 'no items';
	if (order.total < 0) return 'negative total';
	if (order.currency !== 'USD' && order.currency !== 'EUR') return 'bad currency';
	if (order.customer && !order.customer.email) return 'customer without email';
	return null;
}

// After: each rule is trivially testable, the driver stays at 2.
const RULES = [
	[(o) => !o.id, 'missing id'],
	[(o) => o.items.length === 0, 'no items'],
	[(o) => o.total < 0, 'negative total'],
	[(o) => !['USD', 'EUR'].includes(o.currency), 'bad currency'],
	[(o) => Boolean(o.customer) && !o.customer.email, 'customer without email']
];

function validate(order) {
	for (const [fails, message] of RULES) if (fails(order)) return message;
	return null;
}

كيف تستخدمه دون إغضاب الجميع

اقرأ الرافعتين على حدة

  • - الدرجة المرتفعة بدافع التعقيد مهمة إعادة هيكلة لا مهمة اختبار
  • - الدرجة المرتفعة بدافع نقص التغطية مهمة اختبار، ورخيصة
  • - اعرض دائمًا CC والتغطية بجانب الدرجة، لأن الرقم وحده لا يقول أيّهما السبب

رجّح بحسب معدل التغيير

  • - مخاطرة التغيير لا تهم إلا حيث يحدث التغيير
  • - درجة 200 في ملف لم يُلمس منذ أربع سنوات أقل إلحاحًا من 60 في ملف يُعدَّل كل أسبوع
  • - رتّب بحسب الدرجة مضروبة في تواتر الكوميتات، واعمل من أعلى القائمة

اجعل الاستثناءات صريحة

  • - الكود المُولَّد والمحوّلات وعبارات switch الشاملة تنفخ التعقيد دون أن تنفخ المخاطرة الحقيقية
  • - استثنِها في الإعدادات مع تعليق يوضح السبب، بدلًا من رفع العتبة بصمت
  • - أعد النظر في قائمة الاستثناءات عندما يتوقف توليد الكود الذي تحميه

قراءات خاطئة شائعة

التعامل معه كدرجة جودة

يقدّر CRAP مخاطرة تغيير الكود. ولا يقول شيئًا عن صحته أو حسن تسميته أو جودة تصميمه. فالدالة النظيفة المغطاة جيدًا ذات التعقيد المجالي الحقيقي تحصل على الدرجة نفسها التي تحصل عليها دالة مزعجة.

التلاعب به بالتغطية

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

إطلاق حملة تنظيف كبرى

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

توقّع أداة مصانة

أداة Crap4j الأصلية خامدة منذ سنوات. يحمل NDepend المقياس على .NET، وهناك تنفيذات مجتمعية لـ Rust و.NET وGroovy، لكن في معظم البيئات تحسبه بنفسك من بيانات تجمعها أصلًا.

رقم واحد وسؤال واحد

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

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

الأسئلة الشائعة

ما الذي يُعدّ درجة CRAP سيئة؟

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

لماذا التعقيد للأس اثنين والجزء غير المُختبَر للأس ثلاثة؟

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

هل ما زال مقياس CRAP وجيهًا؟

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

هل تعني درجة CRAP منخفضة أن تغيير الكود آمن؟

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

هل ينبغي أن يُفشل CRAP عملية البناء؟

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

© 2026 - Ryware.