أنماط كتابة الشيفرات المُتعارَف عليها (Coding Conventions)

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

تناقش هذه الصفحة أنماط كتابة الشيفرات المُتَّفق عليها من قِبل مبرمجي لغة Kotlin.

تطبيق دليل التنسيق

لضبط التنسيق بما يتوافق مع هذا الدليل يُنصَح بتثبيت إضافة Kotlin بالإصدار 1.2.20 (أو أي إصدار أحدث) ومن ثم ضبط المّحرِّر من خلال الذهاب إلى الإعدادات (Settings) ثمّ المُحرِّر (Editor) ثم نمط الشيفرة (Code Style) ثم Kotlin واضغط على "ضبط من..." Set from…"‎" في الزاوية اليمنى العُلويَّة واختر الخيار Predefined style/Kotlin style guide من القائمة.

وللتأكد من تنسيق الشيفرة وفقًا لدليل Kotlin انتقل لإعدادت التدقيق (inspection settings) وفعِّل الخاصية الموجودة في Kotlin ثم Style issues ثمّ File is not formatted according to project setting (أيّ أنَّ تنسيق الملف ليس خاضعًا لإعدادات المشروع)، أمّا التدقيقات الإضافيّة والتي تتبع لدليل الأنماط المتعارَف عليه (مثل قواعد التسمية) فهي مفعَّلة بالحالة الافتراضيَّة.

تنظيم الشيفرة المصدريّة source code

بُنية المجلدات (Directory Structure)

في المشاريع البرمجيّة المكتوبة بعدة لغات برمجة يجب أن توضع الملفات المصدريّة (source files) الخاصّة بلغة Kotlin بنفس الجذر الذي يحتوي على الملفات المصدريّة بلغة Java، وأن تتبع نفس بنية الأدلّة (يجب أن يُخزَّن كل ملف (file) في دليلٍ بحسب توصيف الحزمة (package) الوارد فيه).

أمّا في المشاريع البرمجيّة المكتوبة كليًّا بلغة Kotlin فيُنصَح باتِّباع بنية الحزم (package structure) مع حذف حزمة الجذر المشتركة، فإذا كانت كلُّ الشيفرات في المشروع موجودةً في الحزمة "org.example.kotlin" مثلًا أو في أيّ من الحزم الفرعية الموجودة فيها، فإنّ الملفات الموجودة في الحزمة "org.example.kotlinn" يجب أن تُوضَع مباشرةً تحت الجذر الأصل، أمّا الملفات في الحزمة "org.example.kotlin.foo.bar" فيجب أن توضع في الدليل الفرعي "foo/bar" من الجذر الأصل دون ذكر الحزمة المشتركة "org.example.kotlin.foo.bar".

أسماء الملفات المصدريّة (Source File Names)

إذا كان الملف يحتوي على صنفٍ (class) واحدٍ فقط (وقد يكون مرتبطًا بتصريحات (declarations) بمستوىً أعلى) فإنّ تسميته يجب أن تكون بنفس تسمية هذا الصنف مضافًا إليها اللاحقة ‎.kt. أمّا إن كان الملف يحتوي على أكثر من صنف أو على تصريحات بمستوىً أعلى فقط  فيجب اختيار اسم يعبِّر عن محتويات هذا الملف وتسميته به باعتماد نمط التسمية camel humps بالبدء بحرفٍ كبيرٍ لكلِّ كلمةٍ مثل: ProcessDeclarations.kt.

يجب أن يكون اسم الملف ذا دلالةٍ لما تقوم به الشيفرة التي يحتوي عليها، ويُنصح بتجنُّب الأسماء الاعتباطية غير المُعبِّرة مثل "Util".

تنظيم الملفات المصدريّة (Source File Organization)

من الجيد وضع التصريحات (declarations) المتعدِّدة (كالأصناف (classes) أو الدوال بمستوى أعلى (top-level functions) أو الخاصّيات (properties)) في نفس الملف المصدريّ طالما أن هذه التصريحات مرتبطةٌ فيما بينها، وما دام حجم الملف الإجماليّ مقبولًا (دون أن يتجاوز عدد أسطره عدة مئات).

