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

من موسوعة حسوب
< Kotlin
مراجعة 03:20، 7 مارس 2018 بواسطة عبد اللطيف ايمش (نقاش | مساهمات) (تصحيح خطأ مطبعي)
اذهب إلى التنقل اذهب إلى البحث

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

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

لضبط تنسيق IntelliJ formatter بما يتوافق مع هذا الدليل يُنصَح بتثبيت إضافة 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.kotlin" يجب أن تُوضَع مباشرةً في المجلد الجذر للملفات المصدرية، أمّا الملفات في الحزمة "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)

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

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

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

من المُساعد لدى تطبيق (implementing) الواجهة المحافظةُ على نفس ترتيب العناصر الوارد في الواجهة (وقد تتخلَّلها بعض الطرق [methods] الخاصَّة [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)

إن كان لنفس التصريح (declaration) أكثر من مُحدِّد فهي توضع بحسب الترتيب الآتي:

public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation
companion
inline
infix
operator
data

وفي حال وجود أي حاشية (annotation) فإنها تُكتب قبل المُحدِّدات، مثل:

@Named("Foo")
private val foo: Foo

ومن الأفضل حذف أي مُحدِّدات زائدة (مثل public)، ولكنّ هذا لا ينطبق في حال العمل على المكتبات (libraries).

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

تُوضع الحاشية (annotation) بسطرٍ منفصلٍ غالبًا قبل التصريح (declaration) المرتبطة به وبنفس المحاذاة العموديّة، مثل:

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

وفي حال وجود أكثر من حاشيةٍ وبدون متحولات (arguements) فيُمكن وضعها بنفس السطر، مثل:

@JsonExclude @JvmField
var x: String

كما يُمكن وضعها إن كانت بدون متحولات بنفس السطر مع التصريح المرتبطة به، مثل:

@Test fun foo() { ... }

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

تُكتَب حاشية الملف (file annotation) بعد سطر التعليق (إن وُجد) وقبل تعليمة الحزمة (package) بحيث تكون منفصلة بسطر عنها (للتأكيد على أنها للملف لا للحزمة)، مثل:

/** الرخصة وحقوق النشر وخلاف ذلك */
@file:JvmName("FooBar")

package foo.bar

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

يتبع تنسيق ترويسة الدالة (function signature) للشكل الآتي إن تعذّر وضعه كاملًا بسطرٍ واحد:

fun longMethodName(
    argument: ArgumentType = defaultValue,
    argument2: AnotherArgumentType
): ReturnType {
    // بُنية الدالة هنا
}

وتستخدم الإزاحة بأربع مسافات (spaces) لمتحوٍّلات الدالة (parameters) وذلك بهدف تحقيق التشابه مع متحوِّلات الباني (constructor parameters). ومن المُفضَّل استخدام بُنية التعبير (expression body) للدوال إن كانت تحتوي على تعبيرٍ (expression) واحدٍ فقط، مثل:

fun foo(): Int {     // تنسيق سيء
    return 1 
}

fun foo() = 1        // تنسيق جيد

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

إذا كانت بُنية التعبير غير مناسبةٍ في سطرٍ واحدٍ كما هو الحال في التصريح (declaration) فحينها يُمكن وضع إشارة الإسناد = في السطر الأول والإزاحة في السطر التالي بمقدار 4 مسافات (spaces) ومتابعة التعبير، مثل:

fun f(x: String) =
    x.length

تنسيق الخاصّيات (Properties)

يُعتمَد نمط التنسيق السطريّ لخاصّيات القراءة فقط (read-only properties) مثل:

val isEmpty: Boolean get() = size == 0

أما للخاصّيات الأكثر تعقيدًا فمن الأفضل استخدام الكلمتين المفتاحيّتين get و set وبأسطرٍ منفصلةٍ بالشكل:

val foo: String
    get() {
        // ...
    }

أمّا في حالة الخاصّيات بإسناد أوليّ (initializer) فإذا كان الإسناد طويلًا يُوضَع بسطرٍ جديدٍ ابتداءً ممّا يلي إشارة الإسناد = وبإزاحة 4 مسافات (spaces)، مثل:

private val defaultCharset: Charset? =
    EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

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

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

if (!component.isSyncing &&
    !hasAnyKotlinRuntimeInScope(module)
) {
    return createKotlinNotConfiguredPanel(module)
}

وبهذا يصبح الفصل واضحًا ما بين الشرط وبُنية التعليمات.

وتُوضَع الكلمات المفتاحيّة else و catch و finally والكلمة المفتاحيّة while من حلقة do/while بنفس السطر الذي يحتوي على قوس الإغلاق للبُنية التي تسبقه، كما يلي:

if (condition) {
    // ...
} else {
    // ...
}

try {
    // ...
} finally {
    // ...
}

وإذا كان أحد الفروع (branches) التابعة لتعليمة when يمتد على أكثر من سطرٍ فعندها يُفصَل عن الفروع الأخرى عبر سطرٍ فارغٍ مثل:

private fun parsePropertyValue(propName: String, token: Token) {
    when (token) {
        is Token.ValueToken ->
            callback.visitValue(propName, token.value)

        Token.LBRACE -> { // ...
        }
    }
}

أما الفروع القصيرة فتُوضَع مع الشرط بنفس السطر وبدون استخدام الأقواس، مثل:

when (foo) {
    true -> bar() // تنسيق جيّد
    false -> { baz() } // تنسيق سيء
}

تنسيق استدعاء التوابع (Method Call)

إذا كانت قائمة متحولات التابع (parameters) طويلةً فعندها يُضاف سطر جديدٍ بعد قوس البدء وتُزاح المتحوّلات بمقدار 4 مسافات (spaces) مع بداية كلِّ سطرٍ، إذ يًمكن تجميع المتحولات المتقاربة من بعضها بنفس السطر، مع وضع فراغات على جانبي إشارة الإسناد = للفصل ما بين اسم المتحول وقيمته، مثل:

drawSquare(
    x = 10, y = 10,
    width = 100, height = 100,
    fill = true
)

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

عند تغليف الاستدعاء المتسلسل يُوضع الرمز . أو .? بالسطر التالي وبإزاحة بمقدار 4 مسافات (spaces) مثل:

val anchor = owner
    ?.firstChild!!
    .siblings(forward = true)
    .dropWhile { it is PsiComment || it is PsiWhiteSpace }

إذ إنّه غالبًا ما يُفصَل الاستدعاء الأول عمّا قبله بسطرٍ جديدٍ ولكن يمكن الاستغناء عن ذلك.

تنسيق Lambda

تُستخدَم المسافات (spaces) على طرفيّ الأقواس في تعابير lambda وكذلك على طرفيّ السهم الفاصل ما بين المتحولات (parameters) والبُنية (body)، وإن استلزم الاستدعاء إلى lambda واحدةٍ فقط فيجب تمريرها لخارج الأقواس ما أمكن، مثال:

list.filter { it > 10 }

ولدى إسناد تسمية (label) إلى lambda فلا تُوضع مسافة (space) ما بين التسمية وقوس البدء التالي لها، مثل:

fun foo() {
    ints.forEach lit@{
        // ...
    }
}

أمّا عند التصريح (declaration) عن أسماء المتحولات (parameters) في lambda بعدة أسطرٍ، تُكتَب التسميات بالسطر الأوّل ويُلحَق بها السطرُ التالي عبر رمز السهم، أيّ:

appendCommaSeparated(properties) { prop ->
    val propertyValue = prop.get(obj)  // ...
}

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

foo {
   context: Context,
   environment: Env
   ->
   context.configureEnv(environment)
}

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

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

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

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

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

ومن الأفضل تجنُّب استخدام الوسمَين (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")

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

ثبات البيانات Immutability

يُفضَّل استخدام البيانات الثابتة على المتغيّرة، ولهذا فإن التصريح عن البيانات يكون بالكلمة المفتاحيّة val بدلًا من var إن لم تكن هناك حاجةٌ لتحويل القيمة المُخزَّنة فيها فيما بعد.

وتُستخدَم دائمًا الواجهات الثابتة للمجموعات (immutable collection interfaces) مثل: Collection أو List أو Set أو Map للتصريح عن المجموعات الثابتة، وعند استخدام الدوال المُنتِجة (factory functions) لإنشاء المجموعات فإنه من الأفضل (ما أمكن) أن تعيد هذه الدوال أنواعًا ثابتة، مثل:(من الجيد استخدام Set عوضًا عن HashSet واستخدام الدالة التي تعيد النوع الثابت List<T>‎ بدلًا من المتغيّر ArrayList<T>‎ )

// استخدام سيء لنوع متغير للقيمة التي لن تتغير فيما بعد
fun validateValue(actualValue: String, allowedValues: HashSet<String>) { ... }

// استخدام جيد إذ إن النوع هنا ثابت
fun validateValue(actualValue: String, allowedValues: Set<String>) { ... }

//  ArrayList<T> استخدام سيء للنوع المُعاد المتغيّر 
val allowedValues = arrayListOf("a", "b", "c")

// List<T> استخدام جيّد للنوع المُعاد الثابت بدلًا من المتغير
val allowedValues = listOf("a", "b", "c")

القيم الافتراضيّة للمتحوِّلات Parameters

إنّ التصريح عن الدوال بقيمٍ افتراضيّةٍ للمتحولات أفضل من التصريح عنها بالتحميل الزائد (overload) مثل:

// Bad
fun foo() = foo("a")
fun foo(a: String) { ... }

// Good
fun foo(a: String = "a") { ... }

تسمية الأنواع Type Aliases

من الأسهل استخدام التسمية (alias) إذا كانت الأنواع وظيفيّة (functional) أو في حالة المتحولات (parameters) المُستخدمة عدة مرّاتٍ ضمن الشيفرة، وهذا بالشكل:

typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>

متحوّلات Lambda

إذا كانت تعابير lambda قصيرة غير متداخلة فمن الأفضل استخدام الاصطلاح it بدلًا من التصريح عن المتحوّل (parameter)، أما في حالات lambda المتداخلة والمحتوية على متحولات فيجب التصريح عنها بشكل واضح (explicitly).

أوامر الرجوع Return في Lambda

من الأفضل تجنُّب استخدام عدة أوامرٍ مُسماة (labeled) للرجوع في lambda، ومن الأسهل عدُّ ذلك كعملية إعادة بناء lambda بشكلٍ يجعلها بنقطة خروجٍ واحدةٍ فقط، وإن لم يكن كذلك، فيجب تحويل lambda إلى دالة مجهولة (anonymous function).

ويجب كذلك ألا يُستخدَم أمر الرجوع بتسمية (labeled return) في التعليمة الأخيرة في lambda.

المتحوّلات المُسماة Named Arguments

تستخدم صيغة تسمية المتحولات (argument) عند وجود أكثر من متحول (parameters) للتابع (method) ومن نفس النوع الأساسيّ أو عند وجود متحولات من النوع الثنائي (Boolean) وذلك إن لم يكن معنى كل المتحولات واضحًا من السياق العامّ، مثل:

drawSquare(x = 10, y = 10, width = 100, height = 100, fill = true)

استخدام الأوامر الشرطيّة Conditional Statements

يُفضَّل استخدام الشكل التعبيريّ لكلٍّ من try و if و when مثل:

return if (x) foo() else bar()

return when(x) {
    0 -> "zero"
    else -> "nonzero"
}

والتي تُعدُّ أفضل من الشيفرة الآتية:

if (x)
    return foo()
else
    return bar()
    
when(x) {
    0 -> return "zero"
    else -> return "nonzero"
}

when و if

يُحبَّذ استخدام if بدلًا من when في حال وجود حالتين فقط للشرط، أي بدلًا من كتابة الشيفرة:

when (x) {
    null -> ...
    else -> ...
}

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

if (x == null) ... else ...

إذ إن when مُخصَّصة للحالات التي يكون فيها أكثر من خيارين.

استخدام القيم الثنائيّة (Boolean) الممكن أن تكون فارغة (nullable) في الشرط

يجب في تلك الحالة التحقُّق من الشرطين if (value == true)‎ و if (value == false)‎.

استخدام الحلقات (Loops)

يُفضَّل استخدام الدوال بمستوى أعلى (مثل filter و map و ...إلخ.) بدلًا من استخدام الحلقات، وتُستثنى من ذلك حلقة forEach، إذ من الأفضل استخدام حلقة for بدلًا عنها، وذلك ما لم يكن مستقبل forEach قابلًا لاحتواء قيمةٍ فارغةٍ (nullable) أو إن كانت حلقة forEach مستخدمةً كجزءٍ من استدعاء متسلسل (call chain) أطول.

وعند اتخاذ القرار بالاختيار ما بين استخدام الحلقة أو استخدام تعبيرٍ معقّدٍ بالاعتماد على عدة دوال بمستوى أعلى (higher-order)، فإنّه يجب تقدير تكلفة العمليات المنجزة في كلِّ حالةٍ والحفاظ على مستوىً جيدٍ من الأداء.

استخدام الحلقات بالاعتماد على المجالات (Ranges)

يًستحسن استخدام الكلمة المفتاحيّة until في الحلقات مفتوحة المجال، مثل:

for (i in 0..n - 1) { ... }  // استخدام سيء
for (i in 0 until n) { ... }  // استخدام جيد

استخدام السلاسل النصيّة Strings

يُحبَّذُ استخدام قوالب السلاسل النصّيّة (string templates) للربط فيما بينها.

ومن الأفضل استخدام سلاسل بأسطر متعدِّدةٍ بدلًا من اللجوء لاستخدام ‎\n في قيمة السلسلة.

وللحفاظ على المحاذاة في السلاسل النصّيّة الواقعة بعدَّة أسطر يمكن استخدام الدالة trimIndent وذلك إن لم يكن هناك حاجةٌ للمحاذاة الداخليّة في السلسلة الناتجة أو استخدام الدالة trimMargin  في حال الحاجة للمحاذاة الداخلية فيها، مثل:

assertEquals("""Foo
                Bar""".trimIndent(), value)

val a = """if(a > 1) {
          |    return a
          |}""".trimMargin()

الدوال Functions والخاصّيّات Properties

يُمكِن في بعض الأحيان التبديل ما بين الدوال الخالية من المتحولات (no-argument functions) وخاصّيّات القراءة فقط (read-only properties)، إذ إنّ معناهما واحد ولكن هناك بعض الاعتبارات الأسلوبيّة للتفضيل بينهما؛ إذ يُستَحسَن استخدام الخاصّيّات عوضًا عن الدوال عندما تتوافر الشروط الآتية في الخوارزمية المُضمَّنة:

  • لا تحتوي على throw
  • غير مكلفة من ناحية العمليات الحسابيّة (أو مُخزَّنة مؤقتًا [cached] أثناء التشغيل الأول)
  • تعيد نفس النتائج لمختلف الاستدعاءات طالما أنّ حالة الكائن (object) لم تتغير.

استخدام الدوال الإضافيّة (Extension Functions)

تُستخدَم الدوال الإضافيّة بشكلٍ مطلق؛ فما دامت هناك دالةٌ تعمل بشكلٍ أساسيّ على كائنٍ ما (object) فيُمكن تحويلها إلى دالة إضافيّة بجعل الكائن مستقبِلًا لها، وللتقليل من كثرة العبء على الواجهات (API) يفضَّل تحديد مرئية الوصول (visibility) للدالة الإضافيّة ما أمكن، إذ يُستحسَن أن يكون النوع خاصَّا (private) عند الحاجة لاستخدام الدوال الإضافيّة سواء كانت محليّة أو باعتبارها كعناصر (member) أو بمستوى أعلى (top-level).

استخدام الدوال الداخلية Infix Functions

يكون التصريح عن الدالة كدالة داخليّة فقط عندما تعمل على كائنين (objects) اثنين لهما نفس الدور، من الأمثلة الجيدة عن ذلك: and و to و zip، ومن الأمثلة غير المجدية: add، ولا يُصرَّح عن الدالة كدالة داخليّة إن كان لها دورٌ بتغيير الكائن المستقبِل (receiver object).

الدوال المُنتِجة Factory Functions

لا تُسمى الدوال المُنتجِة بنفس اسم الصنف (class) الموجودة فيه بل يُستخدم بتسميتها اسمٌ مميزٌ يدل على ما تقوم به هذه الدالة، وفي حالاتٍ قليلةٍ (إن لم يكن للدالة ما يميزها) يُمكن أن تكون بنفس اسم الصنف. مثال:

class Point(val x: Double, val y: Double) {
    companion object {
        fun fromPolar(angle: Double, radius: Double) = Point(...)
    }
}

كما ويمكن استخدام الدوال المُنتِجة بدلًا من البواني زائدة التحميل (overloaded) إن كان للكائن (object) عدة بوانٍ زائدة التحميل والتي لا تستدعي بدورها أي بوانٍ من مستوى أعلى (superclass constructors) ولا يمكن تقليلها لتصبح بانيًا واحدًا بقيم افتراضية للمتحولات (arguement).

أنواع المنصّات Platform Types

يجب التصريح بشكل واضحٍ عن النوع لأي دالةٍ أو تابعٍ من النوع العامّ (public) إن كان يعيد تعبيرًا من النوع platform كما في الشيفرة:

fun apiCall(): String = MyJavaApi.getProperty("name")

وكذلك الأمر بالنسبة لأي خاصّيّة (property) إن كانت بمستوى الحزمة (package-level) أو بمستوى الصنف (class-level) والتي أُسند لها تعبيرٌ من نوع platform كما في الشيفرة:

class Person {
    val name: String = MyJavaApi.getProperty("name")
}

أما إذا أُسند التعبير من نوع platform إلى متحوّل محليّ (local variable) فيمكن أن يٌصرَّح عن النوع أو تجاهل ذلك، مثل:

fun main(args: Array<String>) {
    val name = MyJavaApi.getProperty("name")
    println(name)
}

استخدام الدوال المجاليّة (Scope Functions)‏ ‎apply/with/run/also/let

توفِّر لغة Kotlin عددًا من الدوال التي تسمح بتنفيذ جزء من الشيفرة (block) ضمن سياق شيفرة الكائن (object)، ولاختيار الدالة المناسبة تُراعى النقاط الآتية:

  • هل تُستدعى التوابع من خلال كائنات مختلفة ضمن جزء الشيفرة (block) أو يُمرَّر الكائن كمتحول (arguement)؟ حينها تُستخدم إحدى الدوال التي تسمح بالوصول إلى الكائن المَعنيّ مثل it وليس this‏ (also أو let)، وتُستخدَم الدالة also إن لم يكن المستقبِل (receiver) مستخدَمًا في هذا الجزء إطلاقًا، مثل:
// الكائن المعنى هو it
class Baz {
    var currentBar: Bar?
    val observable: Observable

    val foo = createBar().also {
        currentBar = it                    // الوصول إلى الخاصيّة في الصنف
        observable.registerCallback(it)    // تمرير الكائن كمتحول في الدالة
    }
}

// المستقبِل غير مستخدم في هذا الجزء
val foo = createBar().also {
    LOG.info("Bar created")
}

// الكائن هو this
class Baz {
    val foo: Bar = createBar().apply {
        color = RED    // الوصول إلى الخاصيات فقط من Bar
        text = "Foo"
    }
}
  • كيف ستكون نتيجة الاستدعاء؟ إن كانت النتيجة عبارة عن كائن (object) فعندها تُستخدَم الدالة apply أو also، أما إن كانت النتيجة عبارة عن قيمة (value) مُعادَة من هذا الجزء فحينها تُستخدم الدالة with أو let أو run، مثل:
// النتيجة المعادة عبارة عن كائن
class Baz {
    val foo: Bar = createBar().apply {
        color = RED    // Accessing only properties of Bar
        text = "Foo"
    }
}


// النتيجة المعادة عبارة عن قيمة
class Baz {
    val foo: Bar = createNetworkConnection().let {
        loadBar()
    }
}
  • هل الكائن من نوع nullable أو يُعدُّ نتيجةً لاستدعاءات متسلسلة (call chain)؟ إن كان كذلك فيجب استخدام الدالة apply أو let أو run، واستخدام with أو also فيما عدا ذلك، مثل:
// الكائن من نوع nullable
person.email?.let { sendEmail(it) }

// الكائن هنا يمكن الوصول له مباشرة وليس من نوع null
with(person) {
    println("First name: $firstName, last name: $lastName")
}

المكتبات

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

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

مصادر