Sử dụng các lớp và đối tượng trong Kotlin

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

Lớp học lập trình này hướng dẫn bạn cách sử dụng các lớp và đối tượng trong Kotlin.

Các lớp cung cấp bản thiết kế để tạo ra các đối tượng. Đối tượng là một thực thể của lớp bao gồm dữ liệu dành riêng cho đối tượng đó. Bạn có thể sử dụng hai khái niệm "đối tượng" hoặc "thực thể của lớp" thay thế cho nhau.

Để dễ hình dung, hãy tưởng tượng bạn đang xây một ngôi nhà. Lớp tương tự như bản vẽ của kiến trúc sư, hay còn gọi là bản thiết kế. Bản thiết kế tự thân nó không phải là một ngôi nhà mà là hướng dẫn về cách xây nhà. Ngôi nhà là vật thể thực, hay "đối tượng" được xây dựng dựa trên bản thiết kế.

Giống như bản thiết kế nhà có nhiều phòng và mỗi phòng được thiết kế với mục đích sử dụng riêng, mỗi lớp cũng có thiết kế và mục đích riêng của nó. Để biết cách thiết kế các lớp, bạn cần làm quen với lập trình hướng đối tượng (OOP), một khung hướng dẫn bạn cách đóng gói dữ liệu, logic và hành vi vào trong đối tượng.

OOP giúp bạn đơn giản hoá các vấn đề phức tạp trong thế giới thực thành những đối tượng nhỏ hơn. Có bốn khái niệm cơ bản về OOP mà bạn sẽ tìm hiểu thêm trong lớp học lập trình này:

  • Tính đóng gói. Bao bọc các thuộc tính có liên quan và các phương thức thực hiện hành động trên các thuộc tính đó vào trong một lớp. Lấy điện thoại di động của bạn làm ví dụ. Nó đóng gói máy ảnh, màn hình, thẻ nhớ cùng một số phần cứng và phần mềm khác. Bạn không cần phải lo lắng về cách các thành phần bên trong kết hợp với nhau.
  • Tính trừu tượng. Là phần mở rộng của tính đóng gói, với mục đích là ẩn logic triển khai bên trong càng nhiều càng tốt. Ví dụ: để chụp ảnh bằng điện thoại di động, bạn chỉ cần mở ứng dụng máy ảnh, hướng điện thoại đến nơi muốn chụp và nhấp vào nút để chụp ảnh. Bạn không cần phải biết cách phát triển ứng dụng máy ảnh hoặc phần cứng máy ảnh trên điện thoại di động thực tế hoạt động như thế nào. Tóm lại, cơ chế nội bộ của ứng dụng máy ảnh và cách máy ảnh trên thiết bị di động chụp ảnh đã được trừu tượng hoá để bạn thực hiện các tác vụ cần thiết.
  • Tính kế thừa. Cho phép bạn tạo lớp dựa trên đặc điểm và hành vi của các lớp khác bằng cách thiết lập mối quan hệ cha-con. Ví dụ: có nhiều nhà sản xuất chế tạo ra nhiều thiết bị di động chạy Android OS nhưng giao diện người dùng cho từng thiết bị là khác nhau. Nói cách khác, nhà sản xuất kế thừa tính năng của hệ điều hành Android và tuỳ chỉnh dựa trên tính năng đó.
  • Tính đa hình. Từ này bắt nguồn từ tiếng Hy Lạp, với gốc poly- có nghĩa là nhiều, và -morphism nghĩa là hình thái. Tính đa hình là khả năng sử dụng các đối tượng khác nhau theo một cách chung. Ví dụ: khi bạn kết nối loa Bluetooth với điện thoại di động, điện thoại chỉ cần biết có một thiết bị có thể phát âm thanh bằng Bluetooth. Tuy nhiên, có nhiều loa Bluetooth để bạn chọn và điện thoại không cần phải biết cụ thể cách làm việc với từng loa.

Cuối cùng, bạn sẽ tìm hiểu về uỷ quyền thuộc tính, giúp cung cấp mã có thể sử dụng lại để quản lý các giá trị thuộc tính bằng một cú pháp ngắn gọn. Trong lớp học lập trình này, bạn sẽ học những khái niệm này khi xây dựng cấu trúc lớp cho ứng dụng nhà thông minh.

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

  • Cách mở, chỉnh sửa và chạy mã trong Kotlin Playground.
  • Kiến thức cơ bản về lập trình Kotlin, bao gồm các biến, hàm, cùng với các hàm println()main()

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

  • Tổng quan về OOP.
  • Định nghĩa về lớp.
  • Cách định nghĩa một lớp bằng hàm khởi tạo, hàm và thuộc tính.
  • Cách khởi tạo một đối tượng.
  • Định nghĩa về tính kế thừa.
  • Sự khác biệt giữa mối quan hệ IS-A và HAS-A.
  • Cách ghi đè thuộc tính và hàm.
  • Định nghĩa về các từ khoá quy định phạm vi truy cập.
  • Uỷ quyền là gì và cách sử dụng tính năng uỷ quyền thông qua từ khoá by.

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

  • Cấu trúc lớp trong nhà thông minh.
  • Lớp đại diện cho các thiết bị thông minh, chẳng hạn như TV thông minh và đèn thông minh.

Bạn cần có

  • Máy tính có kết nối Internet và trình duyệt web

2. Định nghĩa một lớp

Khi định nghĩa một lớp, bạn sẽ chỉ định các thuộc tính và phương thức mà tất cả các đối tượng của lớp đó phải có.

Định nghĩa lớp bắt đầu bằng từ khoá class, theo sau là tên lớp và một cặp dấu ngoặc nhọn. Phần cú pháp trước dấu ngoặc nhọn còn được gọi là class header (phần khai báo đầu của lớp). Trong dấu ngoặc nhọn, bạn có thể chỉ định các thuộc tính và hàm cho lớp. Bạn sẽ sớm tìm hiểu về các thuộc tính và hàm. Bạn có thể xem cú pháp định nghĩa một lớp trong biểu đồ này:

Định nghĩa này bắt đầu bằng từ khoá class, theo sau là tên lớp và một cặp dấu ngoặc nhọn. Dấu ngoặc nhọn chứa phần thân của lớp, dùng để mô tả bản thiết kế của lớp đó.

Dưới đây là những quy ước đặt tên được đề xuất cho một lớp:

  • Bạn có thể chọn bất kỳ tên lớp nào mình muốn, nhưng không được dùng từ khoá của Kotlin làm tên lớp, chẳng hạn như từ khoá fun.
  • Tên lớp được viết theo kiểu PascalCase, vì vậy, mỗi từ bắt đầu bằng một chữ cái viết hoa và không có dấu cách giữa các từ. Ví dụ: trong SmartDevice, chữ cái đầu tiên của mỗi từ được viết hoa và không có dấu cách giữa các từ.

Một lớp bao gồm 3 phần chính:

  • Thuộc tính. Các biến chỉ định thuộc tính của đối tượng của lớp.
  • Phương thức. Các hàm chứa hành vi và hành động của lớp.
  • Hàm khởi tạo. Một hàm thành viên đặc biệt dùng để tạo ra các thực thể của lớp trong suốt chương trình nơi nó được định nghĩa.

Đây không phải là lần đầu tiên bạn làm việc với các lớp. Trong các lớp học lập trình trước đây, bạn đã tìm hiểu về các kiểu dữ liệu, chẳng hạn như kiểu dữ liệu Int, Float, StringDouble. Các kiểu dữ liệu này được định nghĩa là các lớp trong Kotlin. Khi định nghĩa một biến như trong đoạn mã dưới đây, bạn đang tạo một đối tượng của lớp Int, đối tượng này được khởi tạo với giá trị là 1:

