Trong bài viết này, bạn sẽ tìm hiểu về kế thừa. Cụ thể hơn, ta sẽ tìm hiểu xem kế thừa là gì và cách triển khai nó trong Kotlin (thông qua các ví dụ).

Kế thừa là một trong những tính năng chính của lập trình hướng đối tượng. Nó cho phép người dùng tạo một class mới (class dẫn xuất) từ một class đã có (class cơ sở).

Class dẫn xuất kế thừa tất cả các tính năng từ class cơ sở và có thể có các tính năng bổ sung của riêng nó.

Trước khi đi vào chi tiết về kế thừa của Kotlin, chúng tôi khuyên bạn nên kiểm tra hai bài viết sau:

1. Vì sao phải kế thừa?

Giả sử, trong ứng dụng của bạn, bạn muốn có ba nhân vật – một giáo viên toán, một cầu thủ bóng đá và một doanh nhân .

Vì tất cả các nhân vật đều là người, họ có thể đi lại và nói chuyện. Tuy nhiên, họ cũng có một số kỹ năng đặc biệt. Một giáo viên toán có thể dạy toán, một cầu thủ bóng đá có thể chơi bóng đá và một doanh nhân có thể điều hành một doanh nghiệp .

Bạn có thể tạo riêng ba class: có thể đi bộ, nói chuyện và thực hiện kỹ năng đặc biệt của họ.

Trong mỗi class, bạn sẽ sao chép cùng một đoạn code để triển khai chức năng đi bộ và nói chuyện cho từng nhân vật.

Nếu bạn muốn thêm một tính năng mới – ví dụ như ăn chẳng hạn, bạn cần triển khai cùng một đoạn code cho mỗi nhân vật ấy. Điều này có thể khiến ta dễ bị lỗi (khi sao chép) và gây trùng lặp code.

Sẽ dễ dàng hơn rất nhiều nếu chúng ta có một class Person với các tính năng cơ bản như nói chuyện, đi bộ, ăn, ngủ và sau đó ta có thể thêm các kỹ năng đặc biệt cho các tính năng đó tùy thuộc vào các nhân vật của chúng ta. Điều này được thực hiện bằng cách sử dụng thừa kế.

Khi sử dụng thừa kế, bạn không cần viết lại cùng một đoạn code cho các hàm walk(), talk() và eat() cho mỗi class. Bạn chỉ cần kế thừa chúng.

Vì vậy, đối với class MathTeacher (class dẫn xuất), bạn kế thừa tất cả các tính năng của class Person (class cơ sở) và thêm một tính năng mới là teachMath(). Tương tự như vậy, đối với class Footballer, bạn kế thừa tất cả các tính năng của class Person và thêm một tính năng mới là playFootball(), v.v.

Việc này làm cho code của bạn gọn gàng, dễ hiểu hơn và có thể mở rộng nhiều hơn.

Điều quan trọng cần nhớ: Khi sử dụng thừa kế, mỗi class dẫn xuất phải thỏa mãn điều kiện: đó phải là class cơ sở. Trong ví dụ trên, MathTeacher là một Person , Footballer là một Person . Nhưng Businessman thì không được phép là một Business .

2. Kế thừa trong Kotlin

Hãy thử triển khai ý tưởng trên thành code:

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
}

Ở đây, Person là một class cơ sở, và các class như MathTeacher, Footballer và Businessman  được dẫn xuất từ class Person.

Lưu ý, từ khóa open đứng trước class cơ sở Person. 

Theo mặc định, các class trong Kotlin là class cuối cùng (class vô sinh). Nếu bạn đã quen thuộc với Java, bạn sẽ biết rằng một class cuối cùng không thể có các subclass. Bằng cách sử dụng từ khóa open trước từ class, trình biên dịch cho phép bạn dẫn xuất được các class mới từ nó.

Ví dụ: Kế thừa trong Kotlin

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

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()
}

Khi bạn chạy chương trình, kết quả sẽ là:

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.

Ở đây, hai class MathTeacher và Footballer được dẫn xuất từ class Person.

