Lớp và tính kế thừa trong Kotlin

1. Trước khi bắt đầu

Điều kiện tiên quyết

  • Quen thuộc với việc sử dụng Kotlin Playground để chỉnh sửa chương trình Kotlin.
  • Nắm được các khái niệm cơ bản về lập trình trong Kotlin dạy trong Bài 1 của khoá học này. Cụ thể là chương trình main(), hàm có đối số trả về giá trị, biến, kiểu dữ liệu và phép toán, cũng như câu lệnh if/else.
  • Có thể định nghĩa một lớp (class) trong Kotlin, tạo một thực thể đối tượng từ lớp đó, cũng như truy cập vào các thuộc tính và phương thức của lớp đó.

Kiến thức bạn sẽ học được

  • Tạo một chương trình Kotlin sử dụng tính kế thừa để triển khai một hệ phân cấp lớp (class hierarchy).
  • Mở rộng một lớp (class), ghi đè chức năng hiện tại của lớp và thêm chức năng mới.
  • Chọn đúng hệ số sửa đổi chế độ hiển thị (visibility modifier) cho biến.

Sản phẩm bạn sẽ tạo ra

  • Một chương trình Kotlin với nhiều loại nhà ở (dwelling) được triển khai dưới dạng hệ phân cấp lớp.

Bạn cần có

2. Hệ phân cấp lớp là gì?

Con người có xu hướng phân loại các mục có thuộc tính và hành vi tương tự nhau thành các nhóm và thậm chí tạo thành một loại phân cấp nào đó giữa các mục. Ví dụ: bạn có thể có một danh mục rộng như rau củ và trong đó, bạn có thể có một loại cụ thể hơn như các loại đậu. Trong các loại đậu, bạn còn có thể có nhiều loại cụ thể hơn như đậu Hà Lan, đậu hạt, đậu lăng, đậu gà và đậu nành.

Bạn có thể biểu thị mối quan hệ này dưới dạng hệ phân cấp vì các loại đậu có chứa hoặc kế thừa (inherit) tất cả đặc tính của rau củ (ví dụ: chúng là thực vật và có thể ăn được). Tương tự, đậu Hà Lan, đậu hạt và đậu lăng đều có các đặc tính của cây họ đậu kèm theo đặc tính riêng.

Hãy xem bạn sẽ thể hiện mối quan hệ này như thế nào bằng thuật ngữ lập trình. Nếu đặt Vegetable làm một lớp (class) trong Kotlin, bạn có thể tạo Legume làm con (child) hoặc lớp con (subclass) của lớp Vegetable. Tức là tất cả thuộc tính và phương thức của lớp Vegetable đều được kế thừa (nghĩa là cũng có sẵn trong) trong lớp Legume.

Bạn có thể trình bày thông tin này trong một sơ đồ hệ phân cấp lớp (class hierarchy) như minh hoạ dưới đây. Bạn có thể gọi Vegetablelớp cha (parent) hoặc siêu lớp (superclass) của lớp Legume.

87e0a5eb0f85042d.png

Bạn có thể tiếp tục và mở rộng hệ phân cấp lớp bằng cách tạo các lớp con của Legume như LentilChickpea. Điều này khiến Legume vừa là con hay lớp con của Vegetable, cũng vừa là lớp cha hoặc siêu lớp của LentilChickpea. Vegetable là lớp gốc hoặc cấp cao nhất (hoặc cơ sở) của hệ phân cấp này.

638655b960530d9.png

Tính kế thừa trong lớp Android

Mặc dù bạn có thể viết mã Kotlin mà không cần sử dụng các lớp và bạn cũng từng làm như vậy trong các lớp học lập trình trước đây, nhưng nhiều phần của Android được cung cấp cho bạn dưới dạng các lớp, trong đó có hoạt động (activity), khung hiển thị (view) và nhóm khung hiển thị (view group). Do đó, hệ phân cấp lớp là yếu tố cơ bản cho việc phát triển ứng dụng Android và cho phép bạn tận dụng các tính năng do khung Android cung cấp.

Ví dụ: lớp View trong Android biểu thị một vùng hình chữ nhật trên màn hình, đồng thời chịu trách nhiệm vẽ và xử lý sự kiện. Lớp TextView là lớp con của lớp View, tức là TextView kế thừa tất cả thuộc tính và chức năng của lớp View, đồng thời bổ sung thêm logic cụ thể để hiển thị văn bản cho người dùng.

