مفهموم Delegation و پیاده سازی در کاتلین

در این نوشته سعی میکنم مفهوم Delegation رو توضیح بدم. بعد از اون بگم کاتلین چطور پیاده سازیش کرده.

مفهومی که شاید برای کسی که اسمش رو میشنوه یکم سخت به نظر برسه ولی چند دقیقه بعد میبینیم که این طور نیست.

نمایندگی یا Delegation

فرض کنید ما میخواهیم یک کلاس B داشته باشیم که عملکرد های یک کلاس دیگه مثل A رو داشته باشه.

2 روش برای انجام این کار وجود داره.

1. روش اول : استفاده از Inheritance یا وراثت. با این کار یک رابطه ی غیر قابل تغییر بین کلاس های A و B ایجاد میشه.
2. روش دوم: استفاده از Delegation یا واگذاری مسئولیت : یک شی از نوع A (یا کلاس های فرزندش) رو به شی ای از کلاس B پاس میدیم. به این ترتیب کل کارهایی رو که A میتونسته انجام بده B هم میتونه انجام بده.

پس میشه Delegation رو اینجوری تعریف کرد: واگذار کردن کارها از یک Object به Object دیگه.

به صورت کلی میشه Delegation رو به 2 روش پیاده سازی کرد.

  • پیاده سازی با زبان های شی گرا
  • پیاده سازی در سطح زبان (مثل Kotlin)

پیاده سازی با زبان های شی گرا

همون طوری که از نامش هم مشخص هست وابسته به زبان خاصی نمیشه و هر زبان شی گرایی میتونه پیاده سازیش بکنه. این مثال رو ببینید.

class Logger {
    void logAll() {
        print("Logger.logAll()");
    }
}

class Controller {
    private Logger logger;

    public Controller(Logger logger) {
        this.logger = logger;
    }

    void logAll() {
        logger.logAll();
    }
}

class Delegation {
    public static void main(String[] args) {

        Logger logger = new Logger();
        Controller controller = new Controller(logger);

        controller.logAll();           // ==> Logger.logAll()
    }
}

این مثال به زبان جاواست. مشخص هم هست که از هیچ ویژگی خاصی توی جاوا استفاده نکردیم.

کلاس Controller، یکی از عملکردهاش رو از کلاس Logger گرفته که در اینجا نوشتنِ لاگ هست. اینجا با صدا زدن متد ()logAll از Controller کد معادلش توی Logger اجرا میشه.

برای کامل تر شدن پیاده سازی میتونیم به جای کلاس Logger یک اینترفیس Logger داشته باشیم که کلاس Controller و کلاس هایی مثل FileLogger یا ConsoleLogger اون رو پیاده سازی بکنن.

کد زیر رو ببینید.

interface Logger {
    void logAll();
}

class FileLogger implements Logger {
    @Override
    public void logAll() {
        File logFile = new File("/path/to/logFile.txt");
        FileWriter fileWriter = new FileWriter(logFile);
        fileWriter.write("FileLogger.logAll()");
    }
}
class ConsoleLogger implements Logger {
    @Override
    public void logAll() {
        print("ConsoleLogger.logAll()");
    }
}

class Controller implements Logger {
    private Logger logger;

    public Controller(Logger logger) {
        this.logger = logger;
    }

    @Override
    public void logAll() {
        logger.logAll();
    }
}

class Delegation {
    public static void main(String[] args) {

        Logger fileLogger = new FileLogger();
        Logger consoleLogger = new ConsoleLogger();

        new Controller(fileLogger).logAll();
        new Controller(consoleLogger).logAll();
    }
}

میبینید که برای پیاده سازی یک Delegation ساده توی جاوا چند خط کد باید زده بشه. فرض کنید اگر logger چندتا متد دیگه هم داشت یا توی Controller قرار بود چند تا delegation دیگه هم اضافه بشه ، اونوقت چقدر کد توی controller برای پیاده سازی باید اضافه میشد.


به همین دلیل بعضی از زبان ها اومدن این کار رو داخل خود زبان انجام دادن و برنامه نویس رو برای زدن کدهای تکراری و خسته کننده معاف کردن.

پیاده سازی در سطح زبان

بر خلاف جاوا ، کاتلین از Delegation به عنوان یکی از ویژگی های زبان پشتیبانی میکنه.

