آموزش کلاس های Abstract در کاتلین توضیح ساده و روان


داستان این کلاس های Abstract از این جا شروع شد که در حال خواندن آموزش پیاده سازی دیتابیس Room در اندروید بودم که به یه قسمتی از کد برخورد کردم :
@Database(entities = [UserEntity::class], version = 1) abstract-->( این کلمه که در این جا هست) class UserDatabase : RoomDatabase() { abstract fun dao(): UserDao }
که در کد بالا مشخص کردم ،کلمه ای که به عنوان abstract در اول خط در قبل از تعریف نام کلاس استفاده شده است ، این سوال این جا پیش میاد
1- این چیه ؟
2- چرا استفاده می کنیم ؟ اصلا بود و نبودش چه فرقی به حال ما داره ؟
برای پاسخ دادن به این سوال ها من ابتدا باید چندتا مفهوم رو بدونیم
- abstract:
کلمه “abstract” در زبان انگلیسی به معنای “انتزاعی”، “مجرد”، “خلاصه” یا “غیرملموس” است. وقتی در مورد یک مفهوم abstract صحبت میکنیم، یعنی داریم به چیزی اشاره میکنیم که واقعی و ملموس نیست، بلکه یک ایده یا مفهوم کلی است. - انتزاعی :
مفهوم ای که نمیشه اون رو به صورت فیزیکی وقابل لمس درک کرد و به صورت ذهنی است و در دنیایی برنامه نویسی اگر ما کلاس های خودمون رو به صورت یه جنس فیزیکی قابل لمس فرض کنیم ، آن الگو یا نقشه ای که این کلاس ها با اون ساخته شدن رو میشه انتزاع تعریف کرد - مجرد :
مجرد به طور کلی به تصورات و مفاهیمی اشاره دارد که خالص و بدون هر گونه پیوستگی یا وابستگی خاصی هستند.(تا این جا که شد کلاس ادبیات فارسی)
1-کلاس های Abstract چیه ؟
از مفاهیم ای که در بالا اشاره کردیم ، می تونیم کلاس های نتزاعی رو به این صورت تعریف کنیم :
- یه جور کلاسهایی هستن که نمیشه ازشون مستقیم شیء ساخت. یعنی نمیتونی بگی “یه دونه از این کلاس abstract بساز”.
- یه سری متدهای abstract دارن که فقط اسم و امضاشون مشخصه، ولی خودِ کارِشون توش تعریف نشده.(تعریف و امضا بدون پیادهسازی داخلی در کلاس )
- این متدها رو کلاسهای بچهشون (subclass) باید پیادهسازی کنن.
خب، با توجه به تعریف های بالا ،می خوایم تک تک این ها رو بررسی کنیم :
1- نمی تونی از کلاس های انتزاعی (abstract) شی بسازی !
صبـــــــر کن !
باز این جا سوال پیش اومد ، این جمله بالا که نمیشه از این کلاس ها شی ساخت یعنی چی ؟
باز به مقدار عمیق تر بشیم :
دنیایی کد و برنامه نویسی چیز قابل لمس فیزیکی نیست ، یعنی این که من یه کلاس ای بسازم و بتونم اون رو با دست لمس کنم ، ما همچنین چیزی نداریم ،
اما زمانی که ، داریم می گیم یه کلاس ساخته شد ، یا یک شی از این کلاس ساخته شد ،یعنی در واقع، فضایی در حافظه (RAM) به آن اختصاص داده شده تا این حافظه برای نگهداری اطلاعات مربوط به کلاس یا شیء (مانند متغیرها و توابع) استفاده شود.
بسیار خب ، اگه این کلاس ساخته نمیشه پس کجا ثبت میشه یا این اصلا کجاست ؟ جواب ، در حافظه (RAM) :)) ، گیج کننده شد نه ؟
اینجا نیاز به مثال داریم :
ما می خوایم یه پل بسازیم ، طرح و نقشه و محاسبات ، مشخصافت فنی رو نیاز داریم ،
- قبل از ساخت پل: طرح پل در یک دفتر مهندسی یا در کامپیوتر ذخیره شده . در این حالت، پل به صورت فیزیکی وجود نداره ، اما “طرح” آن وجود داره .
- هنگام ساخت پل: مهندسان از روی طرح، پل را میسازند. در این مرحله، طرح به یک واقعیت فیزیکی تبدیل میشه (پل واقعی )
در مورد کلاس های انتزاعی هم به این صورته که طرح کلاس در حافظه هست ، اما خود کلاس در حافظه نیست ، خیلی در این توضیحات در مورد کلمه طرح تاکید داشتم ، این تاکید از این جا میاد که این کلاسهای انتزاعی یک نوع الگو و نقشه برای ساخت کلاس های دیگه است پس با این وجود وقتی کلاسی وجود نداره شما نمی تونی ازش شی بسازی !
2-تعریف و امضا بدون پیادهسازی داخلی در کلاس
طبیعه ! زمانی که شما الگو یا طرح اولیه در دست داری ، نیازی نیست که تمام جزئیات عملیات در همون لحظه ابتدا کار مشخص بشه و پیاده کنی پس در این مرحله تمرکز بر روی طرح کلی وساختار کلی است و پیاده سازی این طرح والگو در آینده و مرحله های بعدی اتفاق می افته .
3- این متدها در زیرکلاس ها (subclass) باید پیادهسازی شوند.
همون طوری که در مرحله قبل گفته شد ، شما طرح رو مشخص کردین ، حالا در کلاس های فرزند ، می تونی عملیات های را که قصد داری رو انجام بدی .(حالا در ادامه مثال داریم )
2- چرا این کلاس های انتزاعی (abstract) استفاده میکنیم ؟
دلیل استفاده کردن این کلاس ها رو به صورت طرح واره این جا میارم اما برای درک بهتر باید مثال بررسی کنیم که در محیط عملی فایده این عمل دستمون بیاد ،
- طراحی تمیز: فرض کن میخوای یه سری کارای مشترک رو واسه چند تا کلاس دیتابیس تعریف کنی. کلاس abstract کمک میکنه این کارا رو یه جا بنویسی و بقیه کلاسا فقط جزئیات خودشونو پیادهسازی کنن.
- جلوگیری از شیءهای الکی: چون نمیشه از کلاس abstract شیء ساخت، دیگه شیءهای اضافی و بیخود تولید نمیشه.
- تست و نگهداری راحتتر: وقتی کارا جدا جدا باشن، تست کردن و درست کردن اشکالات کد خیلی راحتتره.
مثال
بسیار خب ، الان میخوام مثالی رو بررسی کنیم که سوال اصلی از همون جا درست شد (در ادامه این مثال نام کلاس دیتابیس عوض شده که در کلیت موضوع فرقی نمیکنه )، استفاده این کلاس های انتزاعی (abstract) در دیتابیس ، کد زیر همان کدی است که در اول این مطلب آمده است :
@Database(entities = [TaskEntity::class], version = 1, exportSchema = false) abstract class DataBaseTask : RoomDatabase() { abstract fun DaoTask(): DaoTask }
حالا می خوام ابتدا این مثال رو با کلاسی مثال بزنم که از انتزاعی (abstract) استفاده نمیکند و ببینیم که در صورت استفاده کردن کلاس های معمولی ، چه اتفاقی می افته ؟
مثال :
فرض کنید ما یک کلاس به نام DatabaseManager داریم که بهطور مستقیم پیادهسازی شده و شامل متدهایی برای اضافه و دریافت اطلاعات از جدول است:
class DatabaseManager { fun addTask(task: TaskEntity) { // کدها برای درج اطلاعات در دیتابیس } fun getTask(): List<TaskEntity> { // کدهای برای دریافت اطلاعات از دیتابیس return listOf() } }
همان طور که در بالا می بینید شکل کلی کلاس به این صورت خواهد شد حالا در ادامه از این کلاس DatabaseManager می خواهیم استفاده کنیم چون این کلاس یه نوع کلاس مدیر، پدر ، بالارده است و زیر مجموعه ها میان از این کلاس استفاده میکند ،
در مثال ما نیاز داریم که اطلاعات زیر و ثبت کنیم
- اطلاعات سفارش
- اطلاعات مشتری
کلاس برای مدیریت اطلاعات سفارش درست می کنیم :
class OrderManager { fun addOrder(order: Order) { // کد برای اضافه کردن سفارش به دیتابیس // یه لحظه فکر کن که این جا کلمه // Add // رو نذاشته باشی ، در آینده باید کلی انرژی و وقت صرف کنی که منطق این کد رو درک کنی و بفهمی که چه کاری رو انجام میده println("سفارش ${order.id} اضافه شد.") } fun getOrder(orderId: Int): Order? { // کد برای دریافت اطلاعات سفارش از دیتابیس println("دریافت اطلاعات سفارش با آیدی $orderId") return Order(orderId, "سفارش $orderId") // برای مثال } }
همان طور که کد اشاره کردم ، امکان اشتباه کردن برای همه برنامه نویس های وجود داره ، و فرض می کنیم اشتباه ما در این جا فراموش کردن کلمه Add می باشد که در نام تابع نشان دهده منطق کاری این تابع هست (یعنی این تابع چی کار میکنه )، در آینده زمانی که به این کد بر می گردیم خیلی طول می کشه که منطق این کد رو بفهمم و بدونیم که چه کاری رو انجام میده ، یا شاید امکان داره که مجبور بشیم بخش های از این کد ها رو دوباره باز نویسی کنیم .
حالا در ادامه
کلاس برای مدیریت اطلاعات مشتری درست می کنیم :
// کلاس برای مدیریت اطلاعات مشتری class CustomerManager { fun addCustomer(customer: Customer) { // کد برای اضافه کردن مشتری به دیتابیس println("مشتری ${customer.name} اضافه شد.") } fun getCustomer(customerId: Int): Customer? { // کد برای دریافت اطلاعات مشتری از دیتابیس println("دریافت اطلاعات مشتری با آیدی $customerId") return Customer(customerId, "مشتری $customerId") // برای مثال } }
این کلاس هم مشابه کلاس اطلاعات سفارش در نظر گرفته شده است اما این جا در مورد اشتباهات مثال نزده ام ، چون مثال قبل دلیل کافی و فرضیه کافی برای این اشتباه ها رو نمایش میده .
الان در این مثال وقت این است که بخواهیم از این کلاس ها استفاده بکنیم :
data class Order(val id: Int, val details: String) data class Customer(val id: Int, val name: String) fun main() { val orderManager = OrderManager() val customerManager = CustomerManager() orderManager.addOrder(Order(1, "سفارش لپتاپ")) customerManager.addCustomer(Customer(1, "رضا")) val order = orderManager.getOrder(1) val customer = customerManager.getCustomer(1) println("سفارش: $order") println("مشتری: $customer") }
ابتدا دوتا شی از کلاس های اطلاعات مشتری و اطلاعات سفارش ساختم و با استفاده از کلاس های مدل ما که نشون دهنده قالب اطلاعات ای هستند که ما می خواهیم به دیتابیس بدیم و بعد از این شی های ساخته شده در داخل تابع Main استفاده کردیم و عملیات ثبت و دریافت اطلاعات رو انجام دادیم .
تا اینجای کار خب هیچ مشکلی به صورت عینی و ظاهری مشاهده نمی شه کد هم کار میکنه حالا ادامه کار را با استفاده از کلاس های انتزاعی بررسی میکنیم :
abstract class DataManager<T> { abstract fun add(item: T) abstract fun get(itemId: Int): T? }
نکته : حرف T استفاده شده یعنی این که ورودی این کلاس ممکنه از هر جنس داده ای باشه ، مانند رشته یا عدد
بسیار خوب در زیر ادامه کار رو با دوتا زیرکلاس(subclass)ادامه می دیم :
class UserManager : DataManager<User>() { private val users = mutableListOf<User>() override fun add(user: User) { users.add(user) println("کاربر ${user.name} اضافه شد.") } override fun get(userId: Int): User? { println("دریافت اطلاعات کاربر با آیدی $userId") return users.find { it.id == userId } } } class ProductManager : DataManager<Product>() { private val products = mutableListOf<Product>() override fun add(product: Product) { products.add(product) println("محصول ${product.name} اضافه شد.") } override fun get(productId: Int): Product? { println("دریافت اطلاعات محصول با آیدی $productId") return products.find { it.id == productId } } }
حالا نکته ای اخر اگر به این کلاس های اخری توجه کرده باشید ، در کلاس والد که میشه همون کلاسی که کلمه abstract استفاده شده ما دوتا تابع داریم که abstract هستند ، و این تابع ها خیلی ساده نام گذاری شده اند ، حالا هر کلاسی که در آینده بخواهید از این کلاس استفاده کند ، منطق کد و کاری که داخل این متد ها میخواد انجام دهد ، مشخص هست و هر مقدار که نیاز باشد می توانیم این کد ها رو توسعه بدهیم
درسته در روش کلاس های انتزاعی تعداد خط کد های که نوشتیم در نگاه اول زیاد باشه اما درک ما از کد و همچنین منطق کد ( که این کد برای ما چه کاری انجام میده ) راحت تر میکنه
و در اخر به این نکته اشاره کنم ، کدی که ما در اون لحظه می نویسیم ، برای چند روز می دونیم که هدف ما از اون کد چیه اما در پروژه های واقعی و بزرگ امکان داره پروژه ای بسیار بزرگ رو بخوایم در آینده بعد 1 سال بروزرسانی کنیم، قابلیت های جدید ای رو به اون اضافه کنیم ، که در این جا یک چهار چوب مشخص(به عنوان مثال ، استفاده کردن از کلاس های انتزاعی Abstract) به ما خیلی کمک میکنه که راحت تر کد ها رو درک کنیم
نتیجهگیری:
کلاسهای abstract بهت کمک میکنن یه الگو داشته باشی که کدتو بهتر و منظمتر بنویسی، از تکرار کد جلوگیری کنی، و نگهداری و تست کد رو راحتتر کنی و از همه مهم تر بین همه برنامه نویس های این الگو مشترک است .
خوشحالم که تا انتهای این مطلب خوندی .