لكل دالة تكتبها درجة تعقيد. إذا لم تحتوي الدالة على أي نقاط قرار، فلن يكون هناك أي تعقيد. if، لا elseلا حلقة، لا switch، تعقيدها الحلقي هو 1: مسار واحد فقط عبر الكود. أضف if يصبح البيان 2. أضف بيانًا آخر ويصبح 3. بحلول الوقت الذي تتراكم فيه دالة ما باثني عشر فرعًا شرطيًا عبر عدة مستويات متداخلة، قد يكون تعقيدها الحلقي 15 أو 20، ويتطلب اختبارها بالكامل عددًا مماثلًا من حالات الاختبار، تغطي كل منها مسار تنفيذ مميز.
طُوِّرَ مفهوم التعقيد الحلقي (CC) على يد توماس ج. مكابي عام ١٩٧٦ كمقياس كمي للتعقيد المنطقي لتدفق التحكم في البرنامج. ولا يزال هذا المفهوم أحد أكثر مقاييس جودة البرمجيات فائدةً من الناحية العملية، نظرًا لتأثيراته الملموسة والقابلة للتنفيذ: إذ يُشير مُعامل التعقيد إلى الحد الأدنى من حالات الاختبار اللازمة لتغطية المسار بالكامل، ويتنبأ بمدى صعوبة فهم الكود وتعديله، ويُحدد الدوال الأكثر عرضةً لاحتواء عيوب غير مكتشفة. يُغطي هذا الدليل الصيغة، والعتبات، وأمثلة خاصة بكل لغة برمجة، وتقنيات إعادة هيكلة الكود التي تُقلل التعقيد فعليًا، وكيفية قياسه وتتبعه تلقائيًا.
ما هو التعقيد الحلقي؟
يقيس التعقيد الحلقي عدد المسارات المستقلة خطيًا عبر شفرة المصدر للبرنامج. وقد اشتقّه توماس ج. مكابي من نظرية الرسوم البيانية: يمكن تمثيل أي برنامج كرسم بياني لتدفق التحكم، حيث تمثل العقد العبارات، وتمثل الحواف التدفقات الممكنة بينها. الصيغة هي:
CC = E - N + 2P
أين:
- E = عدد الحواف في مخطط تدفق التحكم
- N = عدد العقد
- P = عدد المكونات المتصلة (عادةً 1 لوظيفة واحدة)
لإجراء الحسابات العملية، يوجد مكافئ أبسط: CC = عدد نقاط القرار + 1. كل if, else if, while, for, case, catch, &&و || يُضيف نقطة قرار واحدة. قيمة CC الابتدائية لأي دالة هي 1.
جافا
// CC = 1: no decision points
public String greet(String name) {
return "Hello, " + name;
}
// CC = 3: two decision points (two if statements)
public String classify(int score) {
if (score >= 90) return "Excellent";
if (score >= 70) return "Satisfactory";
return "Needs improvement";
}
// CC = 5: four decision points (three conditions + one loop)
public double calculateTotal(List<Item> items, boolean isMember, boolean isHoliday) {
double total = 0;
for (Item item : items) { // +1
total += item.getPrice();
}
if (isMember) total *= 0.9; // +1
if (isHoliday) total *= 0.95; // +1
if (total > 100) total -= 5; // +1
return total;
}
المعايير: ما هي الدرجة المقبولة؟
تحدد إرشادات مكابي الأصلية، والتي لا تزال الأكثر استشهاداً بها على نطاق واسع، أربعة مستويات للمخاطر:
| درجة CC | مستوى الخطر | التفسير |
|---|---|---|
| ٢٠٢٤/٢٠٢٣ | منخفض | بسيط، منظم جيدًا، سهل الاختبار |
| ٢٠٢٤/٢٠٢٣ | معتدل | أكثر تعقيدًا؛ يتطلب جهدًا أكبر في الاختبار |
| ٢٠٢٤/٢٠٢٣ | مرتفع | معقد ويصعب اختباره؛ يوصى بإعادة هيكلته |
| > 50 | عالي جدا | غير قابل للاختبار عملياً؛ مخاطر جسيمة على الجودة |
يُعدّ الحدّ الأقصى البالغ 10 هو الحدّ الأكثر شيوعًا في بوابات ضمان الجودة في التكامل المستمر/التسليم المستمر. ويبلغ الحدّ الأقصى الافتراضي للتعقيد المعرفي في SonarQube 15 (وهو مقياس ذو صلة ولكنه مختلف). وتوصي إرشادات المعهد الوطني للمعايير والتكنولوجيا (NIST) للأنظمة بالغة الأهمية للسلامة بحدّ أقصى قدره 10 لكل وحدة.
ملاحظة هامة: يقيس مؤشر CC التعقيد البنيوي، وليس التعقيد الدلالي. قد يكون فهم دالة ذات مؤشر CC يساوي 8 تُنفذ عملية حسابية مالية معقدة أصعب من فهم دالة ذات مؤشر CC يساوي 15 تتكون من فحوصات دفاعية بسيطة. استخدم مؤشر CC كإشارة للتحقيق، وليس كحكم نهائي.
التعقيد الحلقي في لغات مختلفة
Python
بنى اتخاذ القرار في بايثون التي تساهم في CC: if, elif, else (غير محسوب، ليس له شرط)، for, while, try/except (كل except (يحسب) with (لا يُحتسب)، والمعاملات المنطقية and/or في الظروف.
الثعبان
# CC = 1
def format_name(first: str, last: str) -> str:
return f"{first} {last}"
# CC = 4: three decision points
def calculate_discount(price: float, is_member: bool, is_holiday: bool) -> float:
discount = 0.0
if is_member: # +1
discount += 0.10
if is_holiday: # +1
discount += 0.05
if price > 100: # +1
discount += 0.02
return price * (1 - discount)
# CC = 6: five decision points (list comprehension counts as a loop)
def process_orders(orders: list[dict]) -> list[dict]:
return [
{**order, "total": order["qty"] * order["price"]} # +1 (comprehension)
for order in orders
if order["qty"] > 0 # +1 (filter condition)
if order["price"] > 0 # +1 (second filter)
]
أدوات قياس CC في بايثون: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint باستخدام إضافة التعقيد، تحليل سونار كيوب بايثون.
جافا
جافا
// CC = 7: complex authentication with multiple conditions
public AuthResult authenticate(String userId, String password, boolean isMfa) {
if (userId == null || password == null) return AuthResult.INVALID; // +2 (||)
User user = userRepository.findById(userId);
if (user == null) return AuthResult.NOT_FOUND; // +1
if (!user.checkPassword(password)) return AuthResult.WRONG_PASSWORD;// +1
if (isMfa && !user.hasMfaEnabled()) return AuthResult.MFA_REQUIRED; // +2 (&&)
return AuthResult.SUCCESS;
}
// CC = 1 + 2 + 1 + 1 + 2 = 7
تقليله:
جافا
// After refactoring: CC = 3 (main method) + small helpers with CC = 2 each
public AuthResult authenticate(String userId, String password, boolean isMfa) {
if (hasInvalidInputs(userId, password)) return AuthResult.INVALID;
User user = findVerifiedUser(userId, password);
if (user == null) return AuthResult.WRONG_PASSWORD;
if (requiresMfa(user, isMfa)) return AuthResult.MFA_REQUIRED;
return AuthResult.SUCCESS;
}
private boolean hasInvalidInputs(String userId, String password) {
return userId == null || password == null; // CC = 2
}
private boolean requiresMfa(User user, boolean isMfa) {
return isMfa && !user.hasMfaEnabled(); // CC = 2
}
أدوات جافا سي سي: Checkstyle، PMD، SonarQube، IntelliJ IDEA مدمج، SMART TS XL.
لغة سي شارب وتايب سكريبت
تتبع لغتا C# و TypeScript نفس قواعد Java. والإضافة الرئيسية هي أن عبارات LINQ في C# وسلاسل الشروط الثلاثية في TypeScript تضيف نقاط قرار.
CSHARP
// CC = 5: switch with four cases
public decimal GetShippingCost(string zone) => zone switch {
"domestic" => 5.99m, // +1
"eu" => 15.99m, // +1
"international" => 29.99m, // +1
"express" => 49.99m, // +1
_ => throw new ArgumentException($"Unknown zone: {zone}")
};
COBOL
بنى اتخاذ القرار في لغة كوبول التي تساهم في CC: IF/ELSE, EVALUATE WHEN (كل جملة من جمل "عندما")، PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.
كوبول
CALCULATE-DISCOUNT.
IF WS-CUSTOMER-TYPE = 'GOLD' *> +1
IF WS-PURCHASE-AMT > 1000 *> +1
COMPUTE WS-DISCOUNT = 0.20
ELSE *> (no increment)
COMPUTE WS-DISCOUNT = 0.15
ELSE IF WS-CUSTOMER-TYPE = 'SILVER' *> +1
COMPUTE WS-DISCOUNT = 0.10
ELSE *> (no increment)
COMPUTE WS-DISCOUNT = 0.05
END-IF
EVALUATE TRUE
WHEN WS-REGION = 'NORTH' PERFORM APPLY-REGIONAL-RATE *> +1
WHEN WS-REGION = 'SOUTH' PERFORM APPLY-SOUTHERN-RATE *> +1
END-EVALUATE.
*> Total CC = 1 + 5 = 6
بسبب بنية لغة كوبول المطولة، عادةً ما تكون الفقرات أطول من الدوال المكافئة لها في اللغات الحديثة. وتُعد برامج كوبول التي تحتوي على أكثر من 50 حرفًا لكل فقرة شائعة في قواعد البيانات القديمة، وتمثل أهدافًا ذات أولوية قصوى في كلٍ من إعادة هيكلة البرامج وتحديثها.
كيفية حساب التعقيد الحلقي: ثلاث طرق
الطريقة الأولى: حساب نقاط القرار + 1 أسرع طريقة يدوية. احسب كل if, else if, while, for, case, catch, &&, || في الدالة. أضف 1 للدالة نفسها.
الطريقة الثانية: مخطط تدفق التحكم ارسم الدالة على شكل رسم بياني: عقدة واحدة لكل عبارة أو كتلة، وحواف لكل مسار تحكم. تطبيق CC = E - N + 2.
الطريقة الثالثة: أداة مؤتمتة. هذه هي الطريقة العملية الوحيدة لأي شيء يتجاوز الوظائف البسيطة. تقوم معظم أدوات التحليل الثابت بحساب CC تلقائيًا ودمجه في مسارات CI/CD.
تقنيات إعادة هيكلة البرامج التي تقلل التعقيد فعلياً
بنود الحماية (الإرجاع المبكر)
تؤدي عبارات الحماية إلى إنهاء الدالة مبكراً عند فشل الشروط المسبقة، مما يلغي فروع else ويقلل من عمق التداخل.
الثعبان
# Before: deeply nested, CC = 5
def process_order(order):
if order is not None:
if order.is_valid():
if order.has_stock():
if order.payment_cleared():
return fulfill_order(order)
else:
return "Payment failed"
else:
return "Out of stock"
else:
return "Invalid order"
else:
return "No order"
# After: flat, CC = 5 (same complexity, dramatically better readability)
def process_order(order):
if order is None: return "No order"
if not order.is_valid(): return "Invalid order"
if not order.has_stock(): return "Out of stock"
if not order.payment_cleared(): return "Payment failed"
return fulfill_order(order)
لا ينخفض معدل التكلفة، فتبقى نقاط اتخاذ القرار نفسها، لكن يصبح الكود أسهل بكثير في القراءة والاختبار. يتطلب التخفيض الحقيقي لمعدل التكلفة إزالة نقاط اتخاذ القرار، وليس مجرد إعادة ترتيبها.
طرق الاستخلاص
يؤدي نقل المجموعات المنطقية من القرارات إلى طرق مسماة إلى تقليل التعقيد الزمني للدالة المستدعاة مع توزيع التعقيد عبر وحدات أصغر قابلة للاختبار.
جافا
// Before: one method doing everything, CC = 9
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
double subtotal = 0;
for (LineItem item : invoice.getItems()) {
subtotal += item.getQuantity() * item.getUnitPrice();
if (item.isTaxable()) subtotal += item.getPrice() * 0.1;
}
if (customer.isMember()) subtotal *= 0.9;
if (customer.hasVoucher()) subtotal -= customer.getVoucherValue();
if (subtotal < 0) subtotal = 0;
return subtotal;
}
// After: main method CC = 4, helpers have CC = 2-3 each
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
double subtotal = computeLineItemTotal(invoice.getItems());
subtotal = applyCustomerDiscounts(subtotal, customer);
return Math.max(0, subtotal);
}
استبدال العبارات الشرطية بتعدد الأشكال
عندما تتفرع دالة بناءً على النوع أو الحالة، فإن تعدد الأشكال يلغي التفرع تمامًا.
جافا
// Before: switch on payment type, CC grows with each new type
public void processPayment(String type, double amount) {
switch (type) {
case "CREDIT": processCreditCard(amount); break;
case "PAYPAL": processPayPal(amount); break;
case "CRYPTO": processCrypto(amount); break;
default: throw new IllegalArgumentException("Unknown type: " + type);
}
}
// After: new payment types require no changes to this method, CC = 1
public interface PaymentProcessor {
void process(double amount);
}
public void processPayment(PaymentProcessor processor, double amount) {
processor.process(amount); // no branching
}
تحليل الجمل الشرطية المعقدة
استخرج التعبيرات المنطقية المعقدة إلى طرق مسماة تكشف عن الغرض منها.
الثعبان
# Before: dense boolean logic, hard to understand, easy to mis-test
if user.age >= 18 and user.country in ALLOWED_COUNTRIES and not user.is_banned and user.verified:
grant_access()
# After: named predicate, self-documenting, unit-testable independently
def is_eligible_for_access(user: User) -> bool:
return (
user.age >= 18
and user.country in ALLOWED_COUNTRIES
and not user.is_banned
and user.verified
)
if is_eligible_for_access(user):
grant_access()
التعقيد الحلقي في خطوط أنابيب التكامل المستمر/التسليم المستمر
يمنع تطبيق التحكم التلقائي في التعقيد في مسارات التكامل المستمر/التسليم المستمر تراكم التعقيد بشكل غير مرئي بين مراجعات التعليمات البرمجية.
يامل
# GitHub Actions: fail PR if any function exceeds CC threshold
name: Code Quality
on: [pull_request]
jobs:
complexity-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install radon (Python CC tool)
run: pip install radon
- name: Check cyclomatic complexity
run: |
radon cc src/ --min C --show-complexity
# Fails if any function has CC grade C (11-15) or worse
radon cc src/ --min C --total-average | grep -q "Average complexity" \
&& echo "Complexity check passed" \
|| (echo "Functions with high complexity found" && exit 1)
بالنسبة للغة جافا مع سونار كيوب:
يامل
# SonarQube quality gate blocks merge if CC exceeds threshold
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: |
-Dsonar.qualitygate.wait=true
-Dsonar.java.complexity.Function.threshold=10
ينبغي أن تمنع بوابة الجودة التعليمات البرمجية الجديدة عالية التعقيد، وليس قاعدة التعليمات البرمجية القديمة بأكملها، والتي قد تحتوي بالفعل على نسبة تعقيد عالية يتم معالجتها بشكل منفصل.
التعقيد الحلقي في لغة كوبول وقواعد البيانات القديمة
بالنسبة لأنظمة المؤسسات التي قد تحتوي برامج COBOL فيها على فقرات ذات تعقيد حلقي يتجاوز 50 أو حتى 100، فإن تحليل التعقيد الحلقي يخدم غرضًا أساسيًا مختلفًا عما يخدمه في قواعد البيانات الحديثة. فالسؤال ليس "هل يجب إعادة هيكلة هذه الوظيفة؟"، بل "ما هي مخاطر ترحيل هذا البرنامج، وبأي ترتيب يجب تحديثه؟"
يحتوي برنامج COBOL ذو قيمة CC تساوي 80 في فقرته الرئيسية على 80 مسار تنفيذ مستقل، يتطلب كل منها حالة اختبار للتحقق من صحته. إذا كان البرنامج يفتقر إلى حالات اختبار، كما هو الحال في معظم برامج COBOL القديمة، فإن قيمة CC هي المؤشر الرئيسي لعدد سيناريوهات التحقق التي يجب إنشاؤها قبل اعتبار أي عملية تحويل آمنة.
SMART TS XLالصورة تحليل الكود الثابت يحسب هذا البرنامج التعقيد الحلقي للغات COBOL وJCL وRPG وPL/I وجميع اللغات الحديثة في آنٍ واحد، مُنتجًا توزيع التعقيد الحلقي على مستوى المحفظة، مما يجعل قرارات تسلسل التحديث قائمة على الأدلة. تُعد البرامج ذات أعلى درجات التعقيد الحلقي وأكبر عدد من البرامج المُستدعِية (البرامج ذات التداخل العالي) أهدافًا مُعرَّضة لخطر الترحيل، كما هو موضح في سياق وضع مقاييس مؤشر قابلية الصيانة لتطبيقات COBOL، CC هو أحد مكونات صورة الجودة الأوسع التي تشمل أيضًا حجم هالستيد وعدد أسطر التعليمات البرمجية.
تستخدم قدرة تحليل التأثير تصنيف التعقيد القائم على CC لتحديد نطاق ما يجب التحقق منه عند حدوث أي تغييرات في البرنامج عالي التعقيد: ارتفاع CC يعني المزيد من مسارات التنفيذ، مما يعني المزيد من سيناريوهات الاختبار التي يجب التحقق منها لتأكيد التكافؤ السلوكي قبل وبعد أي تعديل.
بالنسبة للفرق التي تخطط لبرامج تحديث الأنظمة القديمة ، فإن توزيع CC عبر المحفظة هو المدخل لتسلسل موجة الترحيل: البرامج ذات CC المنخفض والتي تحتوي على عدد قليل من المستخدمين يتم ترحيلها مبكرًا؛ البرامج ذات CC العالي والتي تحتوي على العديد من المستخدمين يتم ترحيلها أخيرًا، بعد أن يكتسب الفريق خبرة في المكونات الأبسط وتكون بنية الاختبار جاهزة للتحقق من صحة المكونات المعقدة.
التعقيد ليس هو العدو، بل التعقيد الخفي هو العدو
يُعدّ التعقيد الحلقي أحد مقاييس جودة الكود القليلة التي ترتبط ارتباطًا مباشرًا بقابلية الاختبار، إذ إن عدد حالات الاختبار اللازمة لتغطية المسار بالكامل يساوي، بحكم التعريف، على الأقل قيمة التعقيد الحلقي. هذه العلاقة تجعله قابلاً للتطبيق العملي بطريقة لا تتوفر في العديد من مقاييس الجودة الأخرى.
يتطلب الانضباط في إدارة العقود التعاونية فهم أن التعقيد يتراكم تدريجياً. كل if تُعتبر إضافة عبارة إلى دالة متنامية أمرًا منطقيًا في حد ذاته. قد ينتج عن ثلاث سنوات من القرارات المنطقية بشكل فردي دالة ذات CC = 40 لا يرغب أحد في التعامل معها لأنها معقدة للغاية بحيث يصعب فهمها بشكل آمن. الأدوات والتقنيات الواردة في هذا الدليل، مثل بنود الحماية، واستخراج الأساليب، وتعدد الأشكال، والتفكيك الشرطي، وبوابات جودة التكامل المستمر/التسليم المستمر، موجودة لمنع هذا التراكم من الحدوث بشكل غير مرئي، ولمعالجته بشكل منهجي عند حدوثه بالفعل.