توجيهات في تصميم الشفرة المصدرية


استخدام المتغيرات الموصولة (references)

يستخدم هذا النوع من المؤشرات فقط في الحالات التالية:
  • تعديل المؤثرات (operator overloading).
  • استخدام المعطيات لحمل نتيجة الدالة.
  • تحسين الأداء عن طريق استبدال القيمة بمؤشر لها في معطيات الدالات ونتائجها.
  • ترغب بإعطاء المستخدم مؤشراً على قيمة لكنك لا تريده أن يحتفظ بذلك المؤشر (على سبيل المثال إذا كان موقع الكائن في الذاكرة غير ثابت) وفي هذه الحالة يجب أن يبدأ اسم الدالة التي تعطي المتغير الموصول ب ref بدل get.

الكائنات المفردة (singleton) والمتغيرات الساكنة (static variables)

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

الولوج بين الكائنات المفردة

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

التناسق

يجب الحفاظ على التناسق في تصميم الشفرة المصدرية وتسميات عناصرها. فيما يلي أمثلة على ذلك:
  • إذا كانت فئة مغلفة لصنف معين (wrapper class) تحتوي على مؤثر تحويل صنف (casting operator) فيُتوقع لفئة مغلفة لصنف آخر أن تحتوي على مؤثر تحويل صنف أيضا.
  • إذا كانت دالة بحثية تبدأ بfind قادرةً على إرجاع قيمة باطلة (null) فإن دالة أخرى تبدأ أيضا بfind يُتوقع أيضاً أن تقدر على إرجاع قيمة باطلة.
  • اسم الدالة يجب أن يكون معبراً عن وظيفتها. على سبيل المثال، الدالة التي تُرجع عنصراً في مصفوفة بناءاً على تسلسل ذلك العنصر يجب أن تبدأ بget وليس بfind.
  • إذا امتلكت فئة map دالةَ remove فإن فئة list يُتوقع أن تملك دالةَ remove هي الأخرى.

حصر المتعلقات في نفس المجال

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

المؤشرات الذكية والمؤشرات البسيطة

الاختيار بين المؤشرات البسيطة والذكية يتم حسب القواعد التالية:
  • القاعدة في التخزين هي استعمال المؤشرات الذكية. أي استخدامها المؤشرات الذكية في كل متغير غير مؤقت (المتغير المؤقت هي مدخلات الدالات ومتغيراتها). يجوز الاستثناء من القاعدة في الحالات التالية:
    • المؤشرات الخارجية، أي تلك التي نحصل عليها من مكتبات خارجية لا يمكننا خزنها باستخدام المؤشرات الذكية.
    • حالات تحسين الأداء.
    • التأشير من كائن إلى مالكه بحيث نضمن أن ذلك المؤشر دائم الصلاحية لأن زوال المالك يؤدي لزوال المملوك (ولأن استخدام مؤشرين ذكيين ينتج حلقة مؤشرات ذكية).
    • مؤشرات إلى الكائنات المفردة (singleton).
  • تستخدم المؤشرات البسيطة في المتغيرات المؤقتة مثل مدخلات الدالات ومتغيراتها ويستثنى من القاعدة الحالات التالية:
    • استقبال مؤشرات سيتم الاحتفاظ بها.
    • دالات الولوج إلى المؤشرات الذكية داخل الكائنات، ففي هذه الحالة يُرجع متغير موصول للمؤشر الذكي (reference to smart pointer).
    • إرجاع مؤشر لكائن منشأ للتو.

const String& و const Char*

يُستخدم const Char* لمعطيات الدالّات. الغرض تجنب تشييد كائن String عند تمرير سلاسل المحارف البسيطة إلى الدالّات.
يُستخدم const String& لنتائج الدالّات. الغرض تمكين استخدام الوظائف المتقدمة لString مع نتائج الدالّات. إن كان استخدام String في نتيجة الدالّات يتطلب تشييد كائن غير موجود أساساً فعندها يجب استخدام Char بدل String في نتيجة الدالة.

الإشعارات الاستثنائية (exceptions)

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

الأصناف الداخلية

