السحر في البرمجة ليس استعارة، بل مصطلح دقيق يشير إلى القيم (أرقام، نصوص، أعلام) التي لا يكون معناها واضحاً من السياق وتتطلب معرفة خارجية لفهمها. أكثر أنواع السحر شيوعاً هو الأرقام السحرية: ثوابت رقمية تُكتب مباشرة في الكود دون شرح لسبب اختيار تلك القيمة بالذات. وفقاً لتقرير SonarSource لجودة الكود (2025)، حوالي 8 بالمائة من جميع تحذيرات المحللات الثابتة مرتبطة بالقيم الحرفية غير المفسرة. القيم السحرية تجعل الكود هشاً: تغييرها يتطلب البحث عن جميع المواضع، والمطور الجديد لا يعرف ما إذا كان يمكن تعديل الرقم أم أنه حرج لعمل النظام.
الخلاصة
السحر هو أي قيمة في الكود المصدري لا يكون معناها واضحاً بدون معرفة إضافية بالمجال. المصطلح راسخ في المجتمع: إذا نظر المطور إلى رقم ولم يستطع معرفة من أين جاء — فهذا سحر.
يأتي السحر بعدة أنواع: رقمي (أرقام سحرية)، نصي (نصوص سحرية)، منطقي (أعلام سحرية)، وتكويني (معلمات مبرمجة بشكل ثابت يجب أن تكون في الإعدادات). تشترك الأنواع الأربعة في مشكلة واحدة: عندما يتغير مطلب، يجب على المطور العثور على كل مكان تُستخدم فيه القيمة واستبدالها يدوياً. يؤدي تفويت موضع واحد إلى خطأ.
وفقاً لاستطلاع JetBrains لجودة الكود (2025)، يعتبر 73 بالمائة من المطورين الأرقام السحرية مؤشراً على انخفاض جودة الكود، بينما يعترف 41 بالمائة أنهم يتركونها أحياناً. السبب الرئيسي هو التسرع: «سأضيف الثابت لاحقاً» — لكن هذا لاحقاً لا يأتي أبداً، وبعد شهر يبقى الرقم 0.85 في منتصف دالة بدون شرح.
القاعدة الأساسية: كل قيمة حرفية باستثناء 0، 1، true، false والسلسلة الفارغة يجب استخراجها إلى ثابت مسمى. الاستثناءات: زيادة العداد (i + 1)، الأصفار الرياضية (التحقق من 0)، والقيم الأولية للمراكمات. كل شيء آخر مرشح للتسمية.
الرقم السحري هو قيمة حرفية رقمية لا يكون قيمتها واضحة من السياق. مثال كلاسيكي: 86400 في كود متعلق بوقت الانتظار. يرى المطور الرقم ويجب أن يخمن أنه عدد الثواني في اليوم. إذا أخطأ وكتب 84600، سيكون من الصعب اكتشاف الخطأ لأن المهلة ستنطلق قبل 18 دقيقة.
لماذا الأرقام السحرية خطيرة: أولاً، تضر بسهولة القراءة. الرقم 1024 قد يعني حجم كيلوبايت، أو حد ترحيل الصفحات، أو الحد الأقصى لعدد العناصر. بدون سياق — إنه مجرد رقم. ثانياً، تخلق تكراراً: إذا استُخدم 1024 في خمسة أماكن، فعندما يتغير الحد إلى 2048، يجب على المطور العثور على الخمسة واستبدالهم. إذا فات مكان واحد، يعمل النظام بشكل غير صحيح ولكن دون خطأ صريح.
// before - magic in its pure form
fun calculateTimeout(base: Int): Int {
return base * 3 + 5000
}
// after - values replaced with constants
private const val RETRY_MULTIPLIER = 3
private const val BASE_TIMEOUT_MS = 5000
fun calculateTimeout(base: Int): Int {
return base * RETRY_MULTIPLIER + BASE_TIMEOUT_MS
}
الخطر الثالث هو عدم القدرة على الاختبار. إذا كانت القيمة الحدية مبرمجة بشكل ثابت كقيمة حرفية، لا يمكن للاختبار تجاوزها للتحقق من الشروط الحدية. الثابت المستخرج إلى companion object أو ملف تكوين يجعل الكود قابلاً للاختبار: يستبدل الاختبار قيمة مختلفة ويتحقق من سلوك النظام عند الحد.
طور عادة: كلما كتبت رقماً غير 0، 1، 100 أو 2 — توقف وفكر فيما إذا كان يجب استخراجه إلى ثابت. إذا كان الرقم مرتبطاً بمنطق الأعمال (حد، مهلة، وقت انتظار، حجم) — استخرجه دون تردد. إذا كان الرقم ثابتاً رياضياً (pi، e) — استخدم المكتبة القياسية (Math.PI، Math.E).
النصوص السحرية هي قيم حرفية نصية مدمجة في الكود دون استخراجها إلى ثوابت أو موارد. أمثلة نموذجية: عناوين URL للنقاط الطرفية، أسماء مفاتيح SharedPreferences، إجراءات Intent، مفاتيح bundle، أسماء الملفات واستعلامات SQL.
خطر النصوص السحرية هو عدم وجود فحص في وقت الترجمة. لن يتم اكتشاف خطأ كتابي في النص «user_prefs» حتى وقت التشغيل. إذا استُخدم النص في عشرة أماكن وكتب المطور «user_pref» (بدون s) في أحدها — التطبيق لا يتعطل، لكن البيانات لا تُحفظ. يمكن لمثل هذا الخطأ أن يعيش في الإنتاج لأشهر لأنه لا يسبب تعطلاً.
لمشاريع Android، يجب استخراج النصوص السحرية إلى موارد (strings.xml، arrays.xml) أو ثوابت في companion object. لنظام iOS — إلى موارد نصية (Localizable.strings) أو ثوابت enum. للخلفية — إلى ملفات تكوين (.env، application.properties). لا يجب أن يظهر أي مفتاح أو URL أو مسار في الكود كقيمة حرفية نصية.
// before - magic strings across the class
let prefs = UserDefaults.standard
prefs.set(token, forKey: "auth_token")
prefs.set(userId, forKey: "current_user_id")
// after - strings extracted to enum
enum PrefKeys: String {
case authToken = "auth_token"
case currentUserId = "current_user_id"
}
prefs.set(token, forKey: PrefKeys.authToken.rawValue)
prefs.set(userId, forKey: PrefKeys.currentUserId.rawValue)
انتبه بشكل خاص للنصوص المكررة. إذا ظهر المفتاح نفسه «user_settings» في ثلاثة ملفات — في 99 بالمائة من الحالات سيظهر خطأ كتابي في أحدها في النهاية. استخراجها إلى enum أو ثابت يضمن أن جميع المراجع تستخدم نفس القيمة.
الأعلام السحرية هي معاملات منطقية لا يكون معناها واضحاً من سياق الاستدعاء. نمط معاكس كلاسيكي: تمرير true أو false إلى دالة دون شرح ما يفعله هذا العلم بالضبط.
مثال: userDao.fetch(includeDeleted = false). يرى المطور false ولا يعرف ما إذا كان يعني «لا تضمّن المحذوف» أو «لا تضمّن النشط». بعد شهر، يتحول false إلى true، وتبدأ السجلات المحذوفة في الظهور في المخرجات. يكتشف الخطأ فقط في الإنتاج.
الحل هو استبدال الأعلام المنطقية بـ enum أو فئة مختومة. بدلاً من معامل Boolean، استخدم UserFilter.includeDeleted أو UserFilter.activeOnly. بهذه الطريقة يوثق الكود نيته، ويقترح IDE الخيارات المتاحة أثناء الإكمال التلقائي.
إذا تم تمرير علم منطقي عبر عدة طبقات — فهذه إشارة أخرى على أن التجريد خاطئ. بدلاً من سحب علم عبر ثلاثة مستويات من الاستدعاءات، فكر فيما إذا كان يجب اتخاذ اختيار التصفية على المستوى الأعلى وتمريره كتكوين جاهز. كلما قلّت الأعلام المنطقية في الكود — قلّ السحر.
اعتمد قاعدة: لا يتم تمرير أي معامل منطقي إلى دالة بدون وسيط مسمى (إذا كان اللغة تدعم الوسائط المسماة). في Kotlin وSwift، هذا المطلب تلقائي. في Java، استخدم Builder أو ثوابت enum بدلاً من true/false.
يتم البحث عن القيم السحرية آلياً بواسطة محللات ثابتة مكونة لكشف القيم الحرفية في أماكن غير متوقعة. كل لغة تقدم أدواتها الخاصة مع استثناءات قابلة للتخصيص.
| الأداة | اللغات | القاعدة |
|---|---|---|
| SonarQube | Java، Kotlin، Swift، Python، JS | MagicNumber، HardcodedString |
| ESLint | JavaScript، TypeScript | no-magic-numbers، no-hardcoded-strings |
| Detekt | Kotlin | MagicNumber، ComplexCondition |
| SwiftLint | Swift | magic_number (opt-in) |
| PMD | Java، Apex، PLSQL | MagicNumber (قائمة مسموحة قابلة للتخصيص) |
| فحص PhpStorm | PHP | NumericLiteralWithContext (فحص مدمج) |
تكوين الاستثناءات أمر بالغ الأهمية — بدونه، سيضع المحلل علامة على كل زيادة (-1، +1) وكل صفر رياضي. لـ SonarQube، قائمة الأرقام المسموحة: 0، 1، -1، 2 (للمضاعفة)، 100 (النسب المئوية)، 60 و 24 (الوقت). لجميع القيم الأخرى — طالب بثابت مسمى مع معدّل public static final (Java) أو const val (Kotlin).
للتحليل على مستوى CI، أضف خطوة تتحقق من السحر كتحذير ولكن لا تمنع البناء. سيعرض التشغيل الأول مئات التحذيرات في الكود القديم. تدريجياً، تذكرة تلو الأخرى، قم بترحيل الكود إلى الثوابت وارفع عتبة الجودة. عندما يصبح عدد الأرقام السحرية أقل من 10 — فعّل القاعدة كخطأ في البناء.
إعادة هيكلة السحر هي واحدة من أكثر العمليات أماناً: استبدال قيمة حرفية بثابت لا يغير سلوك الكود. ومع ذلك، يجب أن يكون النهج منهجياً لعدم تفويت التبعيات المخفية (على سبيل المثال، إذا استُخدم نفس الرقم السحري في سياقات غير مرتبطة لكنه يتطابق بالصدفة في القيمة).
عملية خطوة بخطوة: ابحث عن جميع مواضع القيمة السحرية، افهم سياق كل منها، قسمها إلى ثوابت مختلفة (حتى لو تطابقت القيم — السياقات مختلفة، ويجب تسمية الثوابت بشكل مختلف)، استبدل القيم الحرفية بالثوابت، تحقق من خلال الاختبارات. الخطأ في الخطوة 2 هو الأكثر شيوعاً: مفهومان مختلفان (مهلة بالميلي ثانية وحد بالبايت) قد يتطابقان عددياً (على سبيل المثال، 5000)، لكنهما من الناحية الدلالية كميتان مختلفتان ولا يمكن جمعهما في ثابت واحد.
// before - same number in different contexts
public class Config {
public void setupCache() {
cache.setMaxSize(5000); // 5 MB
}
public void setupTimeout() {
client.setReadTimeout(5000); // 5 seconds
}
}
// after - different constants for different contexts
public class Config {
private static final int CACHE_MAX_SIZE_MB = 5;
private static final int READ_TIMEOUT_SECONDS = 5;
public void setupCache() {
cache.setMaxSize(CACHE_MAX_SIZE_MB * 1024 * 1024);
}
public void setupTimeout() {
client.setReadTimeout(
READ_TIMEOUT_SECONDS * 1000
);
}
}
للكود الجديد، القاعدة بسيطة: أي قيمة حرفية باستثناء 0، 1، -1، true، false، null والسلسلة الفارغة تُستخرج إلى ثابت. الاستثناءات: الثوابت الرياضية (استخدم دائماً المكتبة القياسية)، بيانات الاختبار (يمكن ترك القيم الحرفية في الاختبارات ولكن مع اسم متغير وصفي)، والقيم الحدية للزيادة (i + 1 في حلقة أمر مقبول).
الأسئلة الشائعة
نعم، 100 أيضاً رقم سحري إذا استُخدم بدون سياق. بدلاً من 100، اكتب MAX_PERCENT أو PROBABILITY_SCALE. استثناء: عندما يكون 100 نسبة مئوية واضحة في السياق (على سبيل المثال، في معادلة حساب النسبة المئوية)، ولكن حتى في هذه الحالة يحسن الثابت سهولة القراءة.
في الاختبارات، من الأفضل أيضاً استخدام متغيرات مسماة. بدلاً من assertEquals(42, result)، اكتب val expected = 42; assertEquals(expected, result). استثناء: اختبارات القيم الحدية (0، null، سلسلة فارغة) — يمكن تركها كقيم حرفية لأنها مقروءة في سياق الاختبار.
نعم، الأرقام المتعلقة بواجهة المستخدم (الأحجام، الهوامش، مدة الرسوم المتحركة) يجب أن تكون في الموارد (dimens.xml، integers.xml). ثوابت الأعمال (المهل، الحدود) — في companion object أو ملف تكوين. المعيار الرئيسي: إذا كان الرقم يمكن أن يتغير دون تغيير المنطق — فهو مورد.
شغّل SonarQube بقاعدة MagicNumber أو ESLint بـ no-magic-numbers. احصل على التقرير، رتب حسب تكرار الاستخدام وابدأ بالأرقام التي تظهر في ثلاثة أماكن أو أكثر. هي الأكثر احتمالاً لاستخراجها إلى ثوابت.
لا. القيم الحرفية المقبولة: 0، 1، -1 (زيادة/نقصان، فحص فارغ)، true، false، null، سلسلة فارغة. كل الباقي يتطلب تسمية. إذا استُخدم الرقم 0 ليس كفحص فارغ (على سبيل المثال، 0 هو معرف الفئة الجذرية)، فيجب أن يكون 0 أيضاً ثابتاً: ROOT_CATEGORY_ID = 0.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.