قسمت Controller و Main برنامه ی کد بالا برای کاتلین به این شکل میشه.

class Controller(logger: Logger) : Logger by logger

fun main() {
    Controller(ConsoleLogger()).logAll()
}

میبینید که برای اضافه کردن عملکرد Logger به کلاس Controller فقط کافیه از `by` بعد از تعریف کلاس استفاده کنیم. که هم خیلی راحت هست و هم خیلی خلاصه.

از ویژگی های خوب این روش اینه که دیگه لازم نیست خودمون اینترفیس Logger رو برای کلاس Controller پیاده سازی کنیم. چون این کار توسط کامپایلر در زمان کامپایل انجام میشه.

اگر هم در آینده قرار شد Controller ما عملکرد دیگه ای هم بهش اضافه بشه، مثلا عملکرد Repository رو هم داشته باشه، تفاوتی که در این حالت خواهیم داشت اضافه کردن یه عبارت by دیگه به شکل زیر خواهد بود.

class Controller(logger: Logger, repository: Repository) : Logger by logger, Repository by repository

برای این که یک مثال قابل اجرا توی مرورگرتون ببینید میتونید روی این لینک کلیک کنید.

https://pl.kotl.in/_c09AA32u

اما مبحث Delegation توی کاتلین اینجا تموم نمیشه. به این چیزی که صحبت کردیم Class Delegation گفته میشه .کاتلین یه Delegation دیگه هم داره با عنوان Delegated Properties


Delegated Properties

سعی میکنم با یک مثال توضیح بدم.

فرض کنید یه مقداری داریم که می خواهیم فقط یک بار محاسبه بشه و بعد از اون اگر دوباره جای دیگه ای بهش نیاز داشتیم همون مقداری که محاسبه شده، برگرده. به عبارت دیگه متغییر ما Lazy باشه.

کدی که براش میزنیم شبیه اینه :

var lazyValue: String? = null
    get() {
        if (field == null) {
            field = "lazyValue"
        }
        return field
    }

ولی با کمک کاتلین و استفاده از مفهوم Property Delegation میتونیم این کار رو به زبان واگذار کنیم.

val lazyValue: String by lazy {
    "lazyValue"
}

مثال قابل اجراش رو هم اینجا براتون گذاشتم.

https://pl.kotl.in/tmFUu9mqv

فکر میکنم این معروفترین Delegate توی کاتلین باشه. چند تا مورد دیگه هم هستن که توسط کاتلین تعریف شدن. که سعی میکنم به صورت خیلی خلاصه تعریف شون کنم.

  • Observable
  • Vetoable
  • NotNull

Observable

مشخص هست که برای پیاده سازی پترن observer ازش میتونیم استفاده کنیم.

var observable by Delegates.observable("initialValue") { property, oldValue, newValue ->
    println(newValue)
}

کد قابل اجراش رو اینجا ببینید.

https://pl.kotl.in/tmFUu9mqv

Vetoable

از vetoable زمانی استفاده میشه که قبل از اینکه مقداری رو set کنیم شرط های ما رو باید داشته باشه. مثلا میگیم اگر مقدار جدید اومد در صورتی قبولش کن که بزرگتر از 10 باشه.

var vetoable by Delegates.vetoable(100, { property, oldValue, newValue ->
    newValue > 10
})

NotNull

var notNull by Delegates.notNull<String>()

با این Delegate دیگه در زمان کامپایل لازم نیست از عملگر (?.) Safe Access بعد از نام متغییر استفاده کنیم. فقط باید حواسمون باشه اگر قبل از set کردن مقدارش ، بخواهیم ازش استفاده کنیم، خطای IllegalStateException میگیریم.

این ها مواردی بود که خود کاتلین به صورت پیش فرض پشتیبانی می کرد. جدای از این موارد خودتون هم میتونید Delegation property های خودتون رو تعریف کنید.


جمع بندی

کاتلین سعی کرده تا برنامه نویس ها علاوه بر واضح بودن، کدهای خلاصه تری هم بزنن. به نظر میرسه خیلی هم خوب موفق شده. اینجا هم دیدیم که Delegation باعث شد تا با تعداد خط های کمتر، کدی بنویسیم که هم Reusable باشه و هم Clean.

خیلی خوشحال میشم در صورتی که این نوشته رو دوست داشتید ❤️ کنید یا نظرتون را از طریق کامنت برام بنویسید.