استخدام الأصناف الداخلية (تعريف أصناف داخل فئة ما) يتبع ما يلي:
  • إذا كان استخدام الصنف منحصراً داخل فئة فيعرّف داخل تلك الفئة.
  • إذا كان استخدام الصنف منحصراً على فئة ولا يستخدم خارجها إلا في التعامل معها فيُعرّف داخل تلك الفئة. مثال على ذلك صنف ناتج إحدى دالّات الفئة.
  • إذا كان هناك احتمال أن يستخدم الصنف المتعلق بفئة من قبل طرف خارجي في تعامل منفصل عن هذه الفئة فلا يعرف الصنف داخل الفئة.
  • إذا كان الصنف فئة يمكن الاشتقاق منها فتعرّف بشكل مستقل خارج أي فئة أخرى.

ملاحظات متنوعة

  • يجب على std::vector أن تخزن مؤشراً عندما يحتوي الكائن المشار إليه على دالة تشييد معقدة (على سبيل المثال دالة تحجز ذاكرة) وذلك لتجنب تكرار العملية المعقدة عندما تُنسخ عناصر المصفوفة أثناء تغيير حجمها.
  • يجب تعليم كل دالة عضو لا تقوم بأي تغيير على الكائن ب const.
  • استخدام المتغيرات العمومية يجب تلافيه قدر الإمكان.
  • يجب إنشاء المتغيرات داخل أصغر مجال ممكن.
  • يجب أن لا تُستخدم الهياكل إلا عندما لا تحتاج إلى أي دالّات عضوة، يُستثنى من ذلك الدالات التشييدية.
  • عند تشييد الكائن تتم تهيئة المتغيرات باستخدام initialization lists بدل عمليات التعيين. وبصورة عامة يجب استخدام مشيّدات الكائنات بدل عمليات التعيين.
  • يجب اتباع مبادئ SOLID في التصميم الكائني المنحى.

أسلوب التسميات


أسلوب تسميات القواعد

أسماء القواعد الترميزية والقواعد الإعرابية ومجموعات المحارف

تكتب بأسلوب باسكال، أي بحرف كبير في بداية كل كلمة. كما في: ImportCommand.
وتكتب بالعربية مع تطويل أول حرف متصل من كل كلمة: كما في: أمـر_الـشمول.

أسماء الحزم

تكتب بأسلوب باسكال أيضاً مثل أسماء القواعد الترميزية. كما في: ImportSubject.
وتكتب بالعربية مع تطويل أول حرف متصل من كل كلمة: كما في: مـوضوع_الـشمول.

أسماء البيانات

البيانات، مثل string و integer و list و map تكتب بصيغة السنام، أي بحرف كبير في بداية كل كلمة باستثناء الكلمة الأولى. كما في: multiplicationOperators.

أسماء الأصناف

الأصناف، أي تعريفات type، تكتب بالأحرف الصغيرة وتفصل الكلمات بالشارحة السفلية. كما في: valid_subject.

أسلوب تسميات الشفرة المصدرية

الكلمات التمييزية (keywords)

تكتب بالأحرف الصغيرة وتُفصل كلماتها بالشارحة السفلية. كما في: my_keyword، كلمة_معيارية

الفئات (classes)، الوسائط (interfaces)، السرود (enumerations)، المجالات (namespaces)

تكتب بأسلوب باسكال، أي بحرف كبير في بداية كل كلمة. كما في: MyClass، MyNamespace.
وبالعربية تكتب مع تطويل أول حرف متصل من كل كلمة كما في: الـصنف_الـأول، صـنفي.
تستخدم الوسائط كلمات مصدرية (مصادر الأفعال) بينما تستخدم الفئات أسماء الفاعل. مثلاً يحمل الوسيط اسم "كتابة" بينما تحمل الفئة الموفرة لهذا الوسيط اسم "كاتب". وبالانجليزية يكون اسم الوسيط Writing بينما يكون اسم الفئة Writer.

المتغيرات

تكتب بصيغة السنام، أي بحرف كبير في بداية كل كلمة باستثناء الكلمة الأولى. كما في: myVar.