val number: Int = 1

Định nghĩa lớp SmartDevice:

  1. Trong Kotlin Playground, hãy thay nội dung bằng một hàm main() trống:
fun main() {
}
  1. Ở dòng ngay trước hàm main(), hãy định nghĩa một lớp SmartDevice với phần thân bao gồm một bình luận // empty body:
class SmartDevice {
    // empty body
}

fun main() {
}

3. Tạo một thực thể của lớp

Như bạn đã biết, lớp là bản thiết kế cho một đối tượng. Môi trường thời gian chạy của Kotlin sử dụng lớp, hay bản thiết kế đó, để tạo ra một đối tượng thuộc kiểu cụ thể. Với lớp SmartDevice, bạn sẽ có bản thiết kế của một thiết bị thông minh. Để có thiết bị thông minh thực sự trong chương trình, bạn cần tạo một thực thể đối tượng SmartDevice. Cú pháp khởi tạo bắt đầu bằng tên lớp, theo sau là một cặp dấu ngoặc đơn như bạn có thể thấy trong biểu đồ dưới đây:

1d25bc4f71c31fc9.png

Để sử dụng một đối tượng, bạn sẽ tạo đối tượng và gán đối tượng đó cho một biến, tương tự như cách bạn định nghĩa một biến. Bạn dùng từ khoá val để tạo một biến bất biến và từ khoá var cho biến có thể thay đổi. Theo sau từ khoá val hoặc var là tên của biến, tiếp đến là toán tử gán = và cuối cùng là phần khởi tạo đối tượng của lớp. Bạn có thể xem cú pháp trong biểu đồ này:

f58430542f2081a9.png

Khởi tạo lớp SmartDevice dưới dạng một đối tượng:

  • Trong hàm main(), hãy dùng từ khoá val để tạo một biến có tên smartTvDevice và khởi tạo biến đó như một thực thể của lớp SmartDevice:
fun main() {
    val smartTvDevice = SmartDevice()
}

4. Định nghĩa các phương thức của lớp

Trong Bài 1, bạn đã được học về:

  • Định nghĩa một hàm sử dụng từ khoá fun theo sau là cặp dấu ngoặc đơn và dấu ngoặc nhọn. Cặp dấu ngoặc nhọn chứa mã, là hướng dẫn cần thiết để thực thi một tác vụ.
  • Việc gọi một hàm sẽ làm cho đoạn mã nằm bên trong hàm đó được thực thi.

Các hành động mà lớp có thể thực hiện được định nghĩa là các hàm bên trong lớp đó. Ví dụ: hãy tưởng tượng bạn sở hữu một thiết bị thông minh, TV thông minh hoặc đèn thông minh và bạn có thể bật/tắt thiết bị đó bằng điện thoại di động. Thiết bị thông minh này được chuyển thành lớp SmartDevice trong quá trình lập trình. Thao tác bật/tắt thiết bị này sẽ được biểu thị bằng hàm turnOn() và hàm turnOff(), cho phép kích hoạt hành vi bật/tắt đó.

Cú pháp để định nghĩa một hàm trong một lớp hoàn toàn giống với những gì bạn đã học trước đây. Điểm khác biệt duy nhất là hàm được đặt trong phần thân của lớp. Khi bạn định nghĩa một hàm trong phần thân lớp, hàm đó được gọi là hàm thành viên hoặc phương thức và đại diện cho hành vi của lớp. Trong phần còn lại của lớp học lập trình này, các hàm sẽ được gọi là phương thức bất cứ khi nào chúng xuất hiện trong phần thân của một lớp.

Định nghĩa phương thức turnOn()turnOff() trong lớp SmartDevice:

  1. Trong phần thân của lớp SmartDevice, hãy định nghĩa một phương thức turnOn() với phần thân trống:
class SmartDevice {
    fun turnOn() {

    }
}
  1. Trong phần thân của phương thức turnOn(), hãy thêm một câu lệnh println() rồi truyền cho nó chuỗi "Smart device is turned on.":
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. Sau phương thức turnOn(), hãy thêm một phương thức turnOff() để in chuỗi "Smart device is turned off.":
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

Gọi một phương thức trên một đối tượng

Cho đến phần này, bạn đã định nghĩa một lớp đóng vai trò là bản thiết kế cho một thiết bị thông minh, tạo một thực thể của lớp và gán thực thể đó cho một biến. Giờ đây, bạn có thể dùng phương thức của lớp SmartDevice để bật và tắt thiết bị.

Việc gọi một phương thức trong một lớp cũng tương tự như cách bạn gọi các hàm khác từ hàm main() trong lớp học lập trình trước. Ví dụ: nếu cần gọi phương thức turnOff() từ phương thức turnOn(), bạn có thể viết nội dung tương tự như đoạn mã này:

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

Để gọi một phương thức của lớp từ bên ngoài lớp đó, hãy bắt đầu bằng đối tượng của lớp, theo sau là toán tử ., tên hàm và một cặp dấu ngoặc đơn. Nếu có, cặp dấu ngoặc đơn sẽ chứa các đối số mà phương thức yêu cầu. Bạn có thể xem cú pháp trong biểu đồ này:

fc609c15952551ce.png

Gọi các phương thức turnOn()turnOff() trên đối tượng:

  1. Trong hàm main(), ở dòng ngay sau biến smartTvDevice, hãy gọi phương thức turnOn():
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. Ở dòng ngay sau phương thức turnOn(), hãy gọi phương thức turnOff():
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Chạy mã.

Kết quả sẽ như sau:

Smart device is turned on.
Smart device is turned off.

5. Định nghĩa các thuộc tính của lớp

Trong Bài 1, bạn đã học về các biến, đóng vai trò là nơi lưu trữ cho các phần dữ liệu đơn lẻ. Bạn đã tìm hiểu cách tạo một biến chỉ có thể đọc bằng từ khoá val và biến có thể thay đổi bằng từ khoá var.

Trong khi các phương thức định nghĩa những hành động mà một lớp có thể thực hiện, thì các thuộc tính định nghĩa các đặc điểm hoặc thuộc tính dữ liệu của lớp đó. Ví dụ: một thiết bị thông minh có các thuộc tính sau:

  • Tên. Tên thiết bị.
  • Loại thiết bị. Loại thiết bị thông minh, chẳng hạn như giải trí, tiện ích hoặc nấu ăn.
  • Trạng thái thiết bị. Thiết bị đang bật, tắt, trực tuyến hay ngoại tuyến. Thiết bị được coi là trực tuyến khi kết nối với Internet. Nếu không, nó sẽ được xem là ngoại tuyến.

Về cơ bản, thuộc tính là các biến được định nghĩa bên trong phần thân của lớp thay vì phần thân của hàm. Điều này có nghĩa là cú pháp để định nghĩa thuộc tính và biến giống hệt nhau. Bạn định nghĩa một thuộc tính bất biến bằng từ khoá val và thuộc tính có thể thay đổi bằng từ khoá var.

Triển khai các đặc điểm nói trên thành các thuộc tính của lớp SmartDevice:

  1. Ở dòng trước phương thức turnOn(), hãy định nghĩa thuộc tính name và gán cho thuộc tính này chuỗi "Android TV":
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. Ở dòng sau thuộc tính name, hãy định nghĩa thuộc tính category và gán cho thuộc tính đó chuỗi "Entertainment", sau đó định nghĩa thuộc tính deviceStatus và gán cho thuộc tính chuỗi "online":
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. Ở dòng sau biến smartTvDevice, hãy gọi hàm println() rồi truyền cho hàm này chuỗi "Device name is: ${smartTvDevice.name}":
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Chạy mã.