c39a8aaa5b013de8.png

Nếu đi sâu hơn thì các lớp EditTextButton là con của lớp TextView. Các lớp này kế thừa tất cả thuộc tính và phương thức của các lớp TextViewView, đồng thời thêm logic riêng của từng lớp. Ví dụ: EditText thêm chức năng riêng để có thể chỉnh sửa văn bản trên màn hình.

Thay vì phải sao chép và dán tất cả logic từ các lớp ViewTextView vào lớp EditText, EditText chỉ cần là lớp con của lớp TextView (và lớp này lại là con của lớp View). Sau đó, mã trong lớp EditText có thể tập trung cụ thể vào những điểm khiến thành phần giao diện người dùng này khác với các khung hiển thị khác.

đầu trang tài liệu dành cho một lớp Android trên trang web developer.android.com, bạn có thể thấy sơ đồ hệ phân cấp lớp. Nếu bạn thấy kotlin.Any ở đầu hệ phân cấp, thì đó là vì trong Kotlin, mọi lớp đều có một siêu lớp chung là Any. Tìm hiểu thêm tại đây.

1ce2b1646b8064ab.png

Như bạn có thể thấy, việc học cách khai thác tính kế thừa giữa các lớp có thể giúp bạn viết, sử dụng lại, đọc và kiểm thử mã dễ dàng hơn.

3. Tạo lớp cơ sở

Hệ phân cấp lớp nhà ở

Trong lớp học lập trình này, bạn sẽ xây dựng một chương trình Kotlin để trình bày cách hoạt động của hệ phân cấp lớp, sử dụng nhà ở (dwelling) – nơi mọi người cư trú và sinh hoạt và có không gian sàn, các tầng và cư dân để làm ví dụ.

Dưới đây là sơ đồ hệ phân cấp lớp mà bạn sẽ tạo. Ở gốc, bạn có một Dwelling xác định các thuộc tính và chức năng đúng với mọi ngôi nhà, tương tự như bản thiết kế. Sau đó, bạn sẽ có các lớp của một nhà gỗ vuông (SquareCabin), một lều tròn (RoundHut) và một tháp tròn (RoundTower) là một RoundHut có nhiều tầng.

de1387ca7fc26c81.png

Các lớp mà bạn sẽ triển khai:

  • Dwelling: một lớp cơ sở đại diện cho một chỗ ở không cụ thể, chứa thông tin chung của tất cả nhà ở.
  • SquareCabin: một nhà gỗ vuông có khu vực sàn hình vuông.
  • RoundHut: một lều tròn được làm bằng rơm rạ có khu vực sàn tròn và là lớp cha của RoundTower.
  • RoundTower: một tháp tròn được làm bằng đá, có khu vực sàn tròn và nhiều tầng.

Tạo lớp nhà ở trừu tượng

Lớp nào cũng có thể là lớp cơ sở của hệ phân cấp lớp hay lớp mẹ của các lớp khác.

Lớp "trừu tượng" (abstract) là một lớp không thể tạo thực thể vì không được triển khai đầy đủ. Bạn có thể xem đây là một bản phác thảo. Một bản phác thảo kết hợp các ý tưởng và kế hoạch cho điều gì đó, nhưng thường thì không đủ thông tin để xây dựng. Bạn sử dụng bản phác thảo (lớp trừu tượng) để tạo bản thiết kế (lớp) mà bạn dùng để tạo thực thể đối tượng thực tế.

Lợi ích chung của việc tạo siêu lớp là để chứa các thuộc tính và hàm chung cho tất cả lớp con. Nếu không xác định được giá trị của các thuộc tính và cách triển khai hàm, lớp đó trở thành trừu tượng. Ví dụ: Vegetables có nhiều thuộc tính phổ biến đối với mọi loại rau củ, nhưng bạn không thể tạo một thực thể của một loại rau không cụ thể, vì bạn không biết hình dạng hay màu sắc của loại rau đó chẳng hạn. Vì vậy, Vegetable là một lớp trừu tượng mà phải phụ thuộc vào các lớp con để xác định các chi tiết cụ thể về từng loại rau.

Phần khai báo của lớp trừu tượng bắt đầu bằng từ khoá abstract.