الدالّات

تكتب مثل المتغيرات ولكنّها تبدأ بفعل. كما في: getMyVar.

الإشارات (signals)

تكتب مثل المتغيرات ولكنّها تنتهي أما بفعل ماضٍ أو بnotifier أو inquirer أو signal. كما في: tokenProcessed، tokenProcessingSignal، tokenProcessingNotifier.

الثوابت والماكروهات (macros)

تكتب بالأحرف الكبيرة وتفصل الكلمات عن بعضها بالشارحة السفلية. كما في: MY_CONSTANT.
وبالعربية تكتب مع تحويطها بشارحة سفلية من الجهتين كما في: _الثابت_الأول_.

معطيات القوالب

تكتب مثل الثوابت، أي بأحرف كبيرة وبفصل الكلمات عن بعضها بالشارحة السفلية. كما في: TEMPLATE_ARG.
وبالعربية تكتب مع تحويطها بشارحة سفلية من الجهتين كما في: _المعطى_واحد_.

العناصر التابعة

العناصر التابعة هي التي لا يجوز التعامل معها مباشرة وإنما عن طريق عناصر أخرى ويُسبق اسم هذه العناصر بالشارحة السفلية. على سبيل المثال: _getValue لا يجب استدعاؤها مباشرة وإنما عن طريق دالّة أخرى بإسم مشابه كأن يكون getValue.

ملاحظات متنوعة

  • لا تُسبق الأسماء بأي ترميز. على سبيل المثال لا نسبق المؤشرات بالحرف p أو العناصر العمومية (global) بالحرف g.
  • دالّات استحضار قيمة ثنائية يمكن أن تبدأ بالفعل is كما في isVisible، أو أن تبدأ بفعل مضارع بصيغة الشخص الثالث كما في exists.
  • يجب تجنب الأسماء المنفية. على سبيل المثال يجب استخدام errorFound بدل errorNotFound.
  • يمكن لثوابت السرد أن تُسبق باسم الصنف أو اختصاره. كما في:
    enum Color {
      COLOR_RED,
      COLOR_GREEN,
      COLOR_BLUE
    };
    
  • اسم الكائن كامن ولا يجب تكراره في أسماء العناصر الضمنية. على سبيل المثال:
    String::getLength()        // correct
    String::getStringLength()  // wrong
    
  • صيغة الجمع تستخدم مع العناصر التي تحمل مجموعة من الكائنات كما في المصفوفات.
  • الاختصارات تعامل معاملة الكلمات ولا تُكتب كل حروفها بالأحرف الكبيرة. على سبيل المثال نكتب Eof وليس EOF.

أسلوب تسميات الملفّات

  • لا تُسبق أسماء الملفات بأي ترميز
  • تبدأ أسماء المجلدات بالأحرف الكبيرة وتُفصل كلماتها بشارحة سفلية.
  • أسماء الملفات تكتب كاملة بالأحرف الصغيرة وتُفصل كلماتها بالشارحة السفلية.
  • أسماء الملفات والمجلدات المعنية بعناصر من الشفرة المصدرية تطابق أسماء تلك العناصر وبنفس الصيغة. على سبيل المثال الملف الذي يحتوي الفئة MyClass يكتب أيضاً MyClass ولا يكتب My_Class كما هي الحال في الصيغة الافتراضية لكتابة اسماء المجلدات.
  • المجالات (namespaces) تُمثل بمجلدات باستثناء الحالات التي يُمكن فيها تمثيل مجال في ملف واحد فقط عند ذلك يُهمل المجلد ويُسمى الملف باسم المجال وبنفس الصيغة.
  • ملفات الشفرة المصدرية تحمل الإضافة cpp بينما تحمل ملفات التعريفات إضافة h.

تنسيق الشفرة المصدرية


الفئات والدالّات

توضع أقواس البداية والنهاية على أسطر منفصلة.
class MyClass
{
  ...
};
void myFunction()
{
  ...
}

الهياكل

تحمل الهياكل أحدى هاتين الصيغتين:
struct MyStruct
{
  ...
};
struct {
  ...
} myVar;