Kết quả sẽ như sau:

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

Hàm getter và setter trong các thuộc tính

Thuộc tính có thể làm được nhiều việc hơn một biến. Ví dụ: giả sử bạn tạo một cấu trúc lớp để đại diện cho một TV thông minh. Một trong những hành động phổ biến bạn thực hiện là tăng và giảm âm lượng. Để thể hiện hành động này trong lập trình, bạn có thể tạo một thuộc tính có tên là speakerVolume. Thuộc tính này giữ mức âm lượng hiện tại được đặt trên loa TV, nhưng có phạm vi giá trị cho âm lượng. Âm lượng tối thiểu có thể đặt là 0 và tối đa là 100. Để đảm bảo thuộc tính speakerVolume không bao giờ vượt quá 100 hoặc thấp hơn 0, bạn có thể viết một hàm setter. Khi cập nhật giá trị của thuộc tính, bạn cần kiểm tra xem giá trị đó có nằm trong khoảng từ 0 đến 100 hay không. Một ví dụ khác, hãy tưởng tượng có một yêu cầu để đảm bảo tên luôn phải viết hoa. Bạn có thể triển khai hàm getter để chuyển đổi thuộc tính name thành chữ viết hoa.

Trước khi tìm hiểu sâu hơn về cách triển khai các thuộc tính này, bạn cần hiểu rõ cú pháp để khai báo thuộc tính. Cú pháp đầy đủ để định nghĩa một thuộc tính có thể thay đổi bắt đầu bằng định nghĩa biến, theo sau là các hàm get()set() (không bắt buộc). Bạn có thể xem cú pháp trong biểu đồ này:

f2cf50a63485599f.png

Khi bạn không định nghĩa hàm getter và setter cho một thuộc tính, trình biên dịch Kotlin sẽ tự động tạo các hàm này ở bên trong. Ví dụ: nếu bạn sử dụng từ khoá var để định nghĩa thuộc tính speakerVolume và gán giá trị 2 cho thuộc tính đó, trình biên dịch sẽ tự động tạo các hàm getter và setter như có thể thấy trong đoạn mã dưới đây:

var speakerVolume = 2
    get() = field  
    set(value) {
        field = value    
    }

Bạn sẽ không thấy những dòng này trong mã của mình vì trình biên dịch đã thêm chúng vào nền.

Cú pháp đầy đủ cho thuộc tính bất biến có hai điểm khác biệt:

  • Nó bắt đầu với từ khoá val.
  • Các biến thuộc kiểu val là các biến chỉ đọc, do đó, chúng không có hàm set().

Các thuộc tính trong Kotlin sử dụng trường dự phòng để lưu giữ giá trị trong bộ nhớ. Trường dự phòng về cơ bản là một biến của lớp được định nghĩa ngầm bên trong các thuộc tính. Trường dự phòng nằm trong phạm vi một thuộc tính, có nghĩa là bạn chỉ có thể truy cập vào trường đó thông qua các hàm get() hoặc set() của thuộc tính đó.

Để đọc giá trị thuộc tính trong hàm get() hoặc cập nhật giá trị trong hàm set(), bạn cần sử dụng trường dự phòng của thuộc tính. Nó được trình biên dịch Kotlin tạo tự động và được tham chiếu bằng định danh field.

Ví dụ: khi muốn cập nhật giá trị của thuộc tính trong hàm set(), bạn sử dụng tham số của hàm set() (còn được gọi là tham số value) và gán giá trị đó cho biến field như bạn có thể thấy trong đoạn mã này:

var speakerVolume = 2
    set(value) {
        field = value    
    }

Ví dụ: để đảm bảo giá trị được gán cho thuộc tính speakerVolume nằm trong khoảng từ 0 đến 100, bạn có thể triển khai hàm setter như có thể thấy trong đoạn mã dưới đây:

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

Hàm set() kiểm tra xem giá trị Int có nằm trong phạm vi từ 0 đến 100 hay không bằng cách dùng từ khoá in theo sau là khoảng giá trị. Nếu giá trị nằm trong phạm vi dự kiến, thì giá trị field sẽ được cập nhật. Nếu không, giá trị của thuộc tính sẽ không thay đổi.

Bạn sẽ thêm thuộc tính này vào một lớp trong phần Triển khai mối quan hệ giữa các lớp của lớp học lập trình này, do đó, bạn hiện chưa cần phải thêm hàm setter vào mã.

6. Định nghĩa hàm khởi tạo

Mục đích chính của hàm khởi tạo là xác định cách các đối tượng của lớp được tạo ra. Nói cách khác, hàm khởi tạo sẽ tạo một đối tượng và khiến đối tượng đó sẵn sàng để sử dụng. Bạn đã làm điều này khi khởi tạo đối tượng. Mã bên trong hàm khởi tạo được thực thi khi đối tượng của lớp được khởi tạo. Bạn có thể định nghĩa một hàm khởi tạo có hoặc không có tham số.

Hàm khởi tạo mặc định

Hàm khởi tạo mặc định là hàm khởi tạo không có tham số. Bạn có thể định nghĩa một hàm khởi tạo mặc định như trong đoạn mã này:

class SmartDevice constructor() {
    ...
}

Kotlin hướng đến sự ngắn gọn, vì vậy bạn có thể bỏ từ khoá constructor nếu không có các chú giải hoặc các từ khoá quy định phạm vi truy cập (bạn sẽ sớm được tìm hiểu về nó) gắn trên hàm khởi tạo. Bạn cũng có thể bỏ dấu ngoặc đơn nếu hàm khởi tạo không có tham số như minh hoạ trong đoạn mã dưới đây:

class SmartDevice {
    ...
}

Trình biên dịch Kotlin sẽ tự động tạo hàm khởi tạo mặc định. Bạn sẽ không thấy hàm khởi tạo mặc định được tạo tự động trong mã vì hàm đó đã được trình biên dịch thêm vào trong nền.

Định nghĩa hàm khởi tạo có tham số

Trong lớp SmartDevice, các thuộc tính namecategory là bất biến. Bạn cần đảm bảo tất cả các thực thể của lớp SmartDevice đều khởi tạo các thuộc tính namecategory. Bằng cách triển khai hiện tại, các giá trị của thuộc tính namecategory đang được gán cứng. Điều này có nghĩa là tất cả thiết bị thông minh được đặt tên bằng chuỗi "Android TV" và được phân loại bằng chuỗi "Entertainment".

Để duy trì tính bất biến nhưng tránh các giá trị được gán cứng, hãy dùng một hàm khởi tạo có tham số để khởi tạo chúng:

  • Trong lớp SmartDevice, hãy di chuyển các thuộc tính namecategory vào hàm khởi tạo mà không gán giá trị mặc định cho chúng:
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

Hàm khởi tạo bây giờ đã chấp nhận các tham số để thiết lập thuộc tính, vì vậy cách khởi tạo một đối tượng cho lớp này cũng thay đổi theo. Bạn có thể xem cú pháp đầy đủ để khởi tạo một đối tượng trong biểu đồ này:

bbe674861ec370b6.png

Dưới đây là nội dung biểu thị mã:

SmartDevice("Android TV", "Entertainment")