Dwelling sẽ là một lớp trừu tượng như Vegetable. Lớp này sẽ chứa các thuộc tính và hàm phổ biến ở nhiều loại nhà ở, nhưng giá trị chính xác của các thuộc tính và chi tiết triển khai hàm thì không được xác định.

  1. Truy cập Kotlin Playground tại https://developer.android.com/training/kotlinplayground.
  2. Trong trình chỉnh sửa, hãy xoá println("Hello, world!") bên trong hàm main().
  3. Sau đó, hãy thêm mã này bên dưới hàm main() để tạo một lớp abstract có tên Dwelling.
abstract class Dwelling(){
}

Thêm một thuộc tính cho vật liệu xây dựng

Trong lớp Dwelling này, bạn định nghĩa những thứ có sẵn cho tất cả nhà ở, ngay cả khi những thứ này có đặc điểm riêng tuỳ theo nhà. Tất cả ngôi nhà đều được làm bằng một số vật liệu xây dựng.

  1. Bên trong Dwelling, hãy tạo biến buildingMaterial thuộc kiểu String để biểu thị vật liệu xây dựng. Vì vật liệu xây dựng sẽ không thay đổi, hãy sử dụng val để biến này là một biến không thể thay đổi (immutable variable).
val buildingMaterial: String
  1. Hãy chạy chương trình và bạn sẽ gặp lỗi này.
Property must be initialized or be abstract

Thuộc tính buildingMaterial không có giá trị. Trên thực tế, bạn KHÔNG THỂ cung cấp cho thuộc tính này một giá trị, bởi vì một toà nhà không cụ thể thì không được làm bằng bất cứ thứ gì cụ thể. Vì vậy, như thông báo lỗi đã chỉ ra, bạn có thể đặt tiền tố khai báo của buildingMaterial bằng từ khoá abstract để cho biết buildingMaterial sẽ không được định nghĩa ở đây.

  1. Hãy thêm từ khoá abstract vào phần định nghĩa biến.
abstract val buildingMaterial: String
  1. Chạy mã của bạn, và tuy không làm gì nhưng mã vẫn biên dịch mà không gặp lỗi.
  2. Tạo một thực thể của Dwelling trong hàm main() rồi chạy mã.
val dwelling = Dwelling()
  1. Bạn sẽ gặp lỗi vì không thể tạo một thực thể của lớp Dwelling trừu tượng.
Cannot create an instance of an abstract class
  1. Xoá mã không chính xác này.

Mã của bạn cho đến thời điểm này:

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

Thêm một thuộc tính cho sức chứa

Một thuộc tính khác của nhà ở là sức chứa (capacity), tức là số lượng người có thể ở trong đó.

Tất cả nhà ở đều có sức chứa không thay đổi. Tuy nhiên, sức chứa không thể được đặt trong siêu lớp Dwelling. Sức chứa phải được khai báo trong các lớp con tương ứng với từng loại nhà ở cụ thể.

  1. Trong Dwelling, hãy thêm một abstract số nguyên val có tên capacity.
abstract val capacity: Int

Thêm một thuộc tính riêng tư cho số lượng cư dân

Mọi ngôi nhà đều có một số residents cư trú trong nhà (có thể nhỏ hơn hoặc bằng capacity), vì vậy hãy định nghĩa thuộc tính residents trong siêu lớp Dwelling để tất cả lớp con kế thừa và sử dụng.

  1. Bạn có thể đặt residents làm một tham số được truyền vào hàm khởi tạo của lớp Dwelling. Thuộc tính residentsvar, vì số lượng cư dân có thể thay đổi sau khi thực thể được tạo.
abstract class Dwelling(private var residents: Int) {

Xin lưu ý rằng thuộc tính residents được đánh dấu bằng từ khoá private. Riêng tư (private) là một đối tượng sửa đổi chế độ hiển thị (visibility modifier) trong Kotlin, có nghĩa là thuộc tính residents chỉ hiển thị (và dùng được) trong lớp này. Bạn không thể truy cập vào thuộc tính này ở nơi khác trong chương trình. Bạn có thể đánh dấu các thuộc tính hoặc phương thức bằng từ khoá private. Nếu bạn không chỉ định đối tượng sửa đổi chế độ hiển thị thì các thuộc tính và phương thức sẽ có giá trị là public theo mặc định, đồng thời bạn có thể truy cập vào qua các phần khác trong chương trình. Vì số người sống trong nhà thường là thông tin riêng tư (so với thông tin về vật liệu xây dựng hoặc sức chứa của toà nhà), nên đây là một quyết định hợp lý.

Cả capacity của nhà ở và số lượng residents hiện tại đều đã được định nghĩa, nên bạn có thể tạo hàm hasRoom() để xác định xem có còn chỗ ở cho người cư trú khác trong nhà hay không. Bạn có thể định nghĩa và triển khai hàm hasRoom() trong lớp Dwelling vì công thức tính xem có còn chỗ trống hay không có thể áp dụng cho tất cả nhà ở. Có chỗ trong Dwelling nếu số residents nhỏ hơn capacity, và hàm phải trả về true hoặc false dựa trên phép so sánh này.

