Idiomatic Kotlin (Phần 1)

Idiomatic Kotlin là bài viết tổng hợp những mẹo, những kiến thức cơ bản cần ghi nhớ và nên vận dụng khi sử dụng Kotlin. Bài viết này sẽ nêu các use case phổ biến được xử lí bởi Java và chỉ ra những cải tiến mà Kotlin đem lại khi xử lí các vấn đề này. Rất nhiều case mà Kotlin đã hỗ trợ chúng ta, hơn nữa còn cung cấp phương pháp tối ưu hơn cho coding. Trong java, chúng ta thường phải viết khá nhiều boilerplate-code để implement các case, pattern phổ biến. Rất may là có nhiều pattern đã được build sắn trong thư viện standard của Kotlin.

Java Idiom or Pattern Idiomatic Solution in Kotlin
Optional Nullable Types
Getter, Setter, Backing Field Properties
Static Utility Class Top-Level (extension) functions
Immutability, Value Objects data class with immutable properties, copy()
Fluent Setter (Wither) Named and default arguments, apply()
Method Chaining Default arguments
Singleton object
Delegation Delegated properties by
Lazy Initialization (thread-safe) Delegated properties by: lazy()
Observer Delegated properties by: Delegates.observable()

Functional Programing (Lập trình hướng chức năng)

Lập trình hướng chức năng cho chúng ta giảm thiểu được khả năng lỗi liên quan (side-effect) và giúp code

  • ít xác suất xảy ra lỗi
  • dễ hiểu
  • dể test
  • thread-safe

Trái với java8, Kotlin hỗ trợ functional programing rõ ràng hơn

  • Immutability (tính bất biến) : từ khóa val cho variable và properties, các data class immutable, copy()…
  • Expresstions (biểu thức) : chuyển đổi if, when, try-catch thành các expressions, ta có thể kết hợp chúng với các biểu thức khác một cách ngắn gọn.
  • Function Types (kiểu chức năng)
  • Biểu thức lambda ngắn gọn, súc tích
  • Kotlin’s Collection API

Một vài tính năng kể trên giúp cho việc viết các functional code an toàn, ngắn gọn và dễ hiểu. Nhờ đó ta có thể viết những pure function (có ít khả năng gây ra side-effect) dễ hơn.

Use Expresstions (Sử dụng biểu thức)

// Don't
fun getDefaultLocale(deliveryArea: String): Locale {
    val deliverAreaLower = deliveryArea.toLowerCase()
    if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
        return Locale.GERMAN
    }
    if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
        return Locale.ENGLISH
    }
    if (deliverAreaLower == "france") {
        return Locale.FRENCH
    }
    return Locale.ENGLISH
}
// Do
fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
    "germany", "austria" -> Locale.GERMAN
    "usa", "great britain" -> Locale.ENGLISH
    "france" -> Locale.FRENCH
    else -> Locale.ENGLISH
}

Nguyên tắc nên nhớ : Mỗi lần viết if, cân nhắc việc viết thành expressions cho ngắn gọn hơn. try-catch cũng tương tự.

val json = """{"message":"HELLO"}"""
val message = try {
    JSONObject(json).getString("message")
} catch (ex: JSONException) {
    json
}

Top-Level (Extension) Function trong Utility

Trong java, ta thường tạo các phương thức util static trong util class. Đại khái viết trong Kotlin như sau:

//Don't
object StringUtil {
    fun countAmountOfX(string: String): Int{
        return string.length - string.replace("x", "").length
    }
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")

Kotlin cho phép loại bỏ các wrapping class không cần thiết và sử dụng top-level function. Hiểu nôm na là nó sẽ giúp code ngắn gọn và dễ đọc hơn, để rõ hơn thì các bạn xem ví dụ sau :

//Do
fun String.countAmountOfX(): Int {
    return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()

Đặt tên tham số thay vì sử dụng fluent-setter

Với java, fluent-setter (còn được gọi là “Wither”) được sử dụng để đặt giá trị tham số và làm list tham số dễ đọc và ít lỗi.

//Don't
val config = SearchConfig()
       .setRoot("~/folder")
       .setTerm("kotlin")
       .setRecursive(true)
       .setFollowSymlinks(true)

Trong Kotlin, cùng chung mục đích nhưng các tham số có thể truyền thẳng như sau

//Do
val config2 = SearchConfig2(
       root = "~/folder",
       term = "kotlin",
       recursive = true,
       followSymlinks = true
)

Sử dụng apply() cho việc nhóm khởi tạo object

//Don't
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4

apply() giúp nhóm và tâp trung việc khởi tạo object, ngoài ra ta không cần phải gọi lại tên variable liên tục như trên.

//Do
val dataSource = BasicDataSource().apply {
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://domain:3309/db"
    username = "username"
    password = "password"
    maxTotal = 40
    maxIdle = 40
    minIdle = 4
}

Không overload tham số mặc định

Không nên sử dụng overload đối với method và constructor để nhận tham số mặc định. Kotlin có cách giải quyết tốt hơn, tránh phải viết nhiều boilerplate-code

//Don't
fun find(name: String){
    find(name, true)
}
fun find(name: String, recursive: Boolean){
}
//Do
fun find(name: String, recursive: Boolean = true){
}

Giải quyết triệt để Nullability

Tránh sử dụng if-null checks

Cách xử lí nullability của java cần nhiều đoạn code lặp không cần thiết và rất dễ miss

//Don't
if (order == null || order.customer == null || order.customer.address == null){
    throw IllegalArgumentException("Invalid Order")
}
val city = order.customer.address.city

Kotlin giải quyết bằng cách sử dụng null-safe call ?. hoặc sử dụng toán tử elvis ?:

//Do
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")

Tránh sử dụng if-type checks

//Don't
if (service !is CustomerService) {
    throw IllegalArgumentException("No CustomerService")
}
service.getCustomer()

Sử dụng as? và ?: ta có check type, cast hoặc throw exception nếu không thỏa mãn điều kiện. Đặc biết tất cả đều nằm trong 1 biểu thức !

//Do
service as? CustomerService ?: throw IllegalArgumentException("No CustomerService")
service.getCustomer()

Cân nhắc sử dụng let()

Trong vài trường hợp, sử dụng let() sẽ rõ ràng hơn if.

val order: Order? = findOrder()
if (order != null){
    dun(order.customer)
}

Với let(), ta không cần thêm biến mà thực hiện chỉ với 1 biểu thức.

findOrder()?.let { dun(it.customer) }
//or
findOrder()?.customer?.let(::dun)