Cả hai đối số truyền vào hàm khởi tạo đều là chuỗi. Điều này làm cho việc xác định giá trị nào nên được gán cho tham số nào trở nên thiếu rõ ràng. Để khắc phục lỗi này, tương tự như cách bạn truyền đối số cho hàm, bạn có thể tạo một hàm khởi tạo với các đối số được đặt tên như minh hoạ trong đoạn mã dưới đây:

SmartDevice(name = "Android TV", category = "Entertainment")

Có hai loại hàm khởi tạo chính trong Kotlin:

  • Hàm khởi tạo chính. Một lớp chỉ có thể có một hàm khởi tạo chính, được định nghĩa như một phần của phần khai báo đầu của lớp (class header). Một hàm khởi tạo chính có thể là một hàm khởi tạo mặc định hoặc có tham số. Hàm khởi tạo chính không có phần thân, đồng nghĩa với việc nó không thể chứa bất kỳ đoạn mã nào.
  • Hàm khởi tạo phụ. Một lớp có thể có nhiều hàm khởi tạo phụ. Bạn có thể định nghĩa hàm khởi tạo phụ có hoặc không có tham số. Hàm khởi tạo phụ có thể khởi tạo lớp và có phần thân để chứa logic khởi tạo. Nếu lớp đã có hàm khởi tạo chính, mỗi hàm khởi tạo phụ cần phải khởi tạo (gọi đến) hàm khởi tạo chính đó.

Bạn có thể dùng hàm khởi tạo chính để tạo các thuộc tính trong phần khai báo đầu của lớp. Các đối số truyền vào hàm khởi tạo sẽ được gán cho các thuộc tính. Cú pháp để định nghĩa một hàm khởi tạo chính bắt đầu bằng tên lớp, theo sau là từ khoá constructor và một cặp dấu ngoặc đơn. Cặp dấu ngoặc đơn chứa các tham số cho hàm khởi tạo chính. Nếu có nhiều hơn một tham số, các định nghĩa tham số được phân tách bằng dấu phẩy. Bạn có thể xem cú pháp đầy đủ để định nghĩa hàm khởi tạo chính trong biểu đồ này:

aa05214860533041.png

Hàm khởi tạo phụ được đặt trong phần thân của lớp và cú pháp của hàm này bao gồm 3 phần:

  • Khai báo hàm khởi tạo phụ. Định nghĩa hàm khởi tạo phụ bắt đầu bằng từ khoá constructor, theo sau là dấu ngoặc đơn. Nếu có, cặp dấu ngoặc đơn sẽ chứa các tham số mà hàm khởi tạo phụ yêu cầu.
  • Khởi tạo hàm khởi tạo chính. Khởi tạo bắt đầu bằng dấu hai chấm, theo sau là từ khoá this và một cặp dấu ngoặc đơn. Nếu có, cặp dấu ngoặc đơn sẽ chứa các tham số mà hàm khởi tạo chính yêu cầu.
  • Phần thân hàm khởi tạo phụ. Khởi tạo hàm khởi tạo chính, theo sau là một cặp dấu ngoặc nhọn, chứa phần thân của hàm khởi tạo phụ.

Bạn có thể xem cú pháp trong biểu đồ này:

2dc13ef136009e98.png

Ví dụ: giả sử bạn muốn tích hợp API do một nhà cung cấp thiết bị thông minh phát triển. Tuy nhiên, API này trả về mã trạng thái kiểu Int để cho biết trạng thái thiết bị ban đầu. API trả về giá trị 0 nếu thiết bị ngoại tuyến và giá trị 1 nếu thiết bị đó trực tuyến. Đối với bất kỳ giá trị số nguyên nào khác, trạng thái được coi là không xác định. Bạn có thể tạo một hàm khởi tạo phụ trong lớp SmartDevice để chuyển đổi tham số statusCode này thành dạng chuỗi như có thể thấy trong đoạn mã dưới đây:

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. Triển khai mối quan hệ giữa các lớp

Tính kế thừa cho phép bạn tạo một lớp dựa trên đặc điểm và hành vi của một lớp khác. Đây là một cơ chế hiệu quả giúp bạn viết mã có thể sử dụng lại và thiết lập mối quan hệ giữa các lớp.

Ví dụ: có nhiều thiết bị thông minh trên thị trường, chẳng hạn như TV thông minh, đèn thông minh và công tắc thông minh. Khi bạn biểu diễn các thiết bị thông minh trong lập trình, chúng sẽ dùng chung một số thuộc tính như tên, danh mục và trạng thái. Chúng cũng có những hành vi chung, chẳng hạn như khả năng bật và tắt.

Tuy nhiên, cách bật hoặc tắt từng thiết bị thông minh sẽ khác nhau. Ví dụ: để bật TV, bạn có thể phải bật màn hình, sau đó thiết lập mức âm lượng và kênh đã mở lần gần nhất. Mặt khác, để bật đèn, bạn có thể chỉ cần tăng hoặc giảm độ sáng.

Ngoài ra, mỗi thiết bị thông minh có nhiều chức năng và thao tác thực hiện hơn. Ví dụ: đối với TV, bạn có thể điều chỉnh âm lượng và thay đổi kênh. Với đèn, bạn có thể điều chỉnh độ sáng hoặc màu sắc.

Tóm lại, tất cả các thiết bị thông minh đều có các tính năng khác nhau, nhưng có chung một số đặc điểm. Bạn có thể sao chép các đặc điểm chung này tới từng lớp thiết bị thông minh hoặc làm cho mã có thể tái sử dụng thông qua tính kế thừa.

Để làm việc này, bạn cần tạo một lớp cha SmartDevice và định nghĩa các thuộc tính cũng như hành vi chung này. Sau đó, bạn có thể tạo các lớp con, chẳng hạn như lớp SmartTvDeviceSmartLightDevice. Các lớp này kế thừa các thuộc tính của lớp cha.

Trong thuật ngữ lập trình, chúng ta nói các lớp SmartTvDeviceSmartLightDevice kế thừa từ lớp cha SmartDevice. Lớp cha còn được gọi là lớp cơ sở và lớp con được gọi là lớp dẫn xuất. Bạn có thể thấy mối quan hệ giữa chúng trong biểu đồ này:

Biểu đồ thể hiện mối quan hệ kế thừa giữa các lớp.

Tuy nhiên, trong Kotlin, theo mặc định, tất cả các lớp đều là final, nghĩa là bạn không thể mở rộng chúng, vì vậy bạn phải xác định mối quan hệ giữa các lớp.

Định nghĩa mối quan hệ giữa lớp cha SmartDevice và các lớp con của nó:

  1. Trong lớp cha SmartDevice, hãy thêm từ khoá open trước từ khoá class để cho phép nó có thể được mở rộng:
open class SmartDevice(val name: String, val category: String) {
    ...
}

Từ khoá open thông báo cho trình biên dịch rằng lớp này có thể được mở rộng, vì vậy giờ đây các lớp khác có thể kế thừa nó.

Cú pháp để tạo lớp con bắt đầu bằng việc tạo phần khai báo đầu của lớp như bạn đã làm từ trước đến nay. Sau dấu ngoặc đơn đóng của hàm khởi tạo là một khoảng trắng, dấu hai chấm, một khoảng trắng khác, tên của lớp cha và một cặp dấu ngoặc đơn. Nếu cần, cặp dấu ngoặc đơn này sẽ bao gồm các tham số mà hàm khởi tạo của lớp cha yêu cầu. Bạn có thể xem cú pháp trong biểu đồ này:

1ac63b66e6b5c224.png

  1. Tạo lớp con SmartTvDevice kế thừa từ lớp cha SmartDevice:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