  1. Thêm hàm hasRoom() vào lớp Dwelling.
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. Bạn có thể chạy mã này và không gặp lỗi. Hiện bạn chưa thấy gì.

Mã hoàn tất của bạn hiện có dạng như sau:

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. Tạo lớp con

Tạo một lớp con SquareCabin

  1. Bên dưới lớp Dwelling, hãy tạo một lớp có tên SquareCabin.
class SquareCabin
  1. Tiếp theo, bạn cần chỉ ra rằng SquareCabin có liên quan đến Dwelling. Trong mã, bạn nên chỉ ra rằng SquareCabin mở rộng từ Dwelling (hoặc là lớp con của Dwelling)SquareCabin sẽ đưa ra phương thức triển khai cho các phần trừu tượng của Dwelling.

Chỉ ra mối quan hệ kế thừa này bằng cách thêm dấu hai chấm (:) sau tên lớp SquareCabin, theo sau là một lệnh gọi để khởi tạo lớp cha Dwelling. Đừng quên thêm các dấu ngoặc đơn sau tên lớp Dwelling.

class SquareCabin : Dwelling()
  1. Khi mở rộng từ một siêu lớp, bạn phải truyền vào các tham số cần thiết mà siêu lớp đó dự kiến có. Dwelling cần có số residents làm dữ liệu đầu vào. Bạn có thể truyền vào một số lượng cư dân cố định như 3.
class SquareCabin : Dwelling(3)

Tuy nhiên, chương trình của bạn nên linh hoạt hơn và cho phép thay đổi số lượng cư dân trong SquareCabins. Do đó, hãy đặt residents làm một tham số trong định nghĩa lớp SquareCabin. Đừng khai báo residentsval, vì bạn đang sử dụng lại một thuộc tính đã được khai báo trong lớp cha Dwelling.

class SquareCabin(residents: Int) : Dwelling(residents)
  1. Chạy mã.
  2. Việc này sẽ gây ra lỗi. Hãy xem:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

Khi bạn khai báo các hàm và biến trừu tượng, điều này giống như hứa hẹn rằng sau này bạn sẽ cung cấp giá trị và phương thức triển khai cho những đối tượng này. Đối với một biến, như vậy tức là mọi lớp con của lớp trừu tượng đó đều phải cung cấp một giá trị cho biến đó. Đối với một hàm, như vậy tức là mọi lớp con đều phải triển khai nội dung của hàm.

Trong lớp Dwelling, bạn đã khai báo một biến abstractbuildingMaterial. SquareCabin là lớp con của Dwelling, vì vậy, lớp này phải cung cấp giá trị cho buildingMaterial. Hãy sử dụng từ khoá override để cho biết thuộc tính này đã được định nghĩa trong một lớp cha và sẽ bị ghi đè trong lớp này.

  1. Bên trong lớp SquareCabin, hãy override thuộc tính buildingMaterial rồi gán cho thuộc tính này giá trị "Wood".
  2. Làm tương tự với capacity, giả sử 6 cư dân có thể sống trong SquareCabin.
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Mã hoàn thiện của bạn sẽ có dạng như sau.

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Để kiểm thử mã, hãy tạo một thực thể của SquareCabin trong chương trình.

Sử dụng SquareCabin

  1. Chèn một hàm main() trống trước các định nghĩa lớp DwellingSquareCabin.
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. Trong hàm main(), hãy tạo một thực thể của SquareCabin có tên squareCabin gồm 6 cư dân. Thêm các câu lệnh in (print statement) dành cho vật liệu xây dựng, sức chứa và hàm hasRoom().
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

Xin lưu ý rằng hàm hasRoom() không được định nghĩa trong lớp SquareCabin nhưng đã được định nghĩa trong lớp Dwelling. Vì SquareCabin là lớp con của lớp Dwelling, nên hàm hasRoom() được kế thừa tự do. Hàm hasRoom() nay có thể được gọi trên tất cả thực thể của SquareCabin, như đã thấy trong đoạn mã dưới dạng squareCabin.hasRoom().

