Companion Objects in Kotlin

Companion Objects in Kotlin

Companion Objects چیه و چرا مهمه؟

در زبان Kotlin خبری از کلیدواژه‌ی static مثل Java نیست. ولی خب توسعه‌ دهنده‌ ها گاهی به متدها یا متغیر هایی نیاز دارن که مستقیماً بدون ساخت شیء از کلاس قابل دسترسی باشن. "Companion Object" دقیقاً همین نقش رو بازی می‌کنه.

یک Companion Object یک شیء (Object) خاص است که به کلاس والد خود متصل شده است. این اتصال به این معنی است که اعضای تعریف شده در آن، به جای اینکه به یک نمونه خاص از کلاس وابسته باشند، به خود کلاس وابسته هستند.

اهمیت Companion Object:

  1. جایگزینی برای static در Java: فراهم کردن قابلیت دسترسی به اعضا بدون نیاز به نمونه‌ سازی.
  2. تعریف متدهای Utility: گروه‌ بندی توابعی که به حالت (State) خاصی از یک نمونه کلاس نیاز ندارند.
  3. اعلان ثابت‌ها (Constants): نگهداری مقادیر ثابت که برای تمام نمونه‌ های کلاس یا برای خود کلاس مهم هستند.
  4. پشتیبانی از الگوهای طراحی: پیاده‌ سازی آسان‌ تر الگوهایی مانند Singleton (که در Kotlin با object ساده‌ تر است) یا Factory Method.

 

با Companion Object می‌شه:

  • متد و خاصیت رو به کلاس گره زد بدون نیاز به نمونه‌ سازی.
  • مقادیر یا منطق مشترک بین تمام نمونه‌ ها رو یه‌ جا نگه داشت.
  • الگوهای طراحی مثل Singleton یا Factory Method رو راحت پیاده کرد.

 

نحو استفاده در Kotlin

برای تعریف یک Companion Object در داخل یک کلاس، از کلیدواژه companion object استفاده می‌کنیم.

class MyClass {
    companion object {
        // یک ثابت (Constant) که در زمان کامپایل مقداردهی می‌شود
        const val VERSION = "1.0"
        
        // یک متغیر قابل تغییر که وضعیت مشترک را نگه می‌دارد
        var initializedCount = 0

        fun printVersion() {
            println("Version: $VERSION")
        }
        
        fun incrementCounter() {
            initializedCount++
        }
    }
}

fun main() {
    // دسترسی مستقیم از طریق نام کلاس
    MyClass.printVersion() // خروجی: Version: 1.0
    println(MyClass.VERSION) // خروجی: 1.0
    
    MyClass.incrementCounter()
    println("Initialized count: ${MyClass.initializedCount}") // خروجی: Initialized count: 1
}

📌 اینجا نیازی به new یا ساخت آبجکت نیست؛ دقیقاً مشابه متد/فیلد static در جاوا.

 

نام‌ گذاری اختیاری Companion Object

اگر چه معمولاً بدون نام استفاده می‌شود، می‌توان به آن نام داد. این کار زمانی مفید است که بخواهیم یک Companion Object خاص را به عنوان تابع اصلی سازنده کارخانه (Factory) کلاس تعریف کنیم.

class DataWrapper private constructor(val data: String) {
    companion object Factory {
        fun create(input: String): DataWrapper {
            return DataWrapper("Processed: $input")
        }
        
        // متد عمومی کلاس نیز می‌تواند در اینجا باشد
        fun defaultName() = "Default"
    }
}

fun main() {
    // فراخوانی Factory.create
    val wrapper = DataWrapper.Factory.create("Hello")
    println(wrapper.data) // خروجی: Processed: Hello
    
    // دسترسی به متد کلاس اصلی
    println(DataWrapper.defaultName()) // خروجی: Default
}

نکته مهم در مورد دسترسی: اگر نامی به Companion Object داده شود (مثل Factory در مثال بالا)، برای دسترسی به اعضای آن باید از نام آن شیء استفاده کرد (DataWrapper.Factory.create(...)). اگر نامی داده نشود، کامپایلر فرض می‌کند که نام آن شیء، نام کلاس است (DataWrapper.create(...)).

 

مقایسه با Java

درک تفاوت‌ها بین Companion Object در Kotlin و static در Java برای کسانی که از جاوا مهاجرت می‌کنند، بسیار مهم است.

Java:

public class MyClass {
    // ثابت نهایی
    public static final String VERSION = "1.0";

    // متد استاتیک
    public static void printVersion() {
        System.out.println("Version: " + VERSION);
    }
}

نحوه فراخوانی در Java:

MyClass.printVersion();

Kotlin (بدون نام):

class MyClass {
    companion object {
        const val VERSION = "1.0"

        fun printVersion() {
            println("Version: $VERSION")
        }
    }
}

نحوه فراخوانی در Kotlin:

MyClass.printVersion() 

تفاوت‌های کلیدی:

در زبان Kotlin ویژگی Companion Object یک شیء (singleton) است که پشت‌ صحنه ساخته و مدیریت می‌شود. این شیء می‌تواند نام داشته باشد (مثلاً Factory)، و حتی اینترفیس‌ها را هم پیاده‌ سازی کند. هر کلاس فقط یک Companion Object می‌تواند داشته باشد و اعضای آن فقط به خود کلاس not instance ارجاع دارند. این ساختار اجازه می‌دهد که برخی متدها یا خاصیت‌ها را مستقیماً از طریق نام کلاس، بدون ساخت شیء جدید، صدا بزنی. منطق و داده‌ هایی مثل ثابت‌ ها، شمارنده‌ ها یا متد های Factory معمولاً داخل همین بخش قرار می‌گیرند تا کد خوانا و شفاف باقی بماند.

در مقابل، در Java عضوهای static فقط یک عضو وابسته به کلاس هستند و هیچ گونه شیء مستقل یا singleton ساخته نمی‌شود. فضای static نام رسمی ندارد و نمی‌توان به مجموعه staticها یک نام خاص داد. اینترفیس‌ها را نمی‌شود مستقیماً با عضوی static اجرا کرد (مگر با روش خاص متدهای پیش‌فرض JDK 8 به بعد). یک کلاس Java می‌تواند هر تعداد عضو static داشته باشد و این اعضا فقط به سطح کلاس ارجاع دارند، نه به نمونه‌های ساخته شده.

خلاصه : Companion Object در Kotlin امکانات شیء‌ گرایی، انعطاف و تست‌پذیری بیشتری نسبت به static در Java فراهم می‌کند و برای ویژگی‌هایی مثل متدهای کارخانه‌ای ، ثابت‌ های سطح کلاس و الگو های سینگلتون مناسب‌ تر است. اما در Java، static‌ ها ساده و محدود به مورد استفاده مستقیم‌ تر هستند.

 

متدهای کارخانه‌ای (Factory Methods) یعنی متدهایی که مسئول ساخت (ایجاد) نمونه‌های یک کلاس هستند، اما الزاماً همان سازنده (constructor) اصلی نیستند.

این متدها می‌توانند منطق اضافه داشته باشند مثل:

  • مقادیر ورودی را اعتبارسنجی کنند
  • نمونه‌های خاص برگردانند (مثلاً Singleton یا Cache)
  • بسته به ورودی، نوع متفاوتی از شیء بسازند (مثلاً رنگ بر اساس نام یا کد هگز)

در Kotlin، معمولاً متدهای کارخانه‌ای را داخل Companion Object می‌نویسند چون دسترسی به آن‌ها مثل static در جاوا راحت است.

مثال ساده:

class User(val name: String, val age: Int) {
    companion object {
        fun createGuest(): User = User("Guest", 0)
        fun fromMap(map: Map<String, Any>): User =
            User(map["name"] as String, map["age"] as Int)
    }
}

val guest = User.createGuest() // برمی‌گردونه User مهمان
val user = User.fromMap(mapOf("name" to "Ali", "age" to 30))

این متدها به تو قدرت می‌دهند بدون استفاده مستقیم از constructor و با کنترل بیشتر، شیء تولید کنی.

به همین دلیل می‌گن Factory Method = «متدِ سازنده‌ی کارخانه‌ای».

 

چرا Kotlin از شیء واقعی استفاده می‌کند؟
این انعطاف‌پذیری به Kotlin اجازه می‌دهد تا قابلیت‌هایی مانند ارث‌ بری یا پیاده‌ سازی اینترفیس‌ها را روی اعضای سطح کلاس اعمال کند. برای مثال، یک Companion Object می‌تواند یک اینترفیس با متدهایی داشته باشد که توسط متدهای آن پیاده‌سازی شده‌اند.

 

مثال پیشرفته: ذخیره وضعیت بین صفحات (به طور مجازی)

در اندروید، اگرچه مدیریت وضعیت بهینه‌تر از طریق ViewModel یا Saved State انجام می‌شود، اما مفهوم Companion Object به عنوان یک محل ذخیره‌سازی سراسری برای کل برنامه (تا زمانی که برنامه در حافظه است) قابل استفاده است.

فرض کنید می‌خواهیم یک شمارنده ساده را در طول عمر برنامه ردیابی کنیم:

// فرض کنید این کد در پروژه اندروید شماست
class CounterManager {
    companion object {
        // این متغیر تا زمانی که برنامه در حافظه است، مقدار خود را حفظ می‌کند.
        var counter = 0

        fun increment() {
            counter++
            println("Counter incremented to: $counter")
        }
        
        fun reset() {
            counter = 0
        }
    }
}

// در Activity A
fun activityAStartup() {
    println("Starting Activity A")
    CounterManager.increment() // 1
}

// در Activity B (که پس از A باز شده)
fun activityBStartup() {
    println("Starting Activity B")
    CounterManager.increment() // 2
}

// در Activity C (که پس از B باز شده)
fun activityCStartup() {
    println("Starting Activity C")
    println("Current global count is: ${CounterManager.counter}") // 2
    CounterManager.reset()
    println("Count after reset: ${CounterManager.counter}") // 0
}