المجالات

أقواس المجالات توضع على أسطر منفصلة ومحتويات المجالات لا تُزحّف عن بداية السطر.
namespace MyNamespace
{

class MyClass
{
  ...
}; // class

} // namespace
للإختصار، يمكن كتابة المجالات المتداخلة بهذه الصيغة:
namespace OuterNamespace { namespace InnerNamespace {

class MyClass
{
  ...
}; // class

} } // namespace

الحلقات

يوضع قوس البداية على نفس السطر بعد أمر الحلقة ويوضع قوس النهاية على سطر منفصل. يجب وضع مسافة واحدة بعد الكلمة التعريفية وأخرى قبل قوس البداية.
for (...) {
  ...
}
while (...) {
  ...
}
إذا كان متن الحلقة من جملة واحدة يمكن إهمال الأقواس ووضع المتن على نفس السطر بعد أمر الحلقة. إن كان المتن أطول من أن يوضع على نفس السطر فيجب وضع الأقواس حتى لو كان المتن جملة واحدة فقط.
for (...) doSomething();

الأوامر الشرطية

تنسيق الأوامر الشرطية مطابق لتنسيق الحلقات. في حالة وجود عبارة else فتوضع عى نفس السطر مع قوس النهاية السابق وقوس البداية التالي مع ترك مسافات في كلا الجانبين.
if (...) doSomething();
if (...) {
  ...
}
if (...) {
  ...
} else if (...) {
  ...
} else {
  ...
}

أوامر التبديل (switch)

تنسيقها مشابه لتنسيق الحلقات. تُزحف عبارات case مرة واحدة وتزحف متونها مرتين.
switch (...) {
  case ...:
    ...
    break;
  case ...:
    ...
    break;
}

الأخطاء الاستثنائية

تستخدم عبارات try-catch التنسيق التالي:
try {
  ...
}
catch (...) {
  ...
}
catch (...) {
  ...
}
finally {
  ...
}

الأسطر الفارغة

  • لا داعي لفصل تعريفات المتغيرات عن بعضها بأسطر فارغة.
  • تفصل الدالّات الضمنية (inline) بسطر فارغ واحد وتفصل الدالات غير الضمنية بسطرين.
  • تفصل الأقسام المختلفة للفئة بسطرين فارغين. فيوضع سطرين فارغين قبل عنوان ذلك القسم، وسطر واحد فارغ بعد العنوان.
  • يوضع سطر فارغ قبل قوس نهاية المجال.
  • الجملة التعريفية للمجال يسبقها سطر واحد ويليها سطر واحد.

التعليقات

  • يجب شرح عناصر الشفرة المصدرية باستخدام تعليقات doxygen وباسلوب javadoc.
  • يجب الفصل بين الأقسام المختلفة في الملف الواحد باستخدام تعليق من سلسلة من المحرف / تصل إلى العمود 80.
    /**
     * @file My_Class.h
     * .....
     */
    //////////////////////////////////////////////////////////////////////////////
    
    class My_Class
    {
      ////////////////////////////////////////////////////////////////////////////
      // Member Variables
      ...
    
      ////////////////////////////////////////////////////////////////////////////
      // Constructor / Destructor
      ...
    
      ////////////////////////////////////////////////////////////////////////////
      // Member Functions
      ...
    }; // class
    
  • تُفصل أقسام تعليقات doxygen بسطر فارغ. المعطيات والنتيجة تعتبر قسماً واحداً. النبذة و @ingroup تعتبران قسماً واحداً أيضاً. الشرح قد يحتوي عدة أقسام. مثال:
    /**
     * @brief This is the brief.
     * @ingroup samplegroup
     *
     * This is the first
     * details block.
     *
     * This is the second
     * details block.
     *
     * @param param1 ...
     * @param param2 ...
     * @return ......
     *
     * @note .....
     *
     * @sa ...
     * @sa ...
     */
    
  • يمكن إهمال الأسطر الفارغة بين أقسام تعليقات doxygen إذا اقتصر كل قسم على سطر واحد فقط.
  • يُستخدم الرمز <br> إذا أردنا بدأ سطر جديد في الوثيقة الناتجة من doxygen.

