آموزش Enum و Sealed کلاس‌ها در Kotlin

مقدمهدر این پست به بررسی کلاس‌های Enum و Sealed و در انتها تفاوت این دو با یکدیگر می‌پردازیم. شما هم احتمالا در هنگام کد نویسی به این نکته برخورد کرده‌اید که گاهی اوقات نیاز است مقادیر ثابت و مشخصی را داخل کد تبادل کنید و با دو ساختاری که در Kotlin وجود دارد می‌توانید پیاده‌سازی مناسب‌تری داشته باشید. دوستانی که مقالات قبلی من رو دنبال کردن می‌دونن که هدف من مفهومی‌ترین و آسان‌ترین روش برای یادگیری برنامه‌نویسی هستش، در صورت تمایل حتما من رو دنبال کنید. هر لایک شما برای این پست دلگرمی بیشتر برای ارائه مطالب بهتر می‌باشد ;)کلمه Sealed به معنای مهر و موم شده (بسته) است.Enumاگر بخواهیم چهار جهت جغرافیایی را در کد خود ذخیره کنیم و آن‌ها را به فانکشن‌ها پاس دهیم چه راهی را انتخاب می‌کنید؟ ذخیره به عنوان String را انتخاب می‌کنید؟Github Gistobject Direction { val NORTH = &quotnorth&quot val SOUTH = &quotsouth&quot val EAST = &quoteast&quot val WEST = &quotwest&quot } fun move(direction: String) = when (direction) { Direction.NORTH -> {} Direction.SOUTH -> {} Direction.EAST -> {} Direction.WEST -> {} else -> {} }این کد در نگاه اول هیچ مشکلی اجرایی یا منطقی ندارد، ولی چندین دلیل می‌توان ارائه داد که احتمال خطای کدنویسی و عدم کارکرد دلخواه ایجاد خواهد شد.در صورتی که شخصی به جای north نوشته North یا NORTH و ... را به فانکشن move ارسال کند بلاک else اجرا خواهد شد.بلاک else مفهومی ندارد، دلیلی برای وجود آن نیست. در وحله اول اصلا نیازی به صدا زدن این فانکشن بدون ارسال یکی از چهار جهت تعریف شده وجود ندارد.برای حل این چالش ما می‌توانیم از ساختار Enum استفاده کنیم:Github Gistenum class Direction { NORTH, SOUTH, EAST, WEST; } fun move(direction: Direction) = when (direction) { Direction.NORTH -> {} Direction.SOUTH -> {} Direction.EAST -> {} Direction.WEST -> {} } به هر مقدار درون Enum یک Constant (ثابت) می‌گویند.کلمه Enum مخفف Enumeration به معنای شمارش می‌باشد.حالا که دلیل استفاده از Enum را می‌دانیم، کافی است مفهوم و چند تکنیک را که می‌توانیم با آن‌ها پیاده‌سازی شود بررسی کنیم.ساختار Enumهر مقداری که در داخل یک Enum تعریف می‌شود (مثلا NORTH) یک object که از آن کلاس Enum ارث‌بری کرده محسوب می‌شود.‌در کاتلین ساختار object باعث می‌شود که تنها یک نمونه (instance) از آن کلاس داشته باشیم. در واقع می‌توانیم بگوییم که ساختار object در کاتلین ترکیبی از Singleton و Static جاوا می‌باشد.سینگلتون (Singleton) یکی از الگوهای طراحی (Design Pattern) های پر استفاده می‌باشد که در آن تنها و تنها یک نمونه (شی) از آن کلاس ساخته می‌شود.می‌توانید یک Enum را ساختاری برابر با یک کلاس شامل object در نظر بگیرید:Github Gistenum class Direction { NORTH, SOUTH, EAST, WEST; } abstract class Direction { object NORTH : Direction() object SOUTH : Direction() object EAST : Direction() object WEST : Direction() }چند تکتیک با EnumGithub Gistenum class PaymentMethod(val balance: Double) { CASH(147.50) { override fun printFormattedAmount(): String { return NumberFormat.getCurrencyInstance(Locale.US).format(balance) } }, CREDIT(15.00) { override fun printFormattedAmount(): String { return &quot$balance&quot } }; abstract fun printFormattedAmount(): String }متغییر balance که به عنوان Property کلاس محسوب می‌شود به Constructor هر Constant اعمال شده است.فانکشن abstract به نام printFormattedAmount هم در هر Constant بازنویسی (Override) شده است.Github Gistfun main() { println(&quot+-------------------+&quot) PaymentMethod.values().forEach { medthod -> println(&quotPayment Method: ${medthod.name} - - - Balance: ${medthod.balance}&quot) } } enum class PaymentMethod(val balance: Double) { CASH(147.50), CREDIT(15.00) }فانکشن values() تمامی Constant ها را در قالب یک Array به شما می‌دهد.پراپرتی name نام String ای آن Constant را برمی‌گرداند.Github Gistfun main() { val creditPayment = PaymentMethod.valueOf(&quotCREDIT&quot) println(paymentMethod.balance) enumValues<PaymentMethod>().forEach { method -> println(method) } val cashPayment: PaymentMethod = enumValueOf<PaymentMethod>(&quotCASH&quot) println(paymentMethod.name) } enum class PaymentMethod(val balance: Double) { CASH(147.50), CREDIT(15.00) }در صورتی که با نام String ای نیاز داشتید Constant را بگیرید از فانکشن valueOf() استفاده کنید. این فانکشن در صورت عدم وجود IllegalArgumentException ایجاد می‌کند و باعث کرش می‌شود.می‌توانید از Kotlin Extension Function های enumValueOf و enumValues هم استفاده کنید.Sealed Classesمی‌توان Sealed Class ها را Enum های پیشرفته معرفی کرد. به دلیل object بودن هر مقدار (Constant) داخل Enum نمی‌توانید چندین نمونه از آن‌ها بگیرید. فرض کنید دو کاربر داریم که برای هر کدام می‌خواهیم یک روش پرداخت با موجودی متفاوت بسازیم:Github Gistfun main() { val user1 = PaymentMethod.CASH val user2 = PaymentMethod.CASH user1.balance = 2_000.00 println(&quot+-----------------------------------+&quot) println(user1.balance) //Prints 2000.0 println(user2.balance) //Also prints 2000.0 } enum class PaymentMethod(var balance: Double) { CASH(147.50), CREDIT(15.00) }برای حل این مشکل از Sealed Class ها استفاده می‌کنیم که دارای ویژگی‌های زیر می‌باشند:یک کلاس Sealed را تنها اعضای آن می‌توانند ارث بری (Extend) کنند. کلاس Sealed یک کلاس abstract می‌باشد که اعضای آن به عنوان Concrete کلاس شناخته می‌شوند.کلاس abstract کلاسی است که نمی‌توان از آن شی (Object) ساخت و به همین دلیل کلاسی که از آن ارث بری می‌کند و فانکشن‌ها و پراپرتی‌های abstract (بدون بدنه) آن را پیاده سازی می‌کند Concrete گفته می‌شود.Github Gistfun main() { val user1 = PaymentMethod.CASH(2_000.0) val user2 = PaymentMethod.CASH(9_000.0) println(&quot+---------------------+&quot) println(user1.amount) println(user2.amount) } sealed class PaymentMethod(var amount: Double) { class CASH(cashAmount: Double) : PaymentMethod(cashAmount) class CREDIT(credit: Double) : PaymentMethod(credit) data class Crypto(val name: String, val balance: Double) : PaymentMethod(balance) //object Gift : PaymentMethod(0.0) }در یک Sealed Class می‌توانید هر نوع ساختاری (data class, class, and object) را پیاده‌سازی کنید.در نسخه‌های جدیدتر کاتلین نیازی نیست که کلاس ارث برنده داخل بدنه Sealed Class باشد و کافی است که هر دو داخل یک فایل قرار داشته باشند.