  1. Chạy mã của bạn và mã sẽ in nội dung sau:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Bạn đã tạo squareCabin với 6 cư dân, bằng với capacity, vì vậy hasRoom() sẽ trả về false. Bạn có thể thử nghiệm việc khởi tạo SquareCabin với số lượng residents ít hơn. Khi bạn chạy lại chương trình, hasRoom() sẽ trả về true.

Sử dụng with để đơn giản hoá mã

Trong các câu lệnh println(), mỗi khi bạn tham chiếu đến một thuộc tính hoặc hàm của squareCabin, hãy lưu ý cách bạn phải lặp lại squareCabin. Việc này có tính lặp đi lặp lại và có thể là nguồn lỗi khi bạn sao chép và dán các câu lệnh in.

Khi đang xử lý một thực thể cụ thể của một lớp và cần truy cập vào nhiều thuộc tính và chức năng của thực thể đó, bạn có thể sử dụng câu lệnh with để nói rằng "thực hiện tất cả thao tác sau đây cho đối tượng thực thể này". Bắt đầu bằng từ khoá with, theo sau là tên thực thể trong các dấu ngoặc đơn, sau đó là các dấu ngoặc nhọn chứa các thao tác bạn muốn thực hiện.

with (instanceName) {
    // all operations to do with instanceName
}
  1. Trong hàm main(), hãy thay đổi các câu lệnh in để sử dụng with.
  2. Xoá squareCabin. trong các câu lệnh in.
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. Chạy lại mã của bạn để đảm bảo mã đó chạy không có lỗi và cho thấy cùng một kết quả.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Đây là mã đã hoàn tất của bạn:

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Tạo một lớp con RoundHut

  1. Tương tự như SquareCabin, hãy thêm một lớp con khác (RoundHut) vào Dwelling.
  2. Ghi đè buildingMaterial rồi đặt giá trị "Straw".
  3. Ghi đè capacity rồi đặt thành 4.
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. Trong main(), hãy tạo một thực thể của RoundHut có 3 cư dân.
val roundHut = RoundHut(3)
  1. Thêm mã dưới đây để in thông tin về roundHut.
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. Chạy mã của bạn. Kết quả đầu ra cho toàn bộ chương trình sẽ là:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Bây giờ, bạn sẽ có một hệ phân cấp lớp có dạng thế này, với Dwelling là lớp gốc còn SquareCabinRoundHut là lớp con của Dwelling.

c19084f4a83193a0.png

Tạo một lớp con RoundTower

Lớp cuối cùng trong hệ phân cấp lớp này là một toà tháp hình tròn. Bạn có thể coi tháp tròn là một túp lều tròn bằng đá, có nhiều tầng. Do đó, bạn có thể đặt RoundTower làm lớp con của RoundHut.

  1. Tạo một lớp RoundTower là lớp con của RoundHut. Thêm tham số residents vào hàm khởi tạo của RoundTower, sau đó truyền tham số đó vào hàm khởi tạo của siêu lớp RoundHut.
  2. Ghi đè buildingMaterial thành "Stone".
  3. Đặt capacity thành 4.
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. Chạy mã này và bạn sẽ gặp lỗi.
This type is final, so it cannot be inherited from

Lỗi này nghĩa là không thể tạo lớp con của (hoặc kế thừa từ) lớp RoundHut. Theo mặc định, trong Kotlin, các lớp đều là lớp cuối cùng và không thể tạo lớp con được. Bạn chỉ được phép kế thừa từ các lớp abstract hoặc các lớp được đánh dấu bằng từ khoá open. Do đó, bạn cần đánh dấu lớp RoundHut bằng từ khoá open để cho phép các lớp khác có thể kế thừa từ lớp này.

  1. Thêm từ khoá open vào đầu phần khai báo RoundHut.
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. Trong main(), hãy tạo một thực thể của roundTower và in thông tin về thực thể đó.
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

Sau đây là mã hoàn chỉnh.

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. Chạy mã. Mã này đã hoạt động được mà không xảy ra lỗi và tạo ra kết quả sau đây.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

Thêm nhiều tầng vào RoundTower

RoundHut ám chỉ một toà nhà một tầng. Các toà tháp thường có nhiều tầng.

Khi nói đến sức chứa, toà tháp càng có nhiều tầng thì sức chứa càng lớn.

Bạn có thể sửa đổi RoundTower để có nhiều tầng và điều chỉnh sức chứa dựa trên số tầng.

