الوراثة والواجهات في Kotlin: abstract و interface و sealed
في درس الكلاسات ورثت كلاساً من آخر باستخدام open و override. لكن تصميم التطبيقات الحقيقية يحتاج أدوات أدق: كلاس أب لا يُنشأ منه كائن، وقدرات مشتركة بين كلاسات لا قرابة بينها، وقائمة مغلقة من الحالات المحتملة. في هذا الدرس ستتعلم الكلاسات المجردة والواجهات والكلاسات المغلقة، وهي أدوات ستراها في كل مشروع أندرويد.
الكلاس المجرد abstract
Section titled “الكلاس المجرد abstract”ما مساحة «شكل»؟ لا إجابة حتى نعرف هل هو دائرة أم مستطيل. الكلاس 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.0abstract class: كلاس لا يمكن إنشاء كائن منه. كتابةShape("شكل")خطأ ترجمة.abstract fun area(): دالة بلا جسم، وكل كلاس وارث ملزم بكتابتها.- الكلاس المجرد ودواله المجردة مفتوحة للوراثة تلقائياً، فلا تحتاج
open. describeمكتوبة مرة واحدة في الأب، وتستدعي نسخةareaالخاصة بكل شكل. هذا هو تعدد الأشكال (Polymorphism).
الكلاس المجرد مثل عقد إيجار جاهز: البنود العامة مطبوعة، لكن خانات الاسم والمبلغ يجب أن يملأها كل مستأجر.
الواجهة interface
Section titled “الواجهة interface”الطائر يطير والطائرة تطير، وهما ليسا من عائلة واحدة. ما يجمعهما قدرة. الواجهة (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 = buttonitem.click()تم الضغط على حفظعنصر: حفظتم الضغط على حفظ- الكلاس يرث من كلاس أب واحد فقط، لكنه ينفّذ أي عدد من الواجهات مفصولة بفواصل.
- الواجهة قد تحتوي دوالاً لها جسم جاهز مثل
describe، يرثها الكلاس أو يعيد كتابتها. - الواجهة قد تطلب خاصية مثل
labelدون أن تخزن قيمتها، والكلاس يوفرها بـoverride val. - لاحظ الفرق في الكتابة: الكلاس الأب يُكتب بأقواس
Shape(...)لأننا نستدعي مُنشئه، والواجهة بلا أقواس.
الواجهة مثل رخصة القيادة: لا يهم من أنت، المهم أنك أثبتّ قدرتك على القيادة.
فحص النوع: is
Section titled “فحص النوع: is”متغير من نوع الأب قد يحمل كائناً من أي ابن. الكلمة 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
Section titled “الكلاس المغلق sealed”كثيراً ما يكون لشيء عدد محدود ومعروف من الحالات. مثلاً، نتيجة تحميل بيانات من الإنترنت: جارٍ التحميل، أو نجاح، أو فشل. لا حالة رابعة. الكلاس المغلق 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، لأن المترجم يعرف كل الحالات. وإذا أضفت لاحقاً حالة جديدة ونسيت معالجتها، يرفض المترجم الكود وينبهك. هذا الأسلوب أساسي في إدارة حالة الشاشات في أندرويد.
كلاس مجرد أم واجهة؟
Section titled “كلاس مجرد أم واجهة؟”| الكلاس المجرد | الواجهة | |
|---|---|---|
| العلاقة | «هو نوع من» | «يستطيع أن» |
| العدد المسموح | أب واحد | أي عدد |
| تخزين بيانات | نعم، خصائص ومُنشئ | لا تخزن حالة |
| مثال | Shape و Circle |
Clickable و Comparable |
القاعدة العملية: ابدأ بالواجهة، واختر الكلاس المجرد فقط إذا كان الأبناء يشتركون في بيانات وكود فعلي.
جرّب بنفسك
Section titled “جرّب بنفسك”- أنشئ
abstract class Vehicle(val name: String)فيه دالة مجردةwheels(): Intودالة عاديةinfo(). - أنشئ
CarوBikeيرثان منه، وضعهما في قائمة واطبع معلوماتهما. - أنشئ
interface Electricفيهاfun charge()، وكلاسElectricCarيرث منVehicleوينفّذElectric. - تحدٍّ: أنشئ
sealed class Paymentبثلاث حالات:CashوCard(val last4: String)وWallet(val phone: String)، ودالة تستخدمwhenلوصف كل طريقة دفع.
أخطاء شائعة
Section titled “أخطاء شائعة”- نسيان 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("دائرة").
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما فائدة sealed class مع when؟
الحالات معروفة ومحدودة، فلا تحتاج else، وإضافة حالة جديدة تجبرك على معالجتها.
سؤال
كم واجهة يستطيع الكلاس الواحد أن ينفّذ؟
الكلاس يرث من كلاس أب واحد، لكنه ينفّذ أي عدد من الواجهات.
الخلاصة
Section titled “الخلاصة”abstract classلا يُنشأ منه كائن، ودواله المجردة يلتزم بها الأبناء.interfaceتصف قدرة، والكلاس ينفّذ أي عدد منها.isتفحص النوع، والتحويل الذكي يتيح استخدام دوال النوع مباشرة.sealed classقائمة مغلقة من الحالات، وwhenمعها لا تحتاجelse.objectيعرّف كائناً وحيداً لا يتكرر.