Việc định nghĩa constructor cho SmartTvDevice không chỉ định xem các thuộc tính này có thể thay đổi hay bất biến. Điều này có nghĩa là các tham số deviceNamedeviceCategory chỉ đơn thuần là các tham số của constructor thay vì là thuộc tính của lớp. Bạn sẽ không thể dùng chúng trong lớp, mà chỉ đơn giản là truyền chúng cho hàm khởi tạo của lớp cha.

  1. Trong phần thân của lớp con SmartTvDevice, hãy thêm thuộc tính speakerVolume bạn đã tạo khi được học về các hàm getter và setter:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Định nghĩa một thuộc tính channelNumber được gán giá trị bằng 1 với một hàm setter chỉ định phạm vi 0..200:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. Định nghĩa một phương thức increaseSpeakerVolume() để tăng âm lượng và in chuỗi "Speaker volume increased to $speakerVolume.":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    } 
}
  1. Thêm phương thức nextChannel() để tăng số kênh và in chuỗi "Channel number increased to $channelNumber.":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
    
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. Ở dòng sau lớp con SmartTvDevice, định nghĩa một lớp con SmartLightDevice kế thừa từ lớp cha SmartDevice:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. Trong phần thân lớp con SmartLightDevice, định nghĩa một thuộc tính brightnessLevel được gán giá trị bằng 0 với một hàm setter chỉ định phạm vi 0..100:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Định nghĩa một phương thức increaseBrightness() để tăng độ sáng của đèn và in chuỗi "Brightness increased to $brightnessLevel.":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

Mối quan hệ giữa các lớp

Khi sử dụng tính kế thừa, bạn thiết lập mối quan hệ giữa hai lớp được gọi là mối quan hệ IS-A. Một đối tượng cũng là một thực thể của lớp mà nó kế thừa. Trong mối quan hệ HAS-A, một đối tượng có thể sở hữu một thực thể của một lớp khác mà không thực sự là một thực thể của lớp đó. Bạn có thể thấy phần biểu thị cấp cao về các mối quan hệ này trong biểu đồ dưới đây:

Phần biểu thị cấp cao về mối quan hệ HAS-A và IS-A.

Mối quan hệ IS-A

Khi bạn xác định mối quan hệ IS-A giữa lớp cha SmartDevice và lớp con SmartTvDevice, điều đó có nghĩa là lớp con SmartTvDevice có thể làm bất cứ điều gì mà lớp cha SmartDevice làm được. Mối quan hệ này là một chiều, vì vậy bạn có thể nói rằng mọi TV thông minh đều là thiết bị thông minh, nhưng bạn không thể nói rằng mọi thiết bị thông minh đều là TV thông minh. Mã biểu thị cho mối quan hệ IS-A được minh hoạ trong đoạn mã dưới đây:

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

Đừng sử dụng tính kế thừa chỉ để đạt được mục đích tái sử dụng mã. Trước khi bạn quyết định, hãy kiểm tra xem hai lớp có liên quan với nhau hay không. Nếu chúng có thể hiện một số mối quan hệ, hãy kiểm tra xem chúng có thực sự đủ điều kiện để thiết lập mối quan hệ IS-A hay không. Hãy tự hỏi: "Tôi có thể nói lớp con là một lớp cha không?". Ví dụ: Android là một hệ điều hành.

Mối quan hệ HAS-A

Mối quan hệ HAS-A là một cách khác để chỉ định mối quan hệ giữa hai lớp. Ví dụ: bạn có thể đang sử dụng TV thông minh tại nhà của mình. Trong trường hợp này, có mối quan hệ giữa TV thông minh và nhà bạn. Ngôi nhà chứa một thiết bị thông minh, hoặc nói cách khác là nhà có mộtthiết bị thông minh. Mối quan hệ HAS-A giữa hai lớp cũng được gọi là composition (sự hợp thành).

Cho đến nay, bạn đã tạo được một vài thiết bị thông minh. Bây giờ, bạn sẽ tạo lớp SmartHome chứa các thiết bị thông minh. Lớp SmartHome cho phép bạn tương tác với các thiết bị thông minh.

Sử dụng mối quan hệ HAS-A để định nghĩa lớp SmartHome:

  1. Ở giữa lớp SmartLightDevice và hàm main(), hãy định nghĩa một lớp SmartHome:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() { 
    ...
}
  1. Trong hàm khởi tạo lớp SmartHome, dùng từ khoá val để tạo thuộc tính smartTvDevice kiểu SmartTvDevice:
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. Trong phần thân của lớp SmartHome, hãy định nghĩa một phương thức turnOnTv() để gọi phương thức turnOn() trên thuộc tính smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. Ở dòng sau phương thức turnOnTv(), hãy định nghĩa phương thức turnOffTv() để gọi phương thức turnOff() trên thuộc tính smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. Ở dòng sau phương thức turnOffTv(), hãy định nghĩa phương thức increaseTvVolume() để gọi phương thức increaseSpeakerVolume() trên thuộc tính smartTvDevice, sau đó định nghĩa phương thức changeTvChannelToNext() để gọi phương thức nextChannel() trên thuộc tính smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. Trong hàm khởi tạo lớp SmartHome, di chuyển tham số thuộc tính smartTvDevice sang dòng riêng, theo sau là dấu phẩy:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. Ở dòng sau thuộc tính smartTvDevice, dùng từ khoá val để định nghĩa thuộc tính smartLightDevice kiểu SmartLightDevice:
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. Trong phần thân SmartHome, hãy định nghĩa phương thứcturnOnLight() để gọi phương thức turnOn() trên đối tượng smartLightDevice và một phương thức turnOffLight() để gọi phương thức turnOff() trên đối tượng smartLightDevice:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. Ở dòng sau phương thức turnOffLight(), hãy định nghĩa phương thức increaseLightBrightness() để gọi phương thức increaseBrightness() trên thuộc tính smartLightDevice:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}
  1. Ở dòng sau phương thức increaseLightBrightness(), hãy định nghĩa phương thức turnOffAllDevices() để gọi các phương thức turnOffTv()turnOffLight():.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

Ghi đè phương thức của lớp cha từ các lớp con

Như đã thảo luận trước đó, mặc dù chức năng bật và tắt được tất cả các thiết bị thông minh hỗ trợ, nhưng cách chúng thực hiện chức năng này lại khác nhau. Để cung cấp hành vi cụ thể cho từng thiết bị, bạn cần ghi đè các phương thức turnOn()turnOff() được định nghĩa trong lớp cha. Ghi đè có nghĩa là can thiệp vào hành động, thường là để kiểm soát theo cách thủ công. Khi bạn ghi đè một phương thức, phương thức trong lớp con sẽ ngắt quá trình thực thi của phương thức được định nghĩa trong lớp cha và tự cung cấp quá trình thực thi của riêng nó.

Ghi đè các phương thức turnOn()turnOff() của lớp SmartDevice:

  1. Trong phần thân của lớp cha SmartDevice trước từ khoá fun của mỗi phương thức, hãy thêm từ khoá open:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. Trong phần thân của lớp SmartLightDevice, định nghĩa một phương thức turnOn() có phần thân trống:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
    }
}
  1. Trong phần thân của phương thức turnOn(), thiết lập thuộc tính deviceStatus thành chuỗi "on", thiết lập thuộc tính brightnessLevel thành giá trị bằng 2, rồi thêm câu lệnh println() sau đó truyền vào câu lệnh đó chuỗi "$name turned on. The brightness level is $brightnessLevel.":
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. Trong phần thân của lớp SmartLightDevice, định nghĩa một phương thức turnOff() có phần thân trống:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
    }
}
  1. Trong phần thân của phương thức turnOff(), thiết lập thuộc tính deviceStatus thành chuỗi "off", thiết lập thuộc tính brightnessLevel thành giá trị bằng 0, rồi thêm câu lệnh println() sau đó truyền vào câu lệnh đó chuỗi "Smart Light turned off":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}
  1. Trong lớp con SmartLightDevice trước từ khoá fun của các phương thức turnOn()turnOff(), hãy thêm từ khoá override:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

