آشنایی با sealed class در Kotlin -آموزش به زبان ساده

آشنایی با sealed class در Kotlin -آموزش به زبان ساده
در این پست می‌خوانید:

اولین باری که به sealed class برخوردم، یه عالمه سوال تو ذهنم چرخید:

  • اصلاً این چیه؟ بودن یا نبودنش چه فرقی داره؟
  • چرا باید ازش استفاده کنیم؟ یعنی اگه فقط قراره حالت‌ها رو کنترل کنیم، خب با چندتا متغیر نمی‌شه همین کارو کرد؟
  • یعنی چی “مهر و موم شده”؟ کلاس که قفل نداره!

 

sealed class in Kotlin

 

اشتباه رایجی که توی آموزش این مفاهیم هست اینه که فرض می‌کنیم مخاطب داره با یه مشکلی کلنجار می‌ره و sealed class دقیقاً قراره اون مشکل رو حل کنه.

اما واقعیتش اینه که خیلی وقتا اصلاً اون مشکل هنوز تو ذهن برنامه‌نویس شکل نگرفته. مخصوصاً اگه تازه‌کار باشه یا تو یه تیم کوچیک کار کنه. برای همین، بذار

از پایه شروع کنیم و ببینیم اصلاً چه سناریویی وجود داره کهsealed class توش می‌درخشه.

بخش اول : بیان یک سناریو برای معرفی Sealed class

سناریو: مدیریت وضعیت درخواست شبکه

فرض کن یه اپ داریم که قراره از اینترنت اطلاعات بگیره. وضعیت درخواست می‌تونه یکی از حالت‌های زیر باشه:

  • در حال بارگذاری (Loading)
  • موفقیت‌آمیز (Success)
  • خطا خورده (Error)

حالا اگه بخوایم اینو به شکل چندتا متغیر ساده پیاده کنیم( در پاسخ به پرسش دوم در اول متن )، احتمالاً یه چیزی شبیه این می‌نویسیم:

var isLoading = true

var isSuccess = false

var isError = false

خب، فرض کنیم می‌خوایم وضعیت یک درخواست شبکه رو مدیریت کنیم. برای این کار، می‌تونیم از چند تا متغیر استفاده کنیم:

var isLoading = true // نشان‌دهنده اینکه آیا درخواست در حال بارگذاری است یا نه

 var isSuccess = false // نشان‌دهنده اینکه آیا درخواست با موفقیت انجام شده یا نه

 var isError = false // نشان‌دهنده اینکه آیا درخواست با خطا مواجه شده یا نه 

var data: String? = null // برای ذخیره داده‌هایی که از درخواست موفق دریافت می‌کنیم 

var errorMessage: String? = null // برای ذخیره پیام خطایی که از درخواست ناموفق می‌گیریم

تا اینجا همه چیز ساده به نظر می‌رسه. ما متغیرهایی داریم که وضعیت درخواست رو نشون می‌دن و داده یا پیام خطا رو مدیریت می‌کنن. اما مشکل از اینجا شروع می‌شه که بخوایم این متغیرها رو دستی کنترل کنیم.

چطوری ، این طوری :

if (isLoading){
. . . 
}else if (isSuccess){
. . .
}else if (isError){
. . .
}

حالا در داخل این بلوک های که برای هر شرط نوشته میشه ،

  • فرض کن ببین چه مقدار کد میشه ،
  • مدیریت این ها خیلی سخت میشه
  • ، فکر کن در هر فرگمنت یا اسکرین ، این کد ها رو بخوای بنویسی ، بعد اون ها رو هندل کنی ، چه مقدار این کد ها شلوغ میشه ، کنترل این ها سخت میشه ،
  • بعد به این فکر کن که در آینده بخوای این کد ها رو ویرایش کنی ، فهم این های سخت میشه ،

حالا اگه یه الگو باشه ، که همیشه از اون پیروی کنی چی ؟ 🧐

مشکل اول : اشتباهاتی که ممکنه پیش بیاد (خطای انسانی):

فرض کن توی یه تیم کار می‌کنی و همکارت قراره وضعیت درخواست رو مدیریت کنه. حالا اگر این کار دستی انجام بشه، ممکنه همکارت اشتباهی مرتکب بشه. مثلاً همکارت فکر می‌کنه:

“الان داره داده (data) میاد، پس باید isSuccess رو true کنم.”

اما یادش می‌ره که حالت لودینگ (isLoading) رو خاموش کنه.

