در این بخش از مقاله آموزش کاتلین به بررسی مفهوم وراثت در این زبان برنامهنویسی میپردازیم. به بیان دقیقتر در این بخش با ماهیت وراثت به عنوان یکی از ارکان اساسی برنامهنویسی شیءگرا آشنا شده و با بررسی مثالهای عملی شیوه پیادهسازی آن را در زبان کاتلین بررسی خواهیم کرد.
وراثت یکی از قابلیتهای کلیدی برنامهنویسی شیءگرا محسوب میشود و به کاربران امکان میدهد که یک کلاس جدید (کلاس مشتقشده) را از کلاس موجود (کلاس مبنا) بسازند. کلاس مشتقشده همه قابلیتهای کلاس مبنا را به ارث میبرد و میتواند برخی قابلیتهای خاص اضافی نیز برای خود داشته باشد.
چرا باید از وراثت استفاده کنیم؟
فرض کنید میخواهید در اپلیکیشن خودتان سه شخصیت به صورت معلم ریاضی، فوتبالیست و تاجر داشته باشید. از آنجا که همه این افراد، صرفنظر از شغلشان، انسان هستند، میتوانند راه رفته و صحبت کنند. با این حال هر کدام از آنها در زمینه شغلی خود به مهارتهای خاصی نیاز دارند. یک معلم ریاضی میتواند به تدریس ریاضیات بپردازد. یک فوتبالیست میتواند فوتبال بازی کند، یک تاجر میتواند کسبوکاری را اداره کند.
شما میتوانید سه کلاس مجزا برای هر یک از این افراد ایجاد کرده و در آنها قابلیت راه رفتن، صحبت کردن و همچنین مهارتهای خاص هر یک را قرار دهید.
به این ترتیب در هر یک از این کلاسها باید از کد یکسانی برای راه رفتن و صحبت کردن هر شخصیت استفاده کنید.
همچنین در صورتی که بخواهید قابلیت جدیدی مانند غذا خوردن را به هر یک از این افراد بدهید، باید آن را به صورت مجزا به تکتک کاراکترها اضافه کنید. این وضعیت مستعد بروز خطا و همچنین نیازمند نوشتن کدهای تکراری است.
در صورتی که یک کلاس Person داشته باشید که دارای برخی قابلیتهای اولیه مانند راه رفتن، صحبت کردن، خوردن، خوابیدن باشد و در ادامه برخی مهارتهای خاص را بنا به نوع شخصیت به هر کدام اضافه کنید، روند کار تا حدود زیادی بهینه میشود. این وضعیت به نام وراثت خوانده میشود.
بدین ترتیب با بهرهگیری از وراثت، دیگر لازم نیست کد یکسانی را برای متدهای ()walk() ،talk و ()eat در هر کلاس بنویسید. کافی است این قابلیتها را ارثبری کنید.
بنابراین در مورد MathTeacher که یک کلاس مشتقشده است، همه قابلیتهای Person را که کلاس مبنا است به ارث میبریم و یک قابلیت جدید به صورت ()teachMath اضافه میکنیم. به طور مشابه، در مورد کلاس Footballer، همه قابلیتها را از کلاس Person ارثبری میکنیم و یک قابلیت جدید به نام ()playFootball نیز اضافه میکنیم.
این کار موجب میشود که کد تمیزتر شده و قابلیت درک و بسطپذیری بالاتری پیدا کند. باید در خاطر داشته باشید که وقتی با وراثت کار میکنیم، هر کلاس مشتقشده باید یک شرط (is a) داشته باشد که تعیین کنید آیا یک کلاس مبنا است یا نه. در این مثال، MathTeacher یک کلاس Person است. همچنین Footballer یک کلاس مبنا است. اما Businessman یک کلاس مشتقشده از Business نیست.
وراثت در کاتلین
اگر بخواهیم مباحث فوق را در کاتلین پیادهسازی کنیم به کد زیر میرسیم:
open class Person(age: Int) { // code for eating, talking, walking
}
class MathTeacher(age: Int): Person(age) { // other features of math teacher
}
class Footballer(age: Int): Person(age) { // other features of footballer
}
class Businessman(age: Int): Person(age) { // other features of businessman
}
در کد فوق Person یک کلاس مبنا است و کلاسهای MathTeacher ،Footballer و Businessman از کلاس Person مشتق شدهاند. توجه کنید که کلیدواژه open که پیش از آکولاد در Person آمده، بسیار مهم است.
به طور پیشفرض، کلاسها در کاتلین به صورت final هستند. اگر با جاوا آشنا باشید، میدانید که یک کلاس final نمیتواند کلاس فرعی تولید کند. بنابراین با استفاده از حاشیهنویسی open روی یک کلاس به کامپایلر اعلام میکنیم که میتواند کلاسهای جدیدی از آن بسازد.
مثالی از وراثت در کاتلین
open class Person(age: Int, name: String) { init { println("My name is $name.") println("My age is $age") }
}
class MathTeacher(age: Int, name: String): Person(age, name) {
fun teachMaths() { println("I teach in primary school.") }
}
class Footballer(age: Int, name: String): Person(age, name) { fun playFootball() { println("I play for LA Galaxy.") }
}
fun main(args: Array<String>) { val t1 = MathTeacher(25, "Jack") t1.teachMaths()
println()
val f1 = Footballer(29, "Christiano") f1.playFootball()
}
خروجی برنامه فوق به صورت زیر است:
My name is Jack. My age is 25 I teach in primary school. My name is Cristiano. My age is 29 I play for LA Galaxy.
در این کد دو کلاس به نامهای MathTeacher and Footballer از کلاس Person مشتق شدهاند.
سازنده اولیه کلاس Person دو مشخصه به نامهای age و name اعلان کرده است و یک بلوک مقداردهی دارد. بلوک مقداردهی (و تابعهای عضو) کلاس مبنای Person میتوانند از سوی اشیای کلاس مشتقشده یعنی MathTeacher و Footballer مورد دسترسی قرار گیرند.
کلاسهای مشتقشده MathTeacher و Footballer برای خود تابعهای عضو خاصی به ترتیب به نامهای ()teachMaths و ()playFootball دارند. این تابعها تنها از سوی اشیا و کلاس متناظرشان قابل دسترسی هستند.
زمانی که شیء t1 از کلاس MathTeacher ایجاد شود:
val t1 = MathTeacher(25, "Jack")
پارامترها به سازنده اولیه ارسال میشوند. در کاتلین بلوک init زمانی فراخوانی میشود که شیء ایجاد شود. از آنجا که از کلاس Person مشتق شده است، به دنبال بلوک مقداردهی در کلاس مبنا میگردد و آن را اجرا میکند. اگر MathTeacher دارای بلوک init باشد، کامپایلر نیز میتواند بلوک init کلاس مشتقشده را اجرا کند.
سپس تابع ()teachMaths برای شیء t1 با استفاده از گزاره ()t1.teachMaths فراخوانی میشود. این برنامه مشابه زمانی عمل میکند که شیء t1 کلاس Footballer ایجاد میشود. این برنامه بلوک init کلاس مبنا را اجرا میکند. سپس متد ()playFootball کلاس Footballer با استفاده از گزاره ()f1.playFootball اجرا میشود.
نکتههای مهم در مورد وراثت در کاتلین
اگر کلاس یک سازنده اولیه داشته باشد، مبنا باید با استفاده از پارامترهای سازنده اولیه مقداردهی شود. در برنامه فوق، هر دو کلاس مشتقشده دارای پارامترهای age و name هستند و هر دوی این پارامترها در سازنده اولیه کلاس مبنا مقداردهی میشوند. به مثال زیر توجه کنید:
open class Person(age: Int, name: String) { // some code
}
class Footballer(age: Int, name: String, club: String): Person(age, name) { init { println("Football player $name of age $age and plays for $club.") }
fun playFootball() { println("I am playing football.") }
}
fun main(args: Array<String>) { val f1 = Footballer(29, "Cristiano", "LA Galaxy")
}
در کد فوق، سازنده اولیه کلاس مشتقشده 3 پارامتر دارد و کلاس مبنا دارای 2 پارامتر است. توجه کنید که هر دوی این پارامترهای کلاس مبنا، مقداردهی شدهاند. در مورد عدم وجود سازنده اولیه، هر کلاس مبنا باید مبنا را مقداردهی کنند یا سازنده دیگری را که این کار را میکند نمایندگی کند. به مثال زیر توجه کنید:
fun main(args: Array<String>) {
val p1 = AuthLog("Bad Password")
}
open class Log { var data: String = "" var numberOfData = 0 constructor(_data: String) {
} constructor(_data: String, _numberOfData: Int) { data = _data numberOfData = _numberOfData println("$data: $numberOfData times") }
}
class AuthLog: Log { constructor(_data: String): this("From AuthLog -> + $_data", 10) { }
constructor(_data: String, _numberOfData: Int): super(_data, _numberOfData) { }
}
Override کردن تابعهای عضو و مشخصهها
اگر کلاس مبنا و کلاس مشتقشده شامل یک تابع (یا مشخصه) عضو با نام یکسان باشند، میتوانید تابع عضو کلاس مشتقشده را با استفاده از کلیدواژه open برای تابع عضو کلاس مبنا override کنید.
مثالی از Override کردن تابع عضو
// Empty primary constructor
open class Person() { open fun displayAge(age: Int) { println("My age is $age.") }
}
class Girl: Person() {
override fun displayAge(age: Int) { println("My fake age is ${age - 5}.") }
}
fun main(args: Array<String>) { val girl = Girl() girl.displayAge(31)
}
خروجی برنامه فوق به صورت زیر است:
My fake age is 26.
در برنامه فوق، girl.displayAge(31) متد ()displayAge مربوط به کلاس مشتقشده به نام girl.displayAge(31) اقدام به فراخوانی ()displayAge میکند. امکان اُوراید کردن کلاس مبنا به روش مشابه نیز وجود دارد.
// Empty primary constructor
open class Person() { open var age: Int = 0 get() = field set(value) { field = value }
}
class Girl: Person() {
override var age: Int = 0 get() = field set(value) { field = value - 5 }
}
fun main(args: Array<String>) {
val girl = Girl() girl.age = 31 println("My fake age is ${girl.age}.")
}
خروجی برنامه فوق به صورت زیر است:
My fake age is 26.
چنان که میبینید از کلیدواژههای override و open برای مشخصه age به ترتیب در کلاس مشتقشده و کلاس مبنا استفاده کردهایم.
فراخوانی عضوهای کلاس مبنا از کلاس مشتقشده
امکان فراخوانی تابعها (و مشخصههای دسترسی) کلاس مبنا از یک کلاسی مشتقشده با استفاده از کلیدواژه super وجود دارد. به مثال زیر توجه کنید:
open class Person() { open fun displayAge(age: Int) { println("My actual age is $age.") }
}
class Girl: Person() {
override fun displayAge(age: Int) {
// calling function of base class super.displayAge(age) println("My fake age is ${age - 5}.") }
}
fun main(args: Array<String>) { val girl = Girl() girl.displayAge(31)
}
خروجی برنامه فوق به صورت زیر است:
My age is 31. My fake age is 26.