Từ khoá override thông báo cho môi trường thời gian chạy của Kotlin thực thi đoạn mã được đặt trong phương thức đã định nghĩa ở lớp con.

  1. Trong phần thân của lớp SmartTvDevice, định nghĩa một phương thức turnOn() có phần thân trống:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
        
    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
        
    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
    
    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    fun turnOn() {
    }
}
  1. Trong phần thân của phương thức turnOn(), hãy thiết lập thuộc tính deviceStatus thành chuỗi "on" rồi thêm một câu lệnh println(), sau đó truyền vào đó chuỗi "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber.":
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }
}
  1. Trong phần thân của lớp SmartTvDevice sau phương thức turnOn(), định nghĩa một phương thức turnOff() có phần thân trống:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. Trong phần thân của phương thức turnOff(), hãy thiết lập thuộc tính deviceStatus thành chuỗi "off" rồi thêm câu lệnh println(), sau đó truyền vào đó chuỗi "$name turned off":
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. Trong lớp SmartTvDevice trước từ khoá fun của các phương thức turnOn()turnOff(), hãy thêm từ khoá override:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. Trong hàm main(), hãy dùng từ khoá var để định nghĩa một biến smartDevice kiểu SmartDevice khởi tạo một đối tượng SmartTvDevice nhận một đối số "Android TV" và một đối số "Entertainment":
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. Ở dòng sau biến smartDevice, gọi phương thức turnOn() trên đối tượng smartDevice:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. Chạy mã.

Kết quả sẽ như sau:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. Ở dòng sau khi gọi phương thức turnOn(), gán lại biến smartDevice để khởi tạo một lớp SmartLightDevice nhận đối số "Google Light" và đối số "Utility", sau đó gọi phương thức turnOn() trên tham chiếu đối tượng smartDevice:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. Chạy mã.

Kết quả sẽ như sau:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

Đây là một ví dụ về tính đa hình. Mã này gọi phương thức turnOn() trên một biến kiểu SmartDevice và tuỳ thuộc vào giá trị thực tế của biến đó, bạn có thể thực thi các quy trình triển khai khác nhau của phương thức turnOn().

Tái sử dụng mã lớp cha trong các lớp con bằng từ khoá super

Khi xem xét kỹ các phương thức turnOn()turnOff(), bạn nhận thấy có sự tương đồng trong cách biến deviceStatus được cập nhật mỗi khi các phương thức này được gọi trong các lớp con SmartTvDeviceSmartLightDevice: đoạn mã bị trùng lặp. Bạn có thể tái sử dụng đoạn mã khi cập nhật trạng thái trong lớp SmartDevice.

Để gọi phương thức bị ghi đè trong lớp cha từ lớp con, bạn cần sử dụng từ khoá super. Việc gọi một phương thức từ lớp cha cũng tương tự như gọi phương thức từ bên ngoài lớp. Thay vì dùng toán tử . giữa đối tượng và phương thức, bạn cần dùng từ khoá super để thông báo cho trình biên dịch Kotlin gọi phương thức trên lớp cha thay vì lớp con.

Cú pháp để gọi phương thức từ lớp cha bắt đầu bằng từ khoá super, theo sau là toán tử ., tên hàm và một cặp dấu ngoặc đơn. Nếu có, cặp dấu ngoặc đơn sẽ bao gồm các đối số. Bạn có thể xem cú pháp trong biểu đồ này:

18cc94fefe9851e0.png

Sử dụng lại mã trong lớp cha SmartDevice:

  1. Xoá các câu lệnh println() khỏi các phương thức turnOn()turnOff(), đồng thời di chuyển mã trùng lặp từ các lớp con SmartTvDeviceSmartLightDevice sang lớp cha SmartDevice:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. Dùng từ khoá super để gọi các phương thức từ lớp SmartDevice trong các lớp con SmartTvDeviceSmartLightDevice:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

Ghi đè thuộc tính của lớp cha từ các lớp con

Tương tự như phương thức, bạn cũng có thể ghi đè các thuộc tính theo các bước tương tự.

Ghi đè thuộc tính deviceType:

  1. Trong lớp cha SmartDevice, ở dòng sau thuộc tính deviceStatus, hãy dùng các từ khoá openval để định nghĩa một thuộc tính deviceType được thiết lập với chuỗi "unknown":
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. Trong lớp SmartTvDevice, hãy dùng các từ khoá overrideval để định nghĩa một thuộc tính deviceType được thiết lập với chuỗi "Smart TV":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. Trong lớp SmartLightDevice, hãy dùng các từ khoá overrideval để định nghĩa một thuộc tính deviceType được thiết lập với chuỗi "Smart Light":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. Các từ khoá quy định phạm vi truy cập

Các từ khoá quy định phạm vi truy cập đóng vai trò quan trọng trong việc đạt được tính đóng gói:

  • Trong một lớp, nó cho phép bạn ẩn các thuộc tính và phương thức khỏi sự truy cập trái phép từ bên ngoài lớp.
  • Trong một gói, nó cho phép bạn ẩn các lớp và giao diện khỏi sự truy cập trái phép từ bên ngoài gói.

Kotlin cung cấp bốn từ khoá quy định phạm vi truy cập:

  • public. Từ khoá mặc định. Cho phép khai báo có thể truy cập ở mọi nơi. Các thuộc tính và phương thức bạn muốn dùng bên ngoài lớp được đánh dấu là public.
  • private. Cho phép khai báo chỉ có thể truy cập được trong cùng một lớp hoặc cùng một tệp nguồn.

Có thể có một số thuộc tính và phương thức chỉ được dùng bên trong lớp, và bạn không nhất thiết muốn các lớp khác sử dụng. Những thuộc tính và phương thức này có thể được đánh dấu bằng từ khoá private để đảm bảo một lớp khác không thể vô tình truy cập vào.

  • protected. Cho phép khai báo có thể truy cập được trong các lớp con. Các thuộc tính và phương thức mà bạn muốn sử dụng trong lớp định nghĩa chúng và các lớp con của nó được đánh dấu bằng từ khoá protected.
  • internal. Cho phép khai báo có thể truy cập được trong cùng một mô-đun. Từ khoá internal tương tự như private, nhưng bạn có thể truy cập các thuộc tính và phương thức internal từ bên ngoài lớp miễn là nó được truy cập trong cùng một mô-đun.

Khi bạn định nghĩa một lớp, lớp này sẽ hiển thị công khai và có thể được truy cập bởi bất kỳ gói nào import (nhập) nó, tức là lớp đó ở chế độ public theo mặc định, trừ phi bạn chỉ định một từ khoá quy định phạm vi truy cập. Tương tự, khi bạn định nghĩa hoặc khai báo các thuộc tính và phương thức trong lớp, theo mặc định, những thuộc tính và phương thức này có thể được truy cập bên ngoài lớp thông qua đối tượng của lớp. Việc xác định phạm vi truy cập thích hợp cho mã là rất cần thiết, chủ yếu là để ẩn các thuộc tính và phương thức mà các lớp khác không cần truy cập.