این می‌تونه به همچین وضعیتی منجر بشه:

isLoading = true 

isSuccess = true // چی؟ همزمان هم بارگذاریه، هم موفق؟ 😕

اینجا وضعیت درخواست به حالتی غیرمنطقی رسیده:

  • هم می‌گه هنوز در حال بارگذاریه (isLoading = true
  • و هم می‌گه درخواست موفق شده (isSuccess = true).

این تناقض باعث می‌شه برنامه رفتار غیرمنتظره‌ای داشته باشه، چون:

  • چنین حالتی در دنیای واقعی معنی نداره.
  • کدی که با این وضعیت کار می‌کنه، ممکنه به درستی اجرا نشه.

مشکل دوم: اضافه کردن داده‌ها و پیام‌ها

حالا فرض کن می‌خوای اطلاعات بیشتری به هر حالت اضافه کنی. مثلاً:

  • وقتی درخواست موفق شده، باید داده‌ها (data) رو ذخیره کنی.
  • وقتی درخواست خطا داده، باید پیام خطا (errorMessage) رو ذخیره کنی.

برای این کار، باید متغیرهای بیشتری اضافه کنی و همه رو دستی مدیریت کنی. این موضوع باعث می‌شه:

  • کدت شلوغ بشه.
  • احتمال خطاهای انسانی بیشتر بشه.

مشکل سوم : اضافه شدن حالت ، نا خواسته
منظور من از این حالت این که ما سه تا وضعیت داشتیم

  • بارگذاری – loading
  • موفق – sueccess
  • خطا – Error

حالا این وسط ، یکی از برنامه نویس ها در تیمتون ، به تشخیص خودش بیاد ، یه حالت دیگه رو هم در نظر بگیره ، مثلا :

  • قطع اتصال – Disconnected

در ظاهر این کار اشکالی نداره ، و مدیریت صیحیح حالت رو نشون میده ، اما اگه دقت کنی این جا کار دو تا شاخه میشه :

  1. شاخه اول (برنامه نویس ها ) ، که داره 3 تا حالت رو کنترل میکنه .
  2. شاخه دوم (برنامه نویس ها )، داره 4 تا حالت رو کنترل میکنه .

در این جا تیم از هماهنگی در میاد ، چه کنیم که این مشکل ها رو دچار نشیم

الان وقت این رسیده که ، یه روش یا یه الگو برای مقابله با این مشکل بیان کنیم ، که میشه Sealed class

بخش دوم : Sealed class میاد وسط 

ببینیم چطوری می‌تونیم همین سناریو رو خیلی شیک و امن‌تر با sealed class مدیریت کنیم:

ابتدا یه کلاس تشکیل میدیم به این صورت که قبل از class کلمه sealed رو می نویسیم

sealed class NetworkState {

}

بعد میام اون سه تا حالت رو به این کلاس اضافه می کنیم ، به این صورت :

sealed class NetworkState {

    object Loading : NetworkState()

    data class Success(val data: String) : NetworkState()

    data class Error(val message: String) : NetworkState()

}

یه مقدار درباره این کد ها باید توضیحات بدم :

  • sealed class NetworkState –> نام کلاس به همراه پیشوند sealed
  • object –> در کاتلین object به یعنی ” فقط یک نمونه از یک کلاس ” پس یعنی ، از این کلاس یه دونه و ثابت در کل برنامه وجود داره
  •  object Loading : NetworkState() –> این جا ما از NetWorkState داریم ارث بری میکنیم .
  • data class –> یعنی نمونه کلاس ما ، نیاز به دیتا داره و داده هم داره ، پس از پیشوند data کلاس استفاده کردیم .
  • data class Success(val data: String) : NetworkState() –> چون دیتا کلاسه ، دیتا هم وجود داره پس ورودی val data: String هم هست .

حالا چرا برای loading از object استفاده کردیم اما برای Success از data (منظورم پیشوند ها ست ) استفاده کردیم ؟ پاسخ : جواب توی استفاده از اینهاست ، چون loading هیچ داده ای رو انتقال نمیده ، و فقط یه وضعیت رو نشون میده ، پس در استفاده کردن از این من نیاز ندارم که نمونه های متفاوتی رو درست کنم ، همون یه نمونه object بسه اما در دیتا کلاس در حالت ها مختلف من دیتا های مختلف هم دارم پس نیاز دارم که نمونه های مختلفی رو درست کنم برای این منظور از data استفاده میکنم . یه توضحی هم در مورد ارث بری که در این جا (

 object Loading : NetworkState() –>

این جا ما از NetWorkState داریم ارث بری میکنیم . ) بدم :

منظور ما از ارث در این جا این نیست که داریم ویژگی یا تابع های مختلف رو به زیر کلاس انتقال میدیم ، همان طور که میبینید هیچ تابع یا متغییری در کلاس NetwrokState ثبت نشده ، ما این جا از ارث بری در چهارچوب این مفهوم داریم استفاده میکنیم  :” گروه‌بندی منطقی و ساختاری کلاس‌ها ” یعنی چی ؟

کلاس sealed یعنی:

«من یک کلاس پدر هستم، ولی فقط چند فرزند محدود مشخص می‌تونن از من ارث ببرن (یعنی در این چهار چوب باشند ) ، و همه‌شون باید داخل همین فایل تعریف بشن.

خب ، تا این جای کار خیلی کامل و ساده این sealed رو معرفی کردیم :
حالا بریم که از این کلاس استفاده کنیم :

و برای استفاده:

when (state) {

       is NetworkState.Loading -> println(" در حال بارگذاری…")

       is NetworkState.Success -> println(" نتیجه: ${state.data}")

       is NetworkState.Error -> println(" خطا: ${state.message}")

   }

توضیح کد : 

اون state رو از داخل متغییر response که از سمت سرور میاد رو دریافت میکنید ، بعد این response در قالب کلاس sealed بررسی میکنیم که ، میشه استفاده کردن از when  که اگه از روش کلاس sealed استفاده نکنیم ، نمیتونیم از when استفاده کنیم ، میشه همون if else که قبلا بهش اشاره کرده بودم .

مزایا:

  • فقط یک حالت فعال داریم. دیگه امکان خطای “همزمان لود و موفق” وجود نداره.
  • اطلاعات مربوط به هر حالت (مثلاً data یا errorMessage) داخل خودش نگه‌داری می‌شه.
  • کامپایلر موقع when چک می‌کنه که همه حالت‌ها رو بررسی کردی. یعنی اگر یکی رو یادت بره، خودش گیر می‌ده!

خب در کل sealed class یعنی چی؟

فرض کن به کامپایلر می‌گی:

“ببین رفیق، این کلاس فقط همین چند تا حالت رو داره. نه بیشتر، نه کمتر.”

sealed class دقیقاً همینه! یه نوع خاص از کلاس که فقط می‌تونی زیرکلاس‌هاش رو تو همون فایل تعریف کنی. اینطوری هیچ‌کس از جای دیگه نمی‌تونه یواشکی یه حالت جدید بهش اضافه کنه.

چند تا مثال ساده :

sealed class TrafficLight {
    object Red : TrafficLight()
    object Yellow : TrafficLight()
    object Green : TrafficLight()
}

یه مثال دیگه: ماشین 🚗

sealed class CarState {

    object On : CarState()

    object Off : CarState()

    object Broken : CarState()

}

و استفاده  :

fun show(state: CarState) = when (state) {

    CarState.On -> println("ماشین روشنه؛ گاز بده!")

    CarState.Off -> println("ماشین خاموشه؛ استراحت کنیم.")

    CarState.Broken -> println("اوه! تعمیرکار صدا کن.")

}

بخش سوم : جمع بندی و مزایای sealed class 

مزایا  : 

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

یه نکتهٔ مهم:

زیرکلاس‌های sealed class فقط باید داخل همون فایل تعریف بشن. این یعنی کاملاً کنترل‌شده و محلی هستن. کسی از بیرون نمی‌تونه دست‌کاری‌شون کنه.

جمع‌بندی:

sealed class مثل یه قرارداد عمل می‌کنه. یعنی به کامپایلر می‌گی: “اینا تنها حالت‌های ممکن هستن. مراقب باش کسی چیزی اضافه نکنه!”
و این باعث می‌شه:

  • خطاهای انسانی کم بشن
  • کدت قابل نگه‌داری‌تر باشه
  • خیال کامپایلر و خودت راحت باشه که همه حالت‌ها کنترل شدن

و همچنین یه دوره رایگان هم داریم که مطلب sealed class و همچنین مطالب دیگه رو به صورت تصویری توضیح داده شده .

بسیار خب ، مطلب sealed رو خیلی کامل با جزئیات کامل بررسی کردیم ، اگه سوالی داشتی خوشحال میشم بپرسی ، و این مطلب هم تموم شد 🍕

دیدگاه‌ها ۰
ارسال دیدگاه جدید