Hàm tạo chính của class Person đã khai báo hai thuộc tính: age và name và nó có một khối khởi tạo. Khối khởi tạo (và các hàm thành viên) của class cơ sở Person có thể được truy cập bởi các đối tượng của các class dẫn xuất ( MathTeacher và Footballer).

Các class dẫn xuất MathTeacher và Footballer có hàm thành viên riêng lần lượt là teachMaths()và playFootball(). Các hàm này chỉ có thể truy cập từ các đối tượng của class tương ứng.

Khi đối tượng t1 của class MathTeacher được tạo ra,

val t1 = MathTeacher(25, "Jack")

Các tham số được truyền cho hàm tạo chính. Trong Kotlin, khối init sẽ được gọi khi đối tượng đã được tạo. Vì, class MathTeacher có nguồn gốc từ class Person, nó tìm kiếm khối khởi tạo trong class cơ sở (Person) và thực hiện khối khởi tạo đó. Nếu class MathTeacher có khối init, trình biên dịch cũng sẽ thực thi khối init của class dẫn xuất.

Tiếp theo, hàm teachMaths() cho đối tượng t1 được gọi bằng cách sử dụng câu lệnh t1.teachMaths().

Chương trình hoạt động tương tự khi đối tượng f1 của class Footballer được tạo ra. Nó thực hiện khối init của class cơ sở. Sau đó, hàm playFootball() của class Footballer được gọi bằng cách sử dụng câu lệnh f1.playFootball().

2.1. Chú ý quan trọng về kế thừa trong Kotlin

  • Nếu class có một hàm tạo chính, thì cơ sở phải được khởi tạo bằng các tham số của hàm tạo chính. Trong chương trình trên, cả hai class dẫn xuất có hai tham số là age và name, và cả 2 thông số này đều được khởi tạo trong hàm tạo chính của class cơ sở.

Dưới đây là một ví dụ khác:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

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")
}

Ở đây, hàm tạo chính của class dẫn xuất có 3 tham số và class cơ sở có 2 tham số. Lưu ý rằng, cả hai tham số của class cơ sở đều được khởi tạo.

  • Trong trường hợp không có hàm tạo chính, mỗi class cơ sở phải khởi tạo cơ sở (bằng cách sử dụng từ khóa super) hoặc ủy quyền cho một hàm tạo khác thực hiện điều đó. Ví dụ,
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) {
    }
}

Để tìm hiểu thêm về cách hoạt động của chương trình này, hãy truy cập Hàm tạo phụ của Kotlin.

2.2. Ghi đè các hàm thành viên và các thuộc tính 

Nếu class cơ sở và class dẫn xuất chứa hàm thành viên (hoặc thuộc tính) có cùng tên, bạn có thể sẽ cần ghi đè hàm thành viên của class dẫn xuất bằng từ khóa override và sử dụng từ khóa open cho hàm thành viên của class cơ sở.

Ví dụ: Ghi đè hàm thành viên

// 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)
}

Khi bạn chạy chương trình, kết quả sẽ là:

My fake age is 26.

Ở đây, câu lệnh girl.displayAge(31)gọi hàm displayAge() của class dẫn xuất Girl.

Bạn có thể ghi đè thuộc tính của class cơ sở theo cách tương tự.

Hãy truy cập vào bài viết về cách làm việc của getters và setters trong Kotlin hoạt động trong Kotlin trước khi xem xét ví dụ dưới đây.

// 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}.")
}

Khi bạn chạy chương trình, kết quả sẽ là:

My fake age is 26.

Như bạn có thể thấy, chúng ta đã sử dụng từ khóa override và open cho thuộc tính age lần lượt trong class dẫn xuất và class cơ sở.

2.3. Gọi các thành viên của class cơ sở từ class dẫn xuất

Bạn có thể gọi các hàm (và truy cập các thuộc tính) của class cơ sở từ class dẫn xuất bằng từ khóa super. Dưới đây là cách thực hiện:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

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)
}

Khi bạn chạy chương trình, kết quả sẽ là:

My age is 31.
My fake age is 26.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!