Ví dụ: hãy xem xét cách một chiếc ô tô được thiết kế để người lái xe có thể sử dụng. Theo mặc định, những chi tiết về các bộ phận tạo nên một chiếc xe ô tô và cách xe hoạt động được ẩn giấu bên trong. Chiếc xe ô tô được thiết kế để vận hành trực quan nhất có thể. Bạn không muốn ô tô hoạt động phức tạp như một chiếc máy bay thương mại, tương tự như việc bạn không muốn một nhà phát triển khác hoặc chính bạn trong tương lai bị nhầm lẫn về cách sử dụng thuộc tính và phương thức của một lớp.

Các từ khoá quy định phạm vi truy cập giúp bạn hiển thị các phần có liên quan của mã cho các lớp khác trong dự án của mình và đảm bảo rằng phần triển khai bên trong không thể bị sử dụng một cách vô ý, điều này giúp mã dễ hiểu hơn và ít xảy ra lỗi hơn.

Từ khoá quy định phạm vi truy cập nên được đặt trước cú pháp khai báo, trong khi khai báo lớp, phương thức hoặc thuộc tính như bạn có thể thấy trong biểu đồ này:

dcc4f6693bf719a9.png

Chỉ định từ khoá quy định phạm vi truy cập cho các thuộc tính

Cú pháp để chỉ định một từ khoá quy định phạm vi truy cập cho một thuộc tính bắt đầu bằng từ khoá private, protected hoặc internal, theo sau là cú pháp định nghĩa một thuộc tính. Bạn có thể xem cú pháp trong biểu đồ này:

47807a890d237744.png

Ví dụ: bạn có thể xem cách thiết lập thuộc tính deviceStatus thành private trong đoạn mã này:

open class SmartDevice(val name: String, val category: String) {

    ...

    private var deviceStatus = "online"

    ...
}

Bạn cũng có thể thiết lập các từ khoá quy định phạm vi truy cập cho các hàm setter. Từ khoá được đặt trước từ khoá set. Bạn có thể xem cú pháp trong biểu đồ này:

cea29a49b7b26786.png

Đối với lớp SmartDevice, giá trị của thuộc tính deviceStatus phải đọc được bên ngoài lớp thông qua các đối tượng của lớp. Tuy nhiên, chỉ lớp đó và các lớp con của nó mới có thể cập nhật hoặc ghi giá trị. Để thực hiện yêu cầu này, bạn cần dùng từ khoá protected trên hàm set() của thuộc tính deviceStatus.

Dùng từ khoá protected trên hàm set() của thuộc tính deviceStatus:

  1. Trong thuộc tính deviceStatus của lớp cha SmartDevice, thêm từ khoá protected vào hàm set():
open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set(value) {
           field = value
       }

    ...
}

Bạn không thực hiện bất kỳ hành động hoặc bước kiểm tra nào trong hàm set(). Bạn chỉ đơn giản là gán tham số value cho biến field. Như bạn đã được học, điều này tương tự như cách triển khai mặc định cho phương thức setter của thuộc tính. Trong trường hợp này, bạn có thể bỏ qua cặp dấu ngoặc đơn và phần thân của hàm set():

open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. Trong lớp SmartHome, định nghĩa một thuộc tính deviceTurnOnCount được gán giá trị bằng 0 với một hàm setter là private:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. Thêm thuộc tính deviceTurnOnCount theo sau là toán tử số học ++ vào các phương thức turnOnTv()turnOnLight(). Sau đó, thêm thuộc tính deviceTurnOnCount theo sau là toán tử số học -- vào các phương thức turnOffTv()turnOffLight():
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }
    
    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

Các từ khoá quy định phạm vi truy cập cho các phương thức

Cú pháp để chỉ định một từ khoá quy định phạm vi truy cập cho một phương thức bắt đầu bằng các từ khoá private, protected hoặc internal, theo sau là cú pháp định nghĩa một phương thức. Bạn có thể xem cú pháp trong biểu đồ này:

e0a60ddc26b841de.png

Ví dụ: bạn có thể xem cách chỉ định từ khoá protected cho phương thức nextChannel() trong lớp SmartTvDevice ở đoạn mã này:

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    protected fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }      

    ...
}

Các từ khoá quy định phạm vi truy cập cho các hàm khởi tạo

Cú pháp để chỉ định từ khoá quy định phạm vi truy cập cho một hàm khởi tạo tương tự như việc định nghĩa hàm khởi tạo chính nhưng có một vài khác biệt:

  • Từ khoá được chỉ định sau tên lớp, nhưng nằm trước từ khoá constructor.
  • Nếu cần chỉ định từ khoá cho hàm khởi tạo chính, bạn bắt buộc phải giữ lại từ khoá constructor và cặp dấu ngoặc đơn ngay cả khi không có bất kỳ tham số nào.

Bạn có thể xem cú pháp trong biểu đồ này:

6832575eba67f059.png

Ví dụ: bạn có thể xem cách thêm từ khoá protected vào hàm khởi tạo SmartDevice trong đoạn mã dưới đây:

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

Các từ khoá quy định phạm vi truy cập cho các lớp

Cú pháp để chỉ định từ khoá quy định phạm vi truy cập cho một lớp bắt đầu bằng các từ khoá private, protected hoặc internal, theo sau là cú pháp định nghĩa một lớp. Bạn có thể xem cú pháp trong biểu đồ này:

3ab4aa1c94a24a69.png

Ví dụ: bạn có thể xem cách chỉ định từ khoá internal cho lớp SmartDevice trong đoạn mã dưới đây:

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

Bạn nên cố gắng áp dụng phạm vi hiển thị khắt khe cho các thuộc tính và phương thức, vì vậy hãy khai báo chúng với từ khoá private thường xuyên nhất có thể. Nếu bạn không thể giữ chúng ở chế độ private, hãy dùng từ khoá protected. Nếu bạn không thể giữ chúng ở chế độ protected, hãy dùng từ khoá internal. Nếu bạn không thể giữ chúng ở chế độ internal, hãy dùng từ khoá public.

Chỉ định các từ khoá quy định phạm vi truy cập phù hợp

Bảng dưới đây giúp bạn xác định các từ khoá quy định phạm vi truy cập phù hợp dựa trên nơi mà thuộc tính, phương thức của một lớp hoặc hàm khởi tạo cần được truy cập:

Từ khoá quy định phạm vi truy cập

Có thể truy cập trong cùng một lớp

Có thể truy cập trong lớp con

Có thể truy cập trong cùng một mô-đun

Có thể truy cập bên ngoài mô-đun

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

Trong lớp con SmartTvDevice, bạn không nên cho phép điều khiển các thuộc tính speakerVolumechannelNumber từ bên ngoài lớp. Bạn chỉ nên điều khiển các thuộc tính này thông qua các phương thức increaseSpeakerVolume()nextChannel().

Tương tự, trong lớp con SmartLightDevice, thuộc tính brightnessLevel chỉ nên được điều khiển thông qua phương thức increaseLightBrightness().

Thêm các từ khoá quy định phạm vi truy cập thích hợp vào các lớp con SmartTvDeviceSmartLightDevice:

  1. Trong lớp SmartTvDevice, hãy thêm từ khoá quy định phạm vi truy cập private vào các thuộc tính speakerVolumechannelNumber:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. Trong lớp SmartLightDevice, hãy thêm từ khoá private vào thuộc tính brightnessLevel:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. Định nghĩa uỷ quyền thuộc tính