fun main() {
    activityAStartup()
    activityBStartup()
    activityCStartup()
}
// خروجی اجرای شبیه‌سازی شده:
// Starting Activity A
// Counter incremented to: 1
// Starting Activity B
// Counter incremented to: 2
// Starting Activity C
// Current global count is: 2
// Count after reset: 0

📌 چون Companion Object یک singleton است و به عنوان یک شیء واحد در سطح برنامه شناخته می‌شود، مقدار counter در تمام نقاط کد که به CounterManager.counter دسترسی پیدا می‌کنند، حفظ می‌شود.

 

 

استفاده از Companion Object برای پیاده‌سازی Factory Method

یکی از قدرتمندترین کاربردهای Companion Object، الگوی Factory Method است که به شما امکان می‌دهد سازنده‌های مختلفی برای کلاس خود تعریف کنید.

فرض کنید یک کلاس Color داریم که می‌تواند با نام یا با کد هگز ایجاد شود:

data class Color(val red: Int, val green: Int, val blue: Int) {
    
    // این متدها اعضای Companion Object هستند و می‌توانند سازنده اصلی را فراخوانی کنند.
    companion object {
        
        // سازنده اصلی: ایجاد بر اساس RGB
        fun fromRGB(r: Int, g: Int, b: Int): Color {
            return Color(r, g, b)
        }
        
        // سازنده کمکی: ایجاد از کد هگزادسیمال
        fun fromHex(hexCode: String): Color {
            // منطق تبدیل هگز به RGB (برای سادگی، فرض می‌کنیم ورودی معتبر است)
            val cleanHex = hexCode.removePrefix("#")
            val r = cleanHex.substring(0, 2).toInt(16)
            val g = cleanHex.substring(2, 4).toInt(16)
            val b = cleanHex.substring(4, 6).toInt(16)
            return Color(r, g, b)
        }
    }
}

fun main() {
    // 1. استفاده از سازنده پیش‌فرض (اگر پرایوت نباشد) یا سازنده اصلی که در بالا تعریف شده
    val redColor = Color.fromRGB(255, 0, 0)
    println("Red Color (RGB): $redColor")

    // 2. استفاده از متد کمکی (Factory Method)
    val blueColor = Color.fromHex("#0000FF")
    println("Blue Color (Hex): $blueColor")
}

در این حالت، ما توانسته‌ایم سازنده اصلی کلاس (Color(...)) را به عنوان private تنظیم کنیم (اگرچه در مثال بالا آن را حذف کردیم)، و کنترل ایجاد نمونه‌ ها را کاملاً به متدهای سطح کلاس در Companion Object بسپاریم.

 

Best Practices در استفاده از Companion Objects

استفاده صحیح از Companion Object باعث کد تمیزتر و قابل نگهداری‌ تر می‌شود:

  1. برای ثابت‌ها از const استفاده کن: اگر یک مقدار ثابت، ساده (مانند String، Int، Boolean، Char) است و باید در زمان کامپایل مشخص باشد، حتماً از const val استفاده کنید. این کار عملکرد را بهینه می‌کند و معادل public static final در جاوا است.

    companion object {
        const val MAX_SIZE = 100 // کامپایل-تایم
        val DYNAMIC_URL = "api/v1" // ران-تایم (نمی‌تواند const باشد)
    }
    
  2. از تعریف متدهای Utility پیچیده پرهیز کنید: اگر منطق یک تابع بسیار سنگین است یا نیاز به تست‌ های پیچیده دارد، بهتر است آن را به یک کلاس جداگانه (یا یک object Singleton مجزا) منتقل کنید. Companion Object بهتر است برای متدهای مرتبط مستقیم با نحوه ساخت یا خواص ذاتی کلاس اصلی استفاده شود.
  3. به جای Companion Object برای داده‌های سراسری در اندروید، از معماری مناسب استفاده کنید: همانطور که اشاره شد، Companion Object وضعیت سراسری برنامه را حفظ می‌کند. در پروژه‌های مدرن اندروید، برای نگهداری وضعیت‌ها (مانند داده‌های کاربر یا تنظیمات اپلیکیشن) که نیاز به چرخه عمر دارند، باید از ابزارهایی مانند ViewModel، Repository، یا DataStore استفاده کرد، نه Companion Object، مگر اینکه داده‌ها واقعاً ثابت و غیرقابل تغییر باشند.
  4. اجتناب از ارجاع متقابل غیرضروری: مطمئن شوید که متدهای داخل Companion Object به طور تصادفی نمونه‌ های کلاس را دستکاری نمی‌کنند، مگر اینکه هدف اصلی شما این باشد (مانند شمارنده سراسری).

 

خلاصه: Companion Object در Kotlin جایگزین منعطف‌تر static در Java هست که هم خوانایی و هم قابلیت‌های شیء گرایی بیشتری می‌ده، و برای نگه‌داری داده/منطق مشترک ایده‌آله. این ساختار امکان تعریف متدهای کارخانه‌ای (Factory Methods) و ثابت‌های سطح کلاس را بدون نیاز به نمونه‌سازی فراهم می‌آورد.