  1. Cập nhật hàm khởi tạo RoundTower để lấy thêm một tham số dạng số nguyên val floors cho số tầng. Đặt sau residents. Lưu ý rằng bạn không cần truyền giá trị này cho hàm khởi tạo RoundHut của lớp cha vì floors được định nghĩa ở đây trong RoundTower còn RoundHut không có floors.
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. Chạy mã. Đã xảy ra lỗi khi tạo roundTower trong phương thức main() vì bạn chưa cung cấp một con số cho đối số floors. Bạn có thể thêm đối số bị thiếu.

Ngoài ra, trong phần định nghĩa lớp RoundTower, bạn có thể thêm giá trị mặc định cho floors như minh hoạ dưới đây. Sau đó, khi không có giá trị nào cho floors được truyền vào hàm khởi tạo, hệ thống có thể sử dụng giá trị mặc định để tạo thực thể đối tượng.

  1. Trong mã, hãy thêm = 2 sau phần khai báo của floors để gán cho đối tượng này một giá trị mặc định là 2.
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. Chạy mã. Mã này phải biên dịch được vì RoundTower(4) hiện tạo một thực thể đối tượng RoundTower có giá trị mặc định là 2 tầng.
  2. Trong lớp RoundTower, hãy cập nhật capacity để nhân với số tầng.
override val capacity = 4 * floors
  1. Chạy mã và nhận thấy sức chứa của RoundTower giờ đây là 8 cho 2 tầng.

Sau đây là mã đã hoàn tất.

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. Sửa đổi lớp trong hệ phân cấp

Tính diện tích sàn

Trong bài tập này, bạn sẽ tìm hiểu cách khai báo một hàm trừu tượng trong một lớp trừu tượng và sau đó triển khai chức năng của hàm đó trong các lớp con.

Mọi ngôi nhà đều có diện tích sàn (floor area), tuy nhiên cách tính diện tích sàn thì còn tuỳ thuộc vào hình dạng nhà ở.

Khai báo floorArea() trong lớp Dwelling

  1. Trước tiên, hãy thêm hàm abstract floorArea() vào lớp Dwelling. Trả về một Double. Double là một kiểu dữ liệu, như StringInt; kiểu dữ liệu này được sử dụng cho số thập phân, tức là các số có dấu thập phân theo sau là phần thập phân, chẳng hạn như 5,8793.
abstract fun floorArea(): Double

Tất cả phương thức trừu tượng được khai báo trong một lớp trừu tượng đều phải được triển khai trong mọi lớp con của lớp đó. Trước khi có thể chạy mã, bạn cần triển khai floorArea() trong các lớp con.

Triển khai floorArea() cho SquareCabin

Giống như buildingMaterialcapacity, vì bạn đang triển khai hàm abstract được định nghĩa trong lớp cha, nên bạn cần sử dụng từ khoá override.

  1. Trong lớp SquareCabin, hãy bắt đầu bằng từ khoá override, sau đó triển khai thực tế hàm floorArea() như minh hoạ dưới đây.
override fun floorArea(): Double {

}
  1. Trả về diện tích sàn đã tính. Diện tích của một hình chữ nhật hoặc hình vuông là chiều dài của một cạnh nhân với chiều dài của cạnh còn lại. Nội dung của hàm sẽ return length * length.
override fun floorArea(): Double {
    return length * length
}

Chiều dài (length) không phải là biến trong lớp và còn tuỳ theo thực thể, vì vậy bạn có thể thêm chiều dài làm tham số hàm khởi tạo cho lớp SquareCabin.

  1. Thay đổi định nghĩa lớp SquareCabin để thêm tham số length thuộc kiểu Double. Khai báo thuộc tính dưới dạng val vì chiều dài của một toà nhà không thay đổi.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

Dwelling và tất cả lớp con của lớp này có residents làm đối số hàm khởi tạo. Vì đây là đối số đầu tiên trong hàm khởi tạo Dwelling, nên phương pháp hay nhất là đặt đối số này làm đối số đầu tiên trong mọi hàm khởi tạo của các lớp con, đồng thời đặt các đối số theo cùng thứ tự trong mọi định nghĩa lớp. Do đó, hãy chèn tham số length mới sau tham số residents.

