تنها یک شیء
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 7 دقیقه

تنها یک شیء

در جاوا از کلمه کلیدی static برای مشخص کردن متدها و خصوصیات متعلق به یک شیء استفاده می‌شود، نه به عنوان نمونه‌ای از یک شیء. کلمه کلیدی static همچنین برای ایجاد Singletones یکی از پرکاربردترین الگوهای طراحی استفاده می‌شود. Singletones به شما کمک می‌کند تا یک نمونه واحد از یک شیء را ایجاد کنید، که توسط سایر اشیاء قابل دسترس و اشتراک است.

کاتلین یک روش ظریف‌تر برای مقابله با این مسئله دارد. برای پیاده سازی الگو طراحی Singleton می‌توانید از کلمه کلیدی object استفاده کنید. در ادامه با ما همراه باشید تا تفاوت اجرای Singleton در جاوا و کاتلین را بدانید، چگونه می‌توان در کاتلین Singleton را بدون استفاده از کلمه کلیدی static ایجاد کرد(اسپویلر این با استفاده از کلمه کلیدی object بدست می‌آید).

ابتدا بگذارید کمی به عقب برگردیم و بفهمیم که چرا ما به یک نمونه واحد از یک شیء نیاز داریم.

Singleton چیست؟

Singleton یک الگوی طراحی است که تضمین می‌کند که یک کلاس فقط یک نمونه داشته باشد و یک نقطه دسترسی سراسری به شیء را فراهم می‌کند. الگوی singleton بخصوص برای اشیائی که باید بین قسمت‌های مختلف برنامه شما به اشتراک گذاشته شود و منابعی که ایجاد آن‌ها گران است بسیار مفید است.

Singleton در جاوا

برای اطمینان از اینکه یک کلاس فقط یک نمونه دارد، باید چگونگی ایجاد شیء را کنترل کنید. برای ایجاد کلاسی تنها با یک نمونه، سازنده را private کنید و یک مرجع static عمومی از شیء ایجاد کنید. در حالی که این کار را انجام می‌دهید، شما واقعاً نمی‌خواهید singleton را در هنگام راه‌اندازی ایجاد کنید زیرا singleton برای اشیائی استفاده می‌شود که گران قیمت هستند. برای دست‌یابی به این هدف، یک متد static ارائه دهید که بررسی می‌کند اگر شیء ایجاد شده است، شیء ایجاد شده را برگرداند در غیر این صورت سازنده را فراخوانی می‌کند و یک نمونه جدید را برمی‌گرداند. 

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public class Singleton{
    private static Singleton INSTANCE;
    private Singleton(){}
    public static Singleton getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    private int count = 0;
    public int count(){ return count++; }
}