Bạn đã tìm hiểu trong phần trước rằng các thuộc tính trong Kotlin sử dụng một trường dự phòng để lưu giữ giá trị của các thuộc tính đó trong bộ nhớ. Bạn sử dụng định danh field để tham chiếu đến nó.

Khi xem xét đoạn mã cho đến hiện tại, bạn có thể thấy phần mã bị trùng lặp dùng để kiểm tra xem các giá trị có nằm trong phạm vi cho phép đối với các thuộc tính speakerVolume, channelNumberbrightnessLevel trong các lớp SmartTvDeviceSmartLightDevice hay không. Bạn có thể tái sử dụng đoạn mã kiểm tra phạm vi trong hàm setter bằng cách sử dụng uỷ quyền (delegate). Thay vì sử dụng một field cùng với hàm getter và setter để quản lý giá trị, delegate sẽ quản lý giá trị đó.

Cú pháp để tạo uỷ quyền thuộc tính bắt đầu bằng việc khai báo một biến, theo sau là từ khoá by và đối tượng uỷ quyền sẽ xử lý các hàm getter và setter cho thuộc tính. Bạn có thể xem cú pháp trong biểu đồ này:

928547ad52768115.png

Trước khi triển khai lớp mà bạn có thể uỷ quyền thực thi, bạn cần làm quen với các interface (giao diện). Interface là một "hợp đồng" mà các lớp triển khai nó cần phải tuân thủ. Nó tập trung vào việc làm cái gì thay vì làm như thế nào. Tóm lại, interface sẽ giúp bạn đạt được tính trừu tượng.

Ví dụ: trước khi xây nhà, bạn phải thông báo cho kiến trúc sư về những gì bạn muốn. Bạn muốn có phòng ngủ, phòng cho trẻ em, phòng khách, bếp và một vài phòng tắm. Một cách ngắn gọn, bạn xác định những điều bạn muốn và kiến trúc sư chỉ định cách để đạt được điều đó. Bạn có thể xem cú pháp để tạo một interface trong biểu đồ này:

bfe3fd1cd8c45b2a.png

Bạn đã biết cách kế thừa một lớp và ghi đè chức năng của lớp đó. Với interface, lớp sẽ triển khai interface đó. Lớp cung cấp thông tin triển khai cho các phương thức và thuộc tính được khai báo trong interface. Bạn sẽ thực hiện một thao tác tương tự với interface ReadWriteProperty để tạo ra delegate. Bạn sẽ tìm hiểu thêm về interface trong bài sau.

Để tạo lớp uỷ quyền cho kiểu var, bạn cần triển khai interface ReadWriteProperty. Tương tự, bạn cần triển khai interface ReadOnlyProperty cho kiểu val.

Tạo uỷ quyền cho kiểu var:

  1. Trước hàm main(), hãy tạo một lớp RangeRegulator triển khai interface ReadWriteProperty<Any?, Int>:
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

Đừng lo lắng về các dấu ngoặc nhọn hoặc nội dung bên trong chúng. Chúng biểu thị các kiểu tổng quát và bạn sẽ học về chúng trong bài học tiếp theo.

  1. Trong hàm khởi tạo chính của lớp RangeRegulator, hãy thêm một tham số initialValue, một thuộc tính private tên là minValue và một thuộc tính private tên là maxValue, tất cả đều thuộc kiểu Int:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. Trong phần thân của lớp RangeRegulator, hãy ghi đè các phương thức getValue()setValue():
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

Các phương thức này đóng vai trò như các hàm getter và setter của thuộc tính.

  1. Ở dòng trước lớp SmartDevice, hãy nhập các interface ReadWritePropertyKProperty:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. Trong lớp RangeRegulator, ở dòng trước phương thức getValue(), hãy định nghĩa một thuộc tính fieldData và khởi tạo thuộc tính đó bằng tham số initialValue:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

Thuộc tính này đóng vai trò như một trường dự phòng cho biến.

  1. Trong phần thân của phương thức getValue(), hãy trả về thuộc tính fieldData:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}
  1. Trong phần thân của phương thức setValue(), hãy kiểm tra xem tham số value đang được gán có nằm trong phạm vi minValue..maxValue hay không trước khi bạn gán nó cho thuộc tính fieldData:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. Trong lớp SmartTvDevice, hãy dùng lớp uỷ quyền để định nghĩa các thuộc tính speakerVolumechannelNumber:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...

}
  1. Trong lớp SmartLightDevice, hãy dùng lớp uỷ quyền để định nghĩa thuộc tính brightnessLevel:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    ...

}

10. Kiểm tra giải pháp

Bạn có thể xem mã giải pháp trong đoạn mã này:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

Kết quả sẽ như sau:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

11. Tham gia thử thách này

  • Trong lớp SmartDevice, hãy định nghĩa một phương thức printDeviceInfo() để in chuỗi "Device name: $name, category: $category, type: $deviceType".
  • Trong lớp SmartTvDevice, hãy định nghĩa một phương thức decreaseVolume() để giảm âm lượng và một phương thức previousChannel() để chuyển về kênh trước đó.
  • Trong lớp SmartLightDevice, hãy định nghĩa một phương thức decreaseBrightness() để giảm độ sáng.
  • Trong lớp SmartHome, hãy đảm bảo bạn chỉ có thể thực hiện tất cả các hành động khi thuộc tính deviceStatus của mỗi thiết bị được thiết lập là chuỗi "on". Ngoài ra, hãy đảm bảo bạn cập nhật chính xác thuộc tính deviceTurnOnCount.

Sau khi bạn hoàn tất việc triển khai:

  • Trong lớp SmartHome, hãy định nghĩa các phương thức decreaseTvVolume(), changeTvChannelToPrevious(), printSmartTvInfo(), printSmartLightInfo()decreaseLightBrightness().
  • Gọi các phương thức tương ứng từ các lớp SmartTvDeviceSmartLightDevice bên trong lớp SmartHome.
  • Trong hàm main(), hãy gọi các phương thức vừa thêm này để kiểm tra chúng.

12. Kết luận

Xin chúc mừng! Bạn đã học được cách định nghĩa các lớp và khởi tạo các đối tượng. Bạn cũng đã học cách tạo mối quan hệ giữa các lớp và tạo các uỷ quyền thuộc tính.

Tóm tắt

  • Có bốn nguyên tắc chính trong lập trình hướng đối tượng (OOP): tính đóng gói, tính trừu tượng, tính kế thừa và tính đa hình.
  • Các lớp được định nghĩa bằng từ khoá class, đồng thời chứa các thuộc tính và phương thức.
  • Các thuộc tính tương tự như các biến, ngoại trừ việc thuộc tính có thể có các hàm getter và setter tuỳ chỉnh.
  • Hàm khởi tạo chỉ định cách khởi tạo các đối tượng của một lớp.
  • Bạn có thể bỏ qua từ khoá constructor khi định nghĩa một hàm khởi tạo chính.
  • Tính kế thừa giúp bạn dễ dàng tái sử dụng mã.
  • Mối quan hệ IS-A đề cập đến tính kế thừa.
  • Mối quan hệ HAS-A đề cập đến composition (sự hợp thành).
  • Các từ khoá quy định phạm vi truy cập đóng vai trò quan trọng trong việc đạt được tính đóng gói.
  • Kotlin cung cấp bốn từ khoá quy định phạm vi truy cập: public, private, protectedinternal.
  • Uỷ quyền thuộc tính cho phép bạn tái sử dụng mã của hàm getter và setter trong nhiều lớp.

Tìm hiểu thêm