  1. Trong main(), hãy cập nhật việc tạo thực thể cho squareCabin. Truyền 50.0 vào hàm khởi tạo SquareCabin làm length.
val squareCabin = SquareCabin(6, 50.0)
  1. Bên trong câu lệnh with cho squareCabin, hãy thêm câu lệnh in diện tích sàn.
println("Floor area: ${floorArea()}")

Mã của bạn sẽ không chạy bởi vì bạn còn phải triển khai floorArea() trong RoundHut.

Triển khai floorArea() cho RoundHut

Tương tự như vậy, hãy triển khai diện tích sàn (floor area) cho RoundHut. RoundHut cũng là lớp con trực tiếp của Dwelling, vì vậy, bạn cần sử dụng từ khoá override.

Diện tích sàn của một ngôi nhà tròn là PI * bán kính * bán kính.

PI là một giá trị toán học. Giá trị này được xác định trong một thư viện toán học (math library). Thư viện (library) là một tập hợp hàm và giá trị xác định trước mà chương trình có thể sử dụng. Các hàm và giá trị này được định nghĩa bên ngoài chương trình. Để sử dụng hàm hoặc giá trị thư viện, bạn cần cho trình biên dịch (compiler) biết rằng bạn sẽ sử dụng hàm hoặc giá trị đó. Bạn thực hiện việc này bằng cách nhập hàm hoặc giá trị vào chương trình của mình. Để sử dụng PI trong chương trình, bạn cần nhập kotlin.math.PI.

  1. Nhập PI từ thư viện toán học Kotlin Đặt phần này ở đầu tệp, trước main().
import kotlin.math.PI
  1. Triển khai hàm floorArea() cho RoundHut.
override fun floorArea(): Double {
    return PI * radius * radius
}

Cảnh báo: Nếu bạn không nhập kotlin.math.PI, bạn sẽ gặp lỗi, vì vậy, hãy nhập thư viện này trước khi sử dụng. Ngoài ra, bạn có thể viết ra phiên bản PI đủ điều kiện, như trong kotlin.math.PI * bán kính * bán kính, sau đó không cần câu lệnh nhập.

  1. Cập nhật hàm khởi tạo RoundHut để truyền radius.
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. Trong main(), hãy cập nhật quá trình khởi tạo roundHut bằng cách truyền radius10.0 đến hàm khởi tạo RoundHut.
val roundHut = RoundHut(3, 10.0)
  1. Thêm một câu lệnh in bên trong câu lệnh with cho roundHut.
println("Floor area: ${floorArea()}")

Triển khai floorArea() cho RoundTower

Mã của bạn chưa chạy và gặp lỗi này:

Error: No value passed for parameter 'radius'

Trong RoundTower, để chương trình của bạn biên dịch được, bạn không cần triển khai floorArea() vì floorArea() được kế thừa từ RoundHut, nhưng bạn cần cập nhật định nghĩa lớp RoundTower để có cùng đối số radius với lớp cha RoundHut.

  1. Thay đổi hàm khởi tạo của RoundTower để lấy cả radius. Đặt radius sau residents và trước floors. Bạn nên liệt kê các biến có giá trị mặc định ở cuối. Đừng quên truyền radius đến hàm khởi tạo lớp mẹ.
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. Cập nhật quá trình khởi tạo của roundTower trong main().
val roundTower = RoundTower(4, 15.5)
  1. Và thêm câu lệnh in để gọi floorArea().
println("Floor area: ${floorArea()}")
  1. Bây giờ, bạn có thể chạy mã!
  2. Xin lưu ý rằng cách tính cho RoundTower là không chính xác vì được kế thừa từ RoundHut và không tính đến số floors.
  3. Trong RoundTower, hãy override floorArea() để có thể triển khai theo một cách khác: nhân diện tích sàn với số tầng. Hãy lưu ý cách bạn có thể định nghĩa hàm trong một lớp trừu tượng (Dwelling), triển khai hàm này trong lớp con (RoundHut) rồi ghi đè lại trong lớp con của lớp con (RoundTower). Đây là điểm ưu việt nhất của cả hai phương thức – bạn kế thừa chức năng mà bạn muốn đồng thời có thể ghi đè chức năng mà bạn không muốn.
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

Mã này hoạt động, nhưng có một cách để tránh việc lặp lại mã đã có trong lớp mẹ RoundHut. Bạn có thể gọi hàm floorArea() từ lớp cha RoundHut để trả về PI * radius * radius. Sau đó, nhân kết quả đó với số floors.

