أنماط الشيفرات المُتعارَف عليها ما بين مبرمجي Kotlin

من موسوعة حسوب
(بالتحويل من Kotin/coding conventions)

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

تطبيق دليل التنسيق (Applying the Style Guide)

لضبط المُنسِّق 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 (أيّ أنَّ تنسيق الملف لا يخضع لإعدادات المشروع)، أمّا التدقيقات الإضافيّة التابعة لدليل الأنماط المتعارَف عليه (مثل قواعد التسمية [naming conventions]) فهي مفعَّلة (enabled) بالحالة الافتراضيَّة.

تنظيم الشيفرة المصدرية (Source Code)

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

تُوضع الملفات المصدريّة (source files) الخاصّة بلغة Kotlin في المشاريع البرمجيّة المكتوبة بعدّة لغات برمجةٍ في نفس الجذر الذي يحتوي على الملفات المصدريّة بلغة Java، وتتبع نفس بنية المجلدات (يُخزَّن كل ملف [file] في مجلدٍ [directory] بحسب توصيف الحزمة [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 objects)

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

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

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

عند تعريف استخدام (implementing) الواجهة تبقى العناصر بنفس ترتيب العناصر الوارد في الواجهة الأساسيّة (وقد تتخلَّلها بعض الطرق [methods] الخاصَّة [private] المُستخدَمة بعملية تعريف الاستخدام [implementation]).

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

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

قواعد التسمية (Naming Rules)

تتبع لغة 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) المستخدَمة لإنشاء نسخةٍ (instance) عن الأصناف، إذ تكون لها نفس تسمية الصنف المتعلِّقة به، مثل:

abstract class Foo { ... }

class FooImpl : Foo { ... }

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

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

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

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

    @Test fun ensureEverythingWorks_onAndroid() {
    }
}

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

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

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.

التنسيق (Formatting)

تماثل لغة 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?‎.

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

النقطتان الرأسيّتان 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)

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

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) التي لا تتطلَّب أيَّ توثيقٍ جديد، وذلك لدعم إنشاء التوثيق للمكتبة.

مصادر