ملاحظات متنوعة

  • تعرف أعضاء الفئة بهذا الترتيب: الأصناف تليها المتغيرات تليها دالّتا التشييد والتهديم تليهما بقية الدالّات.
  • كل أمر تُتبع كلماته التعريفية بمسافة واحدة.
  • الحد الأقصى للأعمدة في الشفرة المصدرية هو 120 عموداً.
  • علامة التبويب (tab) تُمثَّل بمسافتين. يجب تجنب استخدام محرف الtab.
  • عند تقسيم سطر ما إلى سطور متعددة يبدأ السطر الجديد من نقطة تماثل الجزء الذي ينتمي إليه السطر الجديد. على سبيل المثال:
    result = a + b + c + d + e +
             f + g;
    void myFunction(int arg1, int arg2, int arg3,
                    int arg4, int arg5);
    for (int i = 0;
         i < 5;
         i++) {
      ...
    }
    
  • علامات المؤشرات توضع ملاصقة لأسم المتغير وليس لصنفه كي لا تعطي انطباعاً بأن تأثيرها ينطبق أيضاً على ما بعد الفارزة. على سبيل المثال:
    int *i, j; // correct, because i is a pointer, but not j.
    int* i, j; // wrong
    
    العكس يطبق في حالة صنف الدالة (نتيجة الدالة) كي لا يُظن أنه مؤشر على دالة بدل دالة تنتج مؤشراً.
    int* getSomething(); // correct
    int *getSomething(); // wrong
    
  • عبارات تضمين الملفات التعريفية توضع في أعلى ملف الشفرة المصدرية.
  • يجب الإشارة إلى العناصر العمومية باستخدام :: أو <namespace>::. يُمكن استثناء عناصر المجال Basic الأساسية مثل المؤشرات الذكية على سبيل المثال.
  • العناصر العضوة يشار إليها باستخدام this-> في كل الحالات.
  • العناصر العضوة الساكنة يُشار إليها باستخدام <class name>:: في كل الحالات.
  • متن الدالات يوضع في ملف التنفيذ وليس في ملف التعريف باستثناء الحالات التالية:
    • الدالة مضمنة (inline): توضع في ملف التعريفات.
    • الدالة بسيطة جداً (سطر أو سطرين وبدون حلقات): لا تفضيل.
    • الدالة بسيطة نسبياً (بضع جمل قد تحتوي حلقات بسيطة) وكل الدالات الأخرى في الفئة أما مضمنة أو بسيطة: لا تفضيل.
  • تهيئة متغيرات الحلقات يجب أن يتم قبل الحلقة مباشرة.
  • الحلقات اللانهائية تعرف باستخدام while (true)
  • لا يُسمح لغير عبارات التحكم بالحلقة التواجد داخل أقواس التحكم. على سبيل المثال:
    // Correct:
    total = 0;
    for (i = 0; i < 10; ++i) {
      total += arr[i];
    }
    
    // Wrong:
    for (total = 0, i = 0; i < 10; ++i) {
      total += arr[i];
    }
    
  • يجب الاستعاضة عن حلقات do-while بحلقات while قدر المستطاع لأن الأخيرة أسهل قراءة.
  • الأعداد الحقيقية تكتب دائما بالفاصلة. أي نكتب 5.0 وليس 5 فقط. ولا يجب إهمال الصفر في الاعداد الجزئية (fractional) أي لا نهمل الصفر في 0.5.
  • يجب تجنب إهمال صنف نتيجة الدالة حتى وإن كان المترجم يسمح بذلك.
  • عملية تحويل الأصناف يجب أن تكون صريحة (explicit) وليست كامنة.
  • يجب تجنب معاملة القيم الغير ثنائية كقيم ثنائية في الجمل الشرطية. على سبيل المثال:
    if (str == 0) ... // correct
    if (!str) ...     // wrong
    
  • توضع التعريفات المتعلّقة بفئة معينة داخل الملف التعريفي لتلك الفئة حتى لو كانت التعريفات خارج الفئة نفسها.