تخطَّ إلى المحتوى

الوراثة والواجهات في Kotlin: abstract و interface و sealed

الدرس 9 من 11 · Kotlin

في درس الكلاسات ورثت كلاساً من آخر باستخدام open و override. لكن تصميم التطبيقات الحقيقية يحتاج أدوات أدق: كلاس أب لا يُنشأ منه كائن، وقدرات مشتركة بين كلاسات لا قرابة بينها، وقائمة مغلقة من الحالات المحتملة. في هذا الدرس ستتعلم الكلاسات المجردة والواجهات والكلاسات المغلقة، وهي أدوات ستراها في كل مشروع أندرويد.

ما مساحة «شكل»؟ لا إجابة حتى نعرف هل هو دائرة أم مستطيل. الكلاس Shape فكرة عامة، ولا معنى لإنشاء كائن منه:

abstract class Shape(val name: String) {
abstract fun area(): Double
fun describe() = "$name مساحته ${area()}"
}
class Rectangle(val width: Double, val height: Double) : Shape("مستطيل") {
override fun area() = width * height
}
class Circle(val radius: Double) : Shape("دائرة") {
override fun area() = 3.14 * radius * radius
}
val shapes: List<Shape> = listOf(Rectangle(4.0, 5.0), Circle(10.0))
for (shape in shapes) {
println(shape.describe())
}
الناتج
مستطيل مساحته 20.0
دائرة مساحته 314.0
  • abstract class: كلاس لا يمكن إنشاء كائن منه. كتابة Shape("شكل") خطأ ترجمة.
  • abstract fun area(): دالة بلا جسم، وكل كلاس وارث ملزم بكتابتها.
  • الكلاس المجرد ودواله المجردة مفتوحة للوراثة تلقائياً، فلا تحتاج open.
  • describe مكتوبة مرة واحدة في الأب، وتستدعي نسخة area الخاصة بكل شكل. هذا هو تعدد الأشكال (Polymorphism).

الكلاس المجرد مثل عقد إيجار جاهز: البنود العامة مطبوعة، لكن خانات الاسم والمبلغ يجب أن يملأها كل مستأجر.

الطائر يطير والطائرة تطير، وهما ليسا من عائلة واحدة. ما يجمعهما قدرة. الواجهة (Interface) تصف قدرة يتعهد الكلاس بتنفيذها:

interface Clickable {
fun click()
}
interface Describable {
val label: String
fun describe() = "عنصر: $label"
}
class Button(override val label: String) : Clickable, Describable {
override fun click() {
println("تم الضغط على $label")
}
}
val button = Button("حفظ")
button.click()
println(button.describe())
val item: Clickable = button
item.click()
الناتج
تم الضغط على حفظ
عنصر: حفظ
تم الضغط على حفظ
  • الكلاس يرث من كلاس أب واحد فقط، لكنه ينفّذ أي عدد من الواجهات مفصولة بفواصل.
  • الواجهة قد تحتوي دوالاً لها جسم جاهز مثل describe، يرثها الكلاس أو يعيد كتابتها.
  • الواجهة قد تطلب خاصية مثل label دون أن تخزن قيمتها، والكلاس يوفرها بـ override val.
  • لاحظ الفرق في الكتابة: الكلاس الأب يُكتب بأقواس Shape(...) لأننا نستدعي مُنشئه، والواجهة بلا أقواس.

الواجهة مثل رخصة القيادة: لا يهم من أنت، المهم أنك أثبتّ قدرتك على القيادة.

متغير من نوع الأب قد يحمل كائناً من أي ابن. الكلمة is تفحص النوع الحقيقي، وبعدها تعامل Kotlin المتغير بنوعه الجديد تلقائياً. هذه الميزة اسمها التحويل الذكي (Smart Cast):

val items: List<Any> = listOf("نص", 42, 3.5)
for (item in items) {
if (item is String) {
println("نص طوله ${item.length}")
} else if (item is Int) {
println("عدد صحيح ضعفه ${item * 2}")
} else {
println("نوع آخر: $item")
}
}
الناتج
نص طوله 2
عدد صحيح ضعفه 84
نوع آخر: 3.5

بعد نجاح item is String استخدمنا item.length مباشرة دون أي تحويل يدوي. والنوع Any هو أبو كل الأنواع في Kotlin.

كثيراً ما يكون لشيء عدد محدود ومعروف من الحالات. مثلاً، نتيجة تحميل بيانات من الإنترنت: جارٍ التحميل، أو نجاح، أو فشل. لا حالة رابعة. الكلاس المغلق sealed يخبر المترجم بذلك:

sealed class Result
class Success(val data: String) : Result()
class Failure(val message: String) : Result()
object Loading : Result()
fun render(result: Result): String = when (result) {
is Success -> "البيانات: ${result.data}"
is Failure -> "خطأ: ${result.message}"
Loading -> "جارٍ التحميل..."
}
println(render(Loading))
println(render(Success("3 رسائل جديدة")))
println(render(Failure("لا يوجد اتصال")))
الناتج
جارٍ التحميل...
البيانات: 3 رسائل جديدة
خطأ: لا يوجد اتصال

الفائدة الكبرى: when هنا بلا else، لأن المترجم يعرف كل الحالات. وإذا أضفت لاحقاً حالة جديدة ونسيت معالجتها، يرفض المترجم الكود وينبهك. هذا الأسلوب أساسي في إدارة حالة الشاشات في أندرويد.

الكلاس المجرد الواجهة
العلاقة «هو نوع من» «يستطيع أن»
العدد المسموح أب واحد أي عدد
تخزين بيانات نعم، خصائص ومُنشئ لا تخزن حالة
مثال Shape و Circle Clickable و Comparable

القاعدة العملية: ابدأ بالواجهة، واختر الكلاس المجرد فقط إذا كان الأبناء يشتركون في بيانات وكود فعلي.

  1. أنشئ abstract class Vehicle(val name: String) فيه دالة مجردة wheels(): Int ودالة عادية info().
  2. أنشئ Car و Bike يرثان منه، وضعهما في قائمة واطبع معلوماتهما.
  3. أنشئ interface Electric فيها fun charge()، وكلاس ElectricCar يرث من Vehicle وينفّذ Electric.
  4. تحدٍّ: أنشئ sealed class Payment بثلاث حالات: Cash و Card(val last4: String) و Wallet(val phone: String)، ودالة تستخدم when لوصف كل طريقة دفع.
  • نسيان open: الكلاس العادي في Kotlin مغلق أمام الوراثة. بدون open أو abstract ستقرأ: This type is final, so it cannot be extended.
  • نسيان override: إعادة كتابة دالة بدون الكلمة override خطأ ترجمة في Kotlin، وليست اختيارية كما في Java.
  • إنشاء كائن من كلاس مجرد: Shape("شكل") يعطي: Cannot create an instance of an abstract class.
  • نسيان أقواس الأب: class Circle : Shape خطأ. الكلاس الأب يحتاج استدعاء مُنشئه: Shape("دائرة").

سؤال

ما فائدة sealed class مع when؟

سؤال

كم واجهة يستطيع الكلاس الواحد أن ينفّذ؟

  • abstract class لا يُنشأ منه كائن، ودواله المجردة يلتزم بها الأبناء.
  • interface تصف قدرة، والكلاس ينفّذ أي عدد منها.
  • is تفحص النوع، والتحويل الذكي يتيح استخدام دوال النوع مباشرة.
  • sealed class قائمة مغلقة من الحالات، و when معها لا تحتاج else.
  • object يعرّف كائناً وحيداً لا يتكرر.