التوجيه الديناميكي
جزء من قسم البرمجة كائنية التوجه في رحلة Kotlin على Coddy. الدرس 27 من 57.
تعدد الأشكال يعني أن استدعاءً واحدًا يمكن أن يتصرف بطرق عديدة. عندما تستدعي طريقةً مُعاد تعريفها من خلال نوع فئة أساسية أو نوع واجهة، تنظر Kotlin إلى الفئة الفعلية للكائن أثناء تشغيل البرنامج وتنفّذ إصدار تلك الفئة. يُسمّى هذا الإرسال الديناميكي:
abstract class Animal(val name: String) {
abstract fun sound(): String
fun speak() = "$name: ${sound()}"
}
class Dog(name: String) : Animal(name) {
override fun sound() = "Woof"
}
class Cow(name: String) : Animal(name) {
override fun sound() = "Moo"
}داخل main:
val barn: List<Animal> = listOf(Dog("Rex"), Cow("Bess"), Dog("Fido"))
for (a in barn) println(a.speak())الناتج:
Rex: Woof
Bess: Moo
Fido: Woofلا يتغير كود الاستدعاء عند ظهور فئة فرعية جديدة. كُتبت totalArea أدناه لأي Shape، لذا فهي تعمل مع Triangle أيضًا، رغم أن أحدًا لم يغيّرها:
interface Shape {
fun area(): Double
}
class Square(val side: Double) : Shape {
override fun area() = side * side
}
class Triangle(val base: Double, val height: Double) : Shape {
override fun area() = base * height / 2
}
fun totalArea(shapes: List<Shape>) = shapes.sumOf { it.area() }داخل main:
println(totalArea(listOf(Square(2.0), Triangle(3.0, 4.0))))الناتج:
10.0يَنظر الاستدعاء إلى الكائن، وليس إلى المتغير. يؤدي تعيين كائن آخر إلى المتغير نفسه إلى تغيير الشيفرة التي يتم تشغيلها عند الاستدعاء التالي:
interface Mode {
fun format(text: String): String
}
class Normal : Mode {
override fun format(text: String) = text
}
class Shout : Mode {
override fun format(text: String) = text.uppercase() + "!"
}داخل main:
var mode: Mode = Normal()
println(mode.format("hello"))
mode = Shout()
println(mode.format("hello"))الناتج:
hello
HELLO!تستبدل تعددية الأشكال عمليات تحقق مثل if (kind == "dog") التي كان من الممكن تكرارها في كل موضع تُستخدم فيه الحيوانات. تحتفظ كل فئة بسلوكها الخاص، وإضافة نوع من الحيوانات تعني إضافة فئة واحدة:
// بدون تعدد الأشكال: كل دالة تتحقق من النوع
fun sound(kind: String) = when (kind) { "dog" -> "Woof"; "cow" -> "Moo"; else -> "?" }
// مع تعدد الأشكال: النوع الجديد يجلب سلوكه الخاص
class Sheep(name: String) : Animal(name) {
override fun sound() = "Baa"
}التحدي
سهلCharacter(name, hp) هو صنف مفتوح. تقوم takeHit(damage) بطرح الضرر من hp (بحيث لا يقل أبدًا عن 0)، وتُرجع status() القيمة Ada: 30 hp أو Ada: down عند الوصول إلى 0. يمكن قراءة hp من أي مكان، لكن لا يمكن تغييره إلا داخل Character. أكمل الأصناف الفرعية عبر إعادة تعريف takeHit وتمرير الضرر المُغيَّر إلى super.takeHit: يتلقى Warrior (بـ 40 hp) نصف الضرر، مع التقريب إلى الأسفل؛ يمتلك Mage (بـ 25 hp) درعًا يمتص الضربة الأولى بالكامل؛ ويتفادى Rogue (بـ 30 hp) كل ضربة ثانية يتلقاها (الثانية، والرابعة، ...).
يقرأ الكود المزوَّد المجموعة باعتبارها warrior:Ada,mage:Bo,rogue:Cy، ثم يوجّه ضربة إلى all 10 أو Ada 12. ويستدعي takeHit من خلال النوع Character، ثم يطبع أخيرًا كل قيم status().
ضع كودك في Character.kt وWarrior.kt وMage.kt وRogue.kt. يحتوي Main.kt على كود الإدخال/الإخراج المزوَّد، ولا يمكن تعديله.
جرّب بنفسك
fun main() {
// كود الإدخال/الإخراج المُزوَّد: أبقِه كما هو
val input = generateSequence(::readLine).toList()
val party = input[0].split(",").map { spec ->
val (kind, name) = spec.split(":")
when (kind) {
"warrior" -> Warrior(name)
"mage" -> Mage(name)
else -> Rogue(name)
}
}
for (cmd in input.drop(1)) {
val (target, damage) = cmd.split(" ")
for (c in party) {
if (target == "all" || target == c.name) c.takeHit(damage.toInt())
}
}
for (c in party) println(c.status())
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس البرمجة كائنية التوجه
1الفئات والكائنات
العمل مع الملفاتتعريف الفئاتالمنشئات الأساسيةكتل init والتحقق من الصحةالمنشئات الثانويةمراجعة - بطاقة المكتبة4الوراثة
الأصناف المفتوحة والأصناف الفرعيةتجاوز الأعضاءsuper والمنشئاتالأصناف المجرّدةمراجعة - دليل الموظفين3الكائنات والكائنات المرافقة
تعريفات الكائناتالكائنات المرافقةالثوابت باستخدام constتعبيرات الكائناتمراجعة - شباك التذاكر6تعدد الأشكال
التوجيه الديناميكيفحوصات الأنواع والتحويلات الذكيةwhen مع الأنواعدوال التوسعةمراجعة - حارس حديقة الحيواناتتدرّب بنفسك: Playground لـ Kotlin