Data Class trong Kotlin

Chúng ta thường xuyên tạo các class có mục đích chính là lưu trữ dữ liệu. Trong những class như vậy, những phương thức được cài đặt thường lấy một cách máy móc từ dữ liệu bên trong. Ở trong Kotlin, chúng được gọi là những data class.

data class User(val name: String, val age: Int)

Khi sử dụng từ khóa data đi kèm, trình biên dịch sẽ tự động sinh ra những phương thức cần thiết như là

  • equals()/hashCode()
  • toString()
  • componentN()
  • copy()

Để đảm bảo những đoạn code sinh ra có tính nhất quán và thực hiện đúng ý nghĩa có nó thì những data class phải đáp ứng các yêu cầu sau:

  • Hàm constructor chính phải có ít nhất một tham số truyền vào
  • Tất cả các tham số truyền vào trong hàm constructor chính phải được khai báo là val hoặc var
  • Những data class không thể là abstract, open, sealed hay là inner
  • Nếu sử dụng Kotlin phiên bản trước 1.1, Data class chỉ có thể implement các interface.
  • Từ Kotlin 1.1 trở đi, Data class có thể kế thừa những class khác

So sánh với JVM, nếu data class cần có một hàm constructor rỗng(không có tham số nào) thì hãy thêm những giá trị mặc định cho tất cả thuộc tính

data class User(val name: String = "", val age: Int = 0)

Properties Declared in the Class Body

Lưu ý rằng trình biên dịch chỉ sử dụng các thuộc tính được xác định bên trong hàm constructor chính cho các hàm được tạo tự động. Với những thuộc tính không cần có nhu cầu sinh code tự động, hãy viết ở trong thân class:

data class Person(val name: String) {
    var age: Int = 0
}

Theo như cách cài đặt trên, chỉ có name sẽ được sử dụng bên trong những phương thức toString(), equals(), hashCode(), copy(), ... Trong khi hai đối tượng Person có thể có age khác nhau, nhưng chúng vẫn sẽ được coi là bằng nhau.

val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20

Copying

Thông thường chúng ta cần sao chép một đối tượng thay đổi một số thuộc tính của nó nhưng giữ cho phần còn lại không thay đổi. Đây là function copy() được tạo. Đối với lớp Person ở trên, việc triển khai của nó sẽ như sau:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

Điều đó sẽ cho phép chúng ta viết:

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

Data Classes and Destructuring Declarations

Các Component functions đã sinh ra cho data class chp phép tiêu giảm trong việc khai báo:

val jane = User("Jane", 35) 
val (name, age) = jane
println("$name, $age years of age") // prints "Jane, 35 years of age"

Standard Data Classes

Thư viện tiêu chuẩn cung cấp PairTriple. Tuy nhiên, chúng ta vẫn khuyến khích việc đặt tên cho những data class vì như vậy sẽ làm cho những đoạn code của chúng ta dễ đọc và dễ bảo trì hơn.

Data Class and Builder Design Pattern

Ở bài trước, mình đã giới thiệu với các bạn về chủ đề Builder Pattern.

Nhìn chung Builder Pattern giải quyết cho chúng ta 2 bài toán:

  • Quá nhiều tham số truyền vào trong một hàm constructor, đôi khi không cần thiết
  • Thứ tự các tham số hàm constructor quá nhiều, khó để nhớ được thứ tự tham số

Với 2 bài toán trên, Builder Pattern đã giải quyết cho chúng ta một cách triệt để, tuy nhiên ở trong Kotlin thì sao?

Trong Kotlin, Data Class đã hỗ trợ chúng ta trong việc xây dựng những class có nhiều trường, đôi khi không cần dùng đếnBuilder Pattern nữa.

Thực sự là như vậy, các bạn hãy thử xem Data Class đã giải quyết 2 vấn đề của chúng ta như thế nào nhé!

Bài toán đầu tiên: Quá nhiều tham số truyền vào trong một hàm constructor, đôi khi không cần thiết

Để giải quyết vấn đề này, Data class đã hỗ trợ chúng ta trong việc xây dựng những giá trị mặc định

Với việc khai báo một class như này:

data class Student(
    val id: String, 
    val name: String, 
    val gender: String = "Male", 
    val country: String = "Vietnam",
    val address: String? = null
)

Nếu như không cần quan tâm tới các trường như gender, country hay address, ta có thể không cần phải truyền những tham số liên quan tới chúng.

val student = Student("1234", "Tran Quang Huy")
println(student) // Student(id=1234, name=Tran Quang Huy, gender=Male, country=Vietnam)

Như vậy bài toán thứ nhất đã giải quyết xong, vậy bài toán thứ 2 thì sao nhỉ?

Bài toán thứ hai: Thứ tự các tham số hàm constructor quá nhiều, khó để nhớ được thứ tự tham số.

Như mình đã giới thiệu ở phần trên, chúng ta hoàn toán có thể định nghĩa rõ ràng những tham số truyền vào thuộc về trường nào mà không cần quan tâm tới thứ tự của chúng trong data class.

Ví dụ, cùng một việc khơi tạo đối tượng Student như trên, mình có thể viết như này:

val student = Student(name = "Tran Quang Huy", id = "1234", country = "Japan")
println(student)// Student(id=1234, name=Tran Quang Huy, gender=Male, country=Japan)

Như vậy, với data class, không những không cần quan tâm thứ tự các tham số truyền vào, mà khi đọc code, chúng ta lại có thể hiểu được những tham số ấy thuộc về trường nào.

Đến đây, bạn thử tưởng tượng việc thiết kế Builder Pattern đồ sộ so với những dòng code ngắn gọn của data class, bạn sẽ thích cái nào hơn?

Tuy nhiên việc gì cũng có ưu và nhược điểm của nó, trong bài viết tiếp theo, mình sẽ chia sẻ rõ hơn về những ưu nhược điểm đó nhé.

Reference

Bài viết được dịch trừ docs của Kotlin: https://kotlinlang.org/docs/reference/data-classes.html

Cảm ơn bạn Lê Hồng Phúc đã giúp dỡ để mình có thể hoàn thành bài viết.