کد بالا خوب به نظر می‌رسد اما یک مشکل مهم دارد. این کد thread-safe نیست. هروقت که یک thread در حال اجرای کدهای داخل بلاک if است، thread دیگر یک نمونه دیگر می‌سازد.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public class Singleton{
    private static Singleton INSTANCE;
    private Singleton(){}
    public static Singleton getInstance(){
        if (INSTANCE == null) {                // Single Checked
            synchronized (Singleton.class) {
                if (INSTANCE == null) {        // Double checked
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
    private int count = 0;
    public int count(){ return count++; }
}

برای رفع مشکل بالا، می‌توانید از double checked locked استفاده کنید. با این کار اگر نمونه تهی باشد کلمه کلیدی synchronized یک قفل ایجاد می‌کند و بررسی دوم اطمینان می‌دهد که نمونه هنوز تهی است. اگر نمونه تهی است سپس singleton را ایجاد کنید. با این حال، این کافی نیست و نمونه آن نیز باید volatile باشد. کلمه کلیدی volatile به کامپایلر می‌گوید که یک متغییر ممکن است با اجرای همروند threadها به صورت همزمان تغییر کند.

همه این موارد منجر به ایجاد کدهای تکراری زیادی می‌شود که شما باید هربار که به singleton نیاز دارید تکرار کنید. از آنجا که این کد برای چنین کار ساده‌ای بسیار پیچیده است، بیشتر اوقات برای ایجاد singleton در جاوا از enumها استفاده می‌شود.

Singleton در کاتلین

حال اجازه دهید نگاهی به کاتلین بیاندازیم. کاتلین متدها و فیلدهای static ندارد، بنابراین چگونه می‌توانیم singleton را در کاتلین بسازیم؟

در واقع android studio/InteliJ در درک آن به ما کمک می‌کند. وقتی کد singleton را در جاوا به کاتلین تبدیل می‌کنید، تمام خصوصیات و متدهای static به یک companion object منتقل می‌شود.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class Singleton private constructor() {
    private var count = 0
    fun count(): Int {
        return count++
    }

    companion object {
        private var INSTANCE: Singleton? = null// Double checked

        // Single Checked
        val instance: Singleton?
            get() {
                if (INSTANCE == null) { // Single Checked
                    synchronized(Singleton::class.java) {
                        if (INSTANCE == null) { // Double checked
                            INSTANCE =
                                Singleton()
                        }
                    }
                }
                return INSTANCE
            }
    }
}

کد تبدیل شده همانطور که انتظار می‌رود کار می‌کند اما ما می‌توانیم آن را ساده‌تر کنیم. برای ساده‌تر کردن کد، کلیدواژه constructor و companion را از object جدا کنید. تفاوت بین object و companion objects در ادامه مورد بررسی قرار می‌گیرد.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

object Singleton {
    private var count: Int = 0

    fun count() {
        count++
    }
}

هنگامی که می‌خواهید از متد count() استفاده کنید، می‌توانید از طریق شیء singleton به آن دسترسی داشته باشید. در کاتلین object کلاس خاصی است که فقط یک نمونه دارد. اگر به جای کلاس یک کلاس با کلمه کلیدی object ایجاد کنید، کامپایلر کاتلین سازنده را private می‌کند، یک مرجع static برای شیء ایجاد می‌کند و در یک بلاک static مرجع را مقداردهی می‌کند.

بلاک static فقط یکبار زمانی که فیلد static اولین بار در دسترس قرار می‌گیرد فراخوانی می‌شود. JVM بلاک‌های static را به روشی مشابه بلاک synchronized مدیریت می‌کند، حتی اگر آن‌ها کلمه کلیدی synchronized را نداشته باشند. هنگامی که این کلاس singleton مقداردهی می‌شود، JVM قفل روی بلاک synchronized را بدست می‌رود و دسترسی  thread دیگر به آن غیرممکن می‌شود. وقتی قفل آزاد شد، نمونه singleton از قبل ایجاد شده است بنابراین بلاک static مجدداً اجرا نخواهد شد. این تضمین می‌کند که تنها یک نمونه از singleton وجود دشته باشد، که قرارداد singleton را انجام می‌دهد. به علاوه این شیء هم از نوع thread-safe است و هم به صورت lazily اولین بار که به آن دسترسی پیدا می‌کنید ایجاد می‌شود.

بیایید نگاهی به بایت کدهای decompile شده کاتلین بیاندازیم تا بفهمیم که در پشت چه کاری انجام می‌شود.

برای بررسی بایت کد یک کلاس به مسیر Tools > Kotlin > Show Kotlin Bytecode بروید. پس از نمایش بایت کد کاتلین، روی دکمه Decompile کلیک کنید تا کدهای decompile شده جاوا آشکار شود.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public final class Singleton {
   private static int count;
   public static final Singleton INSTANCE;
   public final int getCount() {return count;}
   public final void setCount(int var1) {count = var1;}
   public final int count() {
      int var1 = count++;
      return var1;
   }
   private Singleton() {}
   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}

با این حال object با محدودیت همراه است. تعیین یک شیء نمی‌تواند سازنده باشد یعنی که آن‌ها نمی‌توانند پارامترهایی را بگیرند. حتی اگر این کار را می‌کردند، پاس دادن یک پارامتر غیرممکن خواهد بود زیرا پارامتر غیر static منتقل شده در سازنده از بلاک static قابل دسترس نیست.

نکته: بلاک static مقداردهی اولیه، مانند سایر متدهای static فقط می‌توانند به خصوصیات static یک کلاس دسترسی داشته باشند. بلاک‌های static قبل از سازنده‌ها فراخوانی می‌شوند، بنابراین امکان دسترسی به خصوصیات یک شیء یا پارامترهای منتقل شده در سازنده وجود ندارد.

Companion Object

Companion object مشابه object است. Companion object همیشه در یک کلاس تعیین می‌شود و با استفاده از شیء میزبان می‌توان به ویژگی‌های آن‌ها دسترسی داشت. Companion object نیازی به اسم ندارد.اگر companion object اسم داشته باشد، صدا زننده می‌تواند با استفاده از نام شیء همراه به اعضاء دسترسی پیدا کند.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

class SomeClass {
    //…
    companion object {
        private var count: Int = 0
        fun count() {
            count++
        }
    }
}
class AnotherClass {
    //…
    companion object Counter {
        private var count: Int = 0
        fun count() {
            count++
        }
    }
}
// usage without name
SomeClass.count()
// usage with name
AnotherClass.Counter.count()

به عنوان مثال ما در اینجا companion objectهای مشابه با اسم و بدون اسم را داریم. هر صدا زننده می‌تواند به متد count() در کلاس SomeClass دسترسی پیدا کند، دقیقاً مانند اینکه عضو static کلاس SomeClass باشد. از طرف دیگر هر صدا زننده می‌تواند به متد count() با استفاده از Counter مانند یک عضو static در کلاس AnotherClass دسترسی داشته باشد.

Companion object به یک کلاس داخلی با سازنده خصوصی decompile می‌شود. کلاس میزبان، کلاس داخلی را از طریق یک سازنده synthetic مقداردهی می‌کند، که فقط به آن امکان دسترسی دارد. کلاس میزبان ارجاع عمومی companion object را نگه می‌دارد که از سایر کلاس‌ها قابل دسترس است.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

public final class AnotherClass {
    private static int count;
    public static final AnotherClass.Counter Counter = new AnotherClass.Counter((DefaultConstructorMarker)null);

    public static final class Counter {
        public final void count() {
            AnotherClass.count = AnotherClass.count + 1;
        }
        private Counter() { }
        // $FF: synthetic method
        public Counter(DefaultConstructorMarker $constructor_marker) {
            this();
        }
    }
    
    public static final class Companion {
        public final void count() {
            AnotherClass.count = AnotherClass.count + 1;
        }
        private Companion() {}
    }
}

عبارت object

تا کنون کلید واژه object را که در تعیین شیء مورد استفاده قرار می‌گرفت، مشاهده کردیم. کلمه کلیدی object را در عبارات object نیز می‌توان استفاده کرد. هنگامی که به عنوان عبارت استفاده می‌شود، کلمه کلیدی object به شما کمک می‌کند که اشیاء و کلاس‌های داخلی anonymous ایجاد کنید.

بیایید بگویم که شما برای نگه داشتن مقادیر به یک شیء موقت نیاز دارید. می‌توانید شیء خود را با مقادیر مورد نظر تعیین کنید وبعداً به آن‌ها دسترسی پیدا کنید.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

val tempValues = object : {
    var value = 2
    var anotherValue = 3
    var someOtherValue = 4
}

tempValues.value += tempValues.anotherValue

در کد تولید شده، به یک کلاس جاوای anonymous ترجمه شده، که با <undefinetype> مشخص می‌شود تا شیء anonymous را با getter و setterهای تولید شده ذخیره کند.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

<undefinedtype> tempValues = new Object() {
    private int value = 2;
    private int anotherValue = 3;
    private int someOtherValue = 4;

    // getters and setters for x, y, z
    //...
};

کلمه کلیدی object همچنین به شما کمک می‌کند تا کلاس‌های anonymous را بدون نوشتن کدهای تکراری ایجاد کنید. می‌توانید از عبارت object استفاده کنید و کامپایلر کاتیلن برای آن یک wrapper برای ایجاد کلاس anonymous تعیین می‌کند.

<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->

val t1 = Thread(object : Runnable {    
    override fun run() {
         //do something 
    }
})
t1.start()

//Decompiled Java
Thread t1 = new Thread((Runnable)(new Runnable() {
     public void run() {
     
     }
}));
t1.start();

کلید واژه object به شما کمک می‌کند تا singletonهای thread-safe، اشیاء anonymous و کلاس‌هایی با کد کمتر ایجاد کنید. با استفاده از object و companion object کاتلین برای دستیابی به عملکرد یکسانی که توسط کلمه کلیدی static ارائه می‌شود، تمام کدها را تولید می‌کند. به علاوه می‌توانید از عبارت object برای ایجاد اشیاء و کلاس‌های anonymous بدون داشتن کد تکراری استفاده کنید.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@pouryasharifi78
پوریا شریفی
توسعه‌دهنده‌ی اندروید

ابتدا که با برنامه‌نویسی آشنا شدم به سمت php و طراحی وب رفتم، بعد از اون به توسعه‌ی اندروید علاقه‌مند شدم و تقریبا ۲ سال است که مشغول به برنامه‌نویسی اندروید هستم، همچنین عاشق یادگیری چیزهای جدید هستم.

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

پوریا شریفی

توسعه‌دهنده‌ی اندروید