  1. Trong RoundTower, hãy cập nhật floorArea() để sử dụng phương thức triển khai siêu lớp floorArea(). Sử dụng từ khoá super để gọi hàm được khai báo trong lớp mẹ.
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. Chạy lại mã và RoundTower tính ra diện tích sàn chính xác cho nhiều tầng.

Sau đây là mã hoàn tất của bạn:

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

Kết quả đầu ra phải là:

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

Cho phép người cư trú mới nhận phòng

Thêm khả năng cho một cư dân mới nhận phòng bằng hàm getRoom() làm tăng số lượng cư dân thêm một. Vì logic này là như nhau đối với tất cả nhà ở, nên bạn có thể triển khai hàm này trong Dwelling. Điều này làm cho hàm getRoom() được cung cấp cho tất cả lớp con và lớp con của lớp con. Tuyệt!

Lưu ý:

  • Sử dụng một câu lệnh if chỉ thêm cư dân nếu còn đủ sức chứa.
  • In một thông điệp cho kết quả.
  • Bạn có thể sử dụng residents++ (là viết tắt của residents = residents + 1) để cộng 1 vào biến residents.
  1. Triển khai hàm getRoom() trong lớp Dwelling.
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. Thêm một số câu lệnh in vào khối câu lệnh with cho roundHut để quan sát xem điều gì xảy ra cho getRoom()hasRoom() khi được sử dụng cùng nhau.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

Kết quả đầu ra cho các câu lệnh in như sau:

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

Hãy xem mã giải pháp để biết thông tin chi tiết.

Đặt một tấm thảm vừa với một ngôi nhà tròn

Giả sử bạn cần biết chiều dài một cạnh của thảm cho RoundHut hoặc RoundTower. Đặt hàm này vào RoundHut để cung cấp cho tất cả ngôi nhà tròn.

2e328a198a82c793.png

  1. Trước tiên, hãy nhập hàm sqrt() từ thư viện kotlin.math.
import kotlin.math.sqrt
  1. Triển khai hàm calculateMaxCarpetLength() trong lớp RoundHut. Công thức tính chiều dài của hình vuông có thể nằm trong hình tròn là sqrt(2) * radius. Điều này được giải thích trong sơ đồ trên.
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

Truyền giá trị Double, 2.0 đến hàm toán học sqrt(2.0), vì loại trả về của hàm là Double không phải Integer.

  1. Phương thức calculateMaxCarpetLength() hiện có thể được gọi trên các thực thể RoundHutRoundTower. Thêm các câu lệnh in vào roundHutroundTower trong hàm main().
println("Carpet Length: ${calculateMaxCarpetLength()}")

Hãy xem mã giải pháp để biết thông tin chi tiết.

Xin chúc mừng! Bạn đã tạo được một hệ phân cấp lớp hoàn chỉnh với các thuộc tính và hàm, giờ hãy tìm hiểu mọi thứ bạn cần để tạo những lớp hữu ích hơn!

6. Mã giải pháp

Đây là mã giải pháp hoàn chỉnh cho lớp học lập trình này, bao gồm cả nhận xét.

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. Tóm tắt

Trong lớp học lập trình này, bạn đã tìm hiểu cách:

  • Tạo một hệ phân cấp lớp, còn gọi là cây phả hệ lớp, nơi lớp con kế thừa chức năng của lớp cha. Các thuộc tính và hàm được kế thừa trong các lớp con.
  • Tạo một lớp abstract mà trong đó một số chức năng còn lại sẽ được các lớp con của lớp đó thực hiện. Do đó, bạn không thể tạo thực thể cho một lớp abstract.
  • Tạo lớp con của lớp abstract.
  • Sử dụng từ khoá override để ghi đè thuộc tính và hàm trong lớp con.
  • Sử dụng từ khoá super để tham chiếu các hàm và thuộc tính trong lớp cha.
  • Làm cho một lớp trở thành open để có thể tạo lớp con.
  • Làm cho một thuộc tính trở thành private để chỉ có thể được sử dụng bên trong lớp đó.
  • Sử dụng cấu trúc with để thực hiện nhiều lệnh gọi trên cùng một thực thể đối tượng.
  • Nhập chức năng từ thư viện kotlin.math

8. Tìm hiểu thêm