أما في الحالة الخاصّة لدى تعريف الدوال الإضافيّة (extension functions) من أجل صنفٍ ما، والتي ترتبط بكافّة عملاء (clients) هذا الصنف فمن الأفضل أن تُوضَع بنفس الملف الذي يُعرَّف فيه الصنف ذاته، أمّا عندما تكون مخصَّصةً لعميلٍ واحدٍ فيُحبَّذ وضعها بجوار شيفرة هذا العميل، دون إنشاء ملفات خاصّة بها.

مخطط الصنف (Class Layout)

تُرتَّب عناصر الصنف بشكل عامٍّ وفقًا للترتيب الآتي:

  • تصريحات الخاصّيات (properties) وأجزاء التهيئة الأولية (initializer blocks)
  • البواني الثانوية (secondary constructors)
  • تصريحات التوابع (methods)
  • الكائنات المرافقة (companion object)

وليس من الشائع ترتيب تصريحات الطرق بطريقة هجائية أو بحسب إمكانية الوصول لها، ولا تٌفصَل الطرق العادية عن التوابع الإضافية (extension methods)، بل تُوضَع العناصر المرتبطة ببعضها سويّةً بحيث تساعد مَن يقرأ الشيفرة بتتبُّع التسلسل المنطقيِّ لها لتوضيح ما تقوم به، ويجب الالتزام بنفس الترتيب (من العناصر الواقعة في المستوى الأعلى للعناصر الواقعة في العناصر الأخفض أو بالعكس) بكل الشيفرات.

ويُفضَّل وضع الأصناف المتداخلة (nested classes) بجوار الشيفرة التي تستخدِم هذه الأصناف، وفي حال وجود أصناف مُعدَّة لاستخدامها خارجيًّا دون الإشارة لها داخل الصنف، فتُوضَع في النهاية بعد الكائنات المرافقة.

مخطط تعريف استخدام الواجهة (Interface Implementation Layout)

من المُساعد لدى إجراء الواجهة المحافظةُ على نفس ترتيب العناصر الوارد في الواجهة (وقد تتخلَّلها بعض الطرق الخاصَّة (private) المُستخدَمة بعملية تعريف الاستخدام (implementation)).

مخطط التحميل الزائد (Overload Layout)

من المتعارَف عليه أن تُوضَع عمليات التحميل الزائد (overload) متجاورةّ دائمًا في الصنف.

قواعد التسمية

تتبع لغة Kotlin قواعد التسمية في لغة Java نفسها؛ إذ يجب أن تكون أسماء الحزم (packages) بأحرف صغيرة دائمًا دون استخدام الشرطة السفلية (underscore) (مثل التسمية org.example.myproject)، ولا يُفضَّل اعتماد التسمية بكلمات متعدِّدة، ولكن في حال الحاجة لها يُستخدم نمط التسمية camel humps بالابتداء بحرفٍ صغيرٍ (مثل org.example.myProject).

أمّا أسماء الأصناف (classes) والكائنات (objects) فتبدأ بحرفٍ كبير ثم تتبعُ نمط التسمية camel humps، مثل:

open class DeclarationProcessor { ... }

object EmptyDeclarationProcessor : DeclarationProcessor() { ... }

أسماء الدوال (Function Names)

تبدأ أسماء كلٍّ من الدوال (functions) والخاصّيات (properties) والمتحوِّلات المحليّة (local variables) بحرفٍ صغيرٍ وباعتماد نمط التسمية camel humps دون استخدام الشرطة السفليّة (underscore)، مثل:

fun processDeclarations() { ... }

var declarationCount = ...

وتُستثنى من ذلك الدوال المُنتِجة (factory functions) والمستخدَمة لإنشاء نسخةٍ عن الأصناف إذ تكون لها نفس تسمية الصنف المتعلِّقة به، مثل:

abstract class Foo { ... }

class FooImpl : Foo { ... }

fun Foo(): Foo { return FooImpl(...) }

أسماء توابع الاختبار (Test Methods Names)

يُسمَح في تسمية توابع الاختبار (والاختبار فقط) استخدامُ المسافات spaces بشرط أن تُوضَع ما بين إشارتي backticks (الرمز `)، ومن الجدير بالذكر هنا أنّ هذه التسمية غير مدعومة في نظام Android، كما ويُسمح باستخدام الشرطة السفليّة في شيفرات الاختبار، مثل:

class MyTestCase {
    @Test fun `ensure everything works`() {
    }

    @Test fun ensureEverythingWorks_onAndroid() {
    }
}

أسماء الخاصّيات Properties Names

أمّا الثوابت (constants) (وهي الخاصّيات المُعرَّفة من نوع const أو خاصّيات المتحولات val التابعة للكائنات (objects) أو بمستوىً أعلى (top-level) دون وجود دالةٍ للحصول عليها get والتي تحتوي فعليَّا على بيانات ثابتة)، فهي تُسمى بأحرفٍ كبيرة إجمالًا ويفصِل بين الكلمات شرطةٌ سفلية (underscore)، مثل:

const val MAX_COUNT = 8

val USER_NAME_FIELD = "UserName"

أما خاصّيات الكائنات (objects) أو الخاصّيات بمستوىً أعلىً (top-level) والتي تحتوي على الكائنات وعمليّاتها أو بيانات قابلةً للتعديل فإنها تُسمى بنمط التسمية camel humps، مثل:

val mutableCollection: MutableSet<String> = HashSet()

أما تسمية الخاصّيات التي تُشير إلى كائنات singleton فهي تتبع لنفس نمط التصريحات عن الكائنات objects، مثل:

val PersonComparator: Comparator<Person> = ...

أمّا عند تسمية الثوابت المتعدِّدة (enumeration) فهناك طريقتان: إما تسميتها بأحرفٍ كبيرةٍ إجمالًا يُفصَل بين كلماتها بالشرطة السفليّة (underscore) مثل: enum class Color { RED, GREEN }‎ . أو بطريقة التسمية الاعتياديّة camel humps ابتداءً بحرفٍ كبيرٍ، ويكون اختيار نوع التسمية بحسب استخدامها.

أسماء الخاصّيات المساعدة (Backing Properties Names)

إذا احتوى الصنف (class) على خاصَّتَين متشابهتَين؛ إحداهما جزءٌ من واجهة API عامّة والأخرى من تفاصيل تعريف الاستخدام (implementation)، حينها تُستخدم الشرطة السفليّة كبادئةٍ قبل اسم الخاصّة (property) التي يكون نوعها خاصًّا (private)، مثل:

class C {
   private val _elementList = mutableListOf<Element>()
   val elementList: List<Element>
        get() = _elementList
}

الاختيار الجيد للتسمية

غالبًا ما يكون اسم الصنف (class) اسمًا أو عبارةً اسميّة تعبِّر عن ماهيّة هذا الصنف، مثل: List أو PersonReader، أما تسمية التوابع (method) فغالبًا ما تكون فعلًا أو عبارة فعليّة تدلُّ على مهمة هذه التابع، مثل: close أو readPersons، وكما يجب أن يدلُّ الاسم عمّا إذا كانت التابع تحدِث تغييرًا في الكائن (object) أو تعيد كائنًا آخر، فمثلًا، تعبِّر التسمية sort عن تبديل ترتيب العناصر الواقعة في المجموعة نفسها، أمّا التسمية sorted فهي تعبِّر عن التابع الذي يعيد مجموعةً (نسخة) مرتبةً من المجموعة؛ إذ يجب أن تُوضِّح الأسماء الهدفَ من الكيان البرمجي المُسمى، ومن الأفضل تجنُّب الأسماء بدون معنىً مثل: Manager أو Wrapper أو… إلخ.

وعند وجود اختصارات (acronym) بالتسمية فتجب كتابة الاسم كاملًا بأحرفٍ كبيرةٍ إن كان مؤلفًا من حرفين فقط مثل: IOStream ، ويبدأ بحرفٍ كبيرٍ ويستمرُّ بأحرفٍ صغيرةٍ إن كان بأكثر من حرفين، مثل: XmlFormatter و HttpInputStream.

التنسيق

تماثل لغة Kotlin في نمط التنسيق لغة Java غالبًا، مثل استخدام 4 مسافات فارغة (spaces) عند الحاشية بدلًا من استخدام tabs. وعند استخدام أقواس المجموعات { } يُوضَع قوس الافتتاح بنهاية السطر حيث سيبدأ الجزء البُنيويّ (construct)، ويُوضَع قوس الإغلاق في سطرٍ منفصلٍ بشكلٍ محاذٍ عموديًّا لبداية هذا الجزء (block)، مثل:

if (elements != null) {
    for (element in elements) {
        // ...
    }
}

ويُلاحظ أنّ إضافة الفاصلة المنقوطة (;) أمرٌ اختياريٌّ في لغة Kotlin وبالتالي فإن الفصل من خلال الانتقال لسطرٍ جديدٍ يظهر واضحًا، ويعتمد النسق العامّ هنا على الأقواس المُستخدَمة في لغة Java وقد يحدث خللٌ ما في حال الاعتماد على نسقٍ مغاير.

المسافات البيضاء الأفقيّة Horizontal whitespace

  • تُضاف المسافات (spaces) على جانبي المعامِلات الثنائية (binary operators) مثل: a + b، وتُستَثنى من ذلك المجالات فتكتب بالشكل: 0..i ، أما في حالة المعامِلات الأحاديّة (unary operators) فلا تُضاف أيّة مسافات مثل: a++‎ .
  • تُضاف مسافةٌ ما بين كلمات التحكم بالتدفق (مثل if و when و for و while ) والقوس الابتدائي التالي لها.
  • لا تُضاف مسافةٌ قبل القوس الابتدائي في التعريف الأساسيّ للبُنى البرمجيّة أو تعريف التوابع (methods) أو استدعائها كما هو واضحٌ في الشيفرة:
class A(val x: Int)

fun foo(x: Int) { }

fun bar() {
    foo(1)
}
  • لا تُضاف أي مسافات بعد أقواس البدء )و] أو قبل أقواس الإنهاء (و[.
  • لا تُضاف أي مسافات على جانبيّ . أو .? مثل: foo.bar().filter { it > 2 }.joinToString()‎ أو foo?.bar()‎ .
  • تُضاف مسافةٌ بعد رمز التعليق // مثل: ‎// This is a comment.
  • لا تُضاف أي مسافة على جانبيّ القوسين < و > والمستخدمة لتحديد متحوّلاتٍ من نوعٍ معيّن، مثل: class Map<K, V> { ... }‎ .
  • لا تُضاف أي مسافة على جانبيّ الرمز :: مثل: Foo::class أو String::length.
  • لا تُضاف أيّ مسافة قبل الرمز ? والمستخدم للدلالة على النوع nullable مثل: String?‎.

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

النقطتان الرأسيّتان Colon (:)

تُضاف مسافةٌ (space) قبل النقطتين الرأسيّتين : في الحالات الآتية:

  • عندما تستخدم للفصل ما بين نوع ونوع أعلى (supertype).
  • عند التعميم لبانٍ من صنف أعلى (superclass constructor) أو لبانٍ آخر ضمن نفس الصنف.
  • بعد الكلمة المفتاحية object.

ولا تُضاف مسافة قبلهما عندما تفصلان ما بين التصريح (declaration) ونوعه.

وتُضاف المسافة دائمًا بعدهما.

مثال:

abstract class Foo<out T : Any> : IFoo {
    abstract fun foo(a: Int): T
}

class FooImpl : Foo() {
    constructor(x: String) : this(x) {
        //...
    }
    
    val x = object : IFoo { ... } 
}

تنسيق ترويسة الصنف Class Header

تُمكن كتابة متحولات الباني (constructor parameters) في سطرٍ واحدٍ إن كان عددها قليلًا مثل:

class Person(id: Int, name: String)

أمّا في حالة الأصناف ذات الترويسات (headers) الطويلة فيجب تنسيقها بحيث يكون كل متحول (parameter) أساسيّ للباني على سطرٍ منفصلٍ مع الإزاحة (4 مسافات)، كما ويُوضع قوس الإنهاء في سطر آخر منفصلٍ ويُضاف له -في حالة الوراثة- استدعاء الباني من الصنف الأعلى (superclass) أو قائمة الواجهات المُعاد تعريف استخدامها (implemented interfaces)، ليصبح بالشكل:

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name) {

    // ...
}

وإذا ما تعدَّدت الواجهات، فيُوضع استدعاء باني الصنف الأعلى (superclass constructor) أولًا ثم توضع كل واجهةٍ بسطرٍ مختلفٍ، مثل:

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name),
    KotlinMaker {
    
    // ...
}

وإذا ما كانت قائمة النوع الأعلى (supertype) طويلةً فعندها يفصل سطرٌ بين النقطتين الرأسيّتين والأنواع، والتي تُكتب وفق محاذاةٍ عموديٍّة، مثل:

class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne {

    fun foo() {}
}

وللفصل الواضح ما بين الترويسة (head) الطويلة للصنف وبُنيته (body) إما أن يُوضع سطرٌ فارغٌ بعد انتهاء الترويسة (كما هو الحال في المثال السابق) أو أن يُكتب قوس البدء في سطرٍ منفصلٍ بالشكل الآتي:

class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne
{
    fun foo() {}
}

إذ تُضاف المحاذاة الاعتياديّة (4 مسافات) قبل متحوّلات الباني، وذلك لجعل محاذاة الخاصّيات (properties) المُعرَّفة في الباني الأساسي بنفس محاذاة الخاصّيات المُعرَّفة في بُنية الصنف (class body).

المُحدِّدات Modifiers

تنسيق الحاشية Annotation

حاشية الملفات File Annotation

تنسيق الدوال Functions

تنسيق بُنية التعبير Expression

تنسيق الخصائص Properties

تنسيق أوامر التحكم بالتدفق Control Flow Statements

تنسيق استدعاء الطرق Method Call

تغليف الاستدعاء المتسلسل Chained Call Wrapping

تنسيق Lambda

التعليقات التوثيقيَّة

عند كتابة التعليقات التوثيقيّة الطويلة فمن المُتفق عليه وضع نجمة إضافيّة لتصبح بداية التعليق بالشكل ‎/**‎ في سطرٍ منفصلٍ وابتداء كل سطرٍ جديدٍ برمز النجمة كما هو واضح في التعليق الآتي:

/**
* هنا سطر
* وهنا سطر آخر
*/

أمّا التعليقات القصيرة فتُوضع في سطرٍ واحدٍ بالشكل الآتي:

/** هذا تعليق سطريّ قصير */

ومن الأفضل تجنُّب استخدام الوسمَين (tags)‏ ‎@param‎ و‎@return‎ ، والاستعاضة عنهما بما يعبِّر عنهما في سياق التعليق التوثيقيّ  وقد تُضاف روابط المتحوّلات المستخدمة عند ذكرها، أمّا إن كان التعليق توصيفيًا طويلًا ومن الصعب صياغته فيُسمَح باستخدام الوسمين السابقين. فعلى سبيل المثال؛ بدلًا من كتابة التعليق بالشكل:

/**
* Returns the absolute value of the given number.
* @param number The number to return the absolute value for.
* @return The absolute value.
*/

fun abs(number: Int) = ...

الأفضل أن يُكتب بالشكل:

/**
* Returns the absolute value of the given [number].
*/

fun abs(number: Int) = ...

تجنُّب البُنى الزائدة

إنّ من المساعِد حذفُ أيّ بنيةٍ زائدةٍ إن كان وجودها اختياريًّا أو حُدِّدَت في بيئة العمل (IDE) بوصفها إضافيّة لا داعي لها؛ لا تُضِف أي عناصر زائدة في الشيفرة بهدف جعلها أكثر وضوحًا وحسب.

النوع Unit

عندما تعيد الدالة النوع Unit فلا داعي لذكره في الشيفرة، مثل:

fun foo() { // حُذفت هنا كلمة النوع Unit
    ...
}

الفاصلة المنقوطة Semicolon (;)

استغنِ عن الفاصلة المنقوطة ما أمكن.

قوالب السلاسل النصية (String Templates)

لا داعي لاستخدام أقواس المجموعة { } عند إدخال متحوِّلٍ بسيطٍ ضمن قالب السلسلة النصيّة، إذ إنها تُستخدَم لتعابير أطول، مثل:

println("$name has ${children.size} children")

الاستخدام الاصطلاحيُّ لمزايا اللغة

المكتبات

من المُستحسَن عند كتابة المكتبات الالتزامُ ببعض القواعد العامّة لضمان استقرار واجهات API، وهي:

  • تحديد إمكانية الوصول (visibilty) للعناصر بشكل صريح، لئلا تُعرَّف -عن طريق الخطأ- من نوعٍ عامٍّ (public).
  • التصريح عن أنواع القيم المُعادَة في الدوال (functions) وكذلك أنواع الخاصّيات (properties)، لتجنُّب حدوث خطأٍ في النوع عند تغيير تعريف الاستخدام (implementation).
  • التزويد بالتعليقات التوثيقيَّة لكافَّة العناصر العامَّة (public) باستثناء عمليات إعادة تعريف الدوال (override) التي لا تتطلَّب أيَّ توثيقٍ جديد، وذلك لدعم إنشاء التوثيق للمكتبة.

مصادر