Các pattern của bộ tứ (Gang of Four) trong Kotlin

Kotlin ngày càng trở nên phổ biến và thích hợp để thay thế Java. Vậy, các mẫu Design Pattern phổ biến được implement trong Kotlin như thế nào? Bài viết này, chúng ta sẽ cùng nhau implement một số mẫu Design Pattern nổi tiếng nhất trong Kotlin.

Mục đích không đơn giản chỉ là implement các pattern. Vì Kotlin hỗ trợ lập trình hướng đối tượng (object oriented programming) và tương thích với Java, chúng ta có thể chỉ cần copy và tự động convert các Java class và nó sẽ vẫn làm việc tốt.

Một điều quan trọng cần lưu ý là các pattern này được sử dụng để khắc phục những thiếu sót về cách các ngôn ngữ lập trình được thiết kế trong những năm 90 (như C++). Nhiều ngôn ngữ hiện đại cung cấp các tính năng để khắc phục những vấn đề này mà không cần viết thêm code hay các pattern.

Đó là lý do tại sao, chúng ta sẽ cố gắng tìm một cách đơn giản hơn, dễ dàng hơn và mang nhiều ý nghĩa hơn để giải quyết cùng một vấn đề mà mỗi pattern đang giải quyết.

Như chúng ta đã biết, có 3 loại pattern (gof): structural, creationalbehavioral.

Structural Patterns

Decorator

Gắn thêm trách nhiệm cho đối tượng một cách năng động

Giả sử chúng ta muốn trang trí cho class Text một số hiệu ứng văn bản.

class Text(val text: String) {
    fun draw() = print(text)
}

Nếu bạn biết về pattern, bạn cũng biết rằng tập hợp các class được tạo ra để "trang trí" (decorate) - đó là mở rộng hành vi - cho class Text.

Với Kotlin, chúng ta có thể tránh phải sử dụng những class mở rộng này, bằng cách sử dụng extension function, như sau:

fun Text.underline(decorated: Text.() -> Unit) {
    print("_")
    this.decorated()
    print("_")
}

fun Text.background(decorated: Text.() -> Unit) {
    print("\u001B[43m")
    this.decorated()
    print("\u001B[0m")
}

Với những extension function này, chúng ta có thể tạo một Text mới và trang trí thêm method draw mà không cần phải tạo một class mới.

Text("Hello").run {
    background {
        underline {
            draw()
        }
    }
}

Nếu bạn chạy nó từ command line, bạn sẽ thấy text _Hello_ với một màu nền. (Nếu terminal của bạn hỗ trợ màu ansi)

So với mẫu Decorator gốc, có một nhược điểm: Chúng ta không thể truyền các đối tượng trước khi trang trí vì không có các class trang trí tách biệt.

Để giải quyết vấn đề này, chúng ta có thể sử dụng function một lần nữa.

fun preDecorated(decorated: Text.() -> Unit): Text.() -> Unit {
    return { background { underline { decorated() } } }
}

Creational Patterns

Builder

Tách riêng việc xây dựng(contruction) một đối tượng phức tạp từ đại diện(representation) của nó sao cho từ cùng một quá trình contruction có thể tạo ra các representation khác nhau.

Builder pattern là rất hữu ích. Chúng ta có thể trách được các contructor nhiều biến và dễ dàng tái sử dụng các thiết lập được thiết lập từ trước. Kotlin hỗ trợ trực tiếp pattern này với một extension function là apply.

Chẳng hạn, class Car:

class Car() {
    var color: String = "red"
    var doors = 3
}

Thay vì tạo một CarBuilder riêng cho class này, chúng ta có thể sử dụng apply (also cũng làm việc tốt) để khởi tạo Car:

Car().apply {
    color = "yellow"
    doors = 5
}

Vì các function có thể được lưu trữ trong một biến, khởi tạo này có thể được lưu trữ trong một biến. Bằng cách này, chúng ta có thể cấu hình sẵn các Builder-function.

val yellowCar: Car.() -> Unit = { color = "yellow" }

Prototype

Sử dụng các loại đối tượng để tạo ra bằng cách sử dụng một thể hiện nguyên mẫu, và tạo ra cách đối tượng mới bằng cách sao chép nguyên mẫu này.

Trong Java, prototyping về lý thuyết có thể được implement bằng cách sử dụng interface CloneableObject.clone(). Tuy nhiên, clone đã bị bỏ đi, vì vậy chúng ta nên tránh sử dụng nó.

Kotlin sửa vấn đề này với các data class.

Khi chúng ta dùng data class, sẽ có sẵn equals, hashCode, toStringcopy. Bằng cách sử dụng copy, chúng ta có thể clone toàn bộ đối tượng và tuỳ ý thay đổi một số thuộc tính của đối tượng mới.

data class EMail(var recipient: String, var subject: String?, var message: String?)
...

val mail = EMail("[email protected]", "Hello", "Don't know what to write.")

val copy = mail.copy(recipient = "[email protected]")

println("Email1 goes to " + mail.recipient + " with subject " + mail.subject)
println("Email2 goes to " + copy.recipient + " with subject " + copy.subject)

Singleton

Đảm bảo một class chỉ có một thể hiện, và cung cấp một global point để truy cập nó.

Tạo một Singleton trong java cần rất nhiều code, với kotlin việc này được thực hiện dễ dàng với việc khai báo object:

object Dictionary {
    private val definitions = mutableMapOf<String, String>

    fun addDefinition(word: String, definition: String) {
        definitions.put(word.toLowerCase(), definition)
    }

    fun getDefinition(word: String): String {
        return definitions[word.toLowerCase()] ?: ""
    }
}

Sử dụng từ khoá object ở đây sẽ tự động tạo class Dictionary và một thể hiện duy nhất cho nó. Thể hiện được tạo theo cách lazily, vì vậy, nó sẽ không được tạo cho đến khi thực sự được sử dụng.

Đối tượng được truy cập như các static function trong java:

val word = "kotlin"
Dictionary.addDefinition(word, "an awesome programming language created by JetBrains")
println(word + " is " + Dictionary.getDefinition(word))

Behavioral Pattern

Template Method

Định nghĩa một khung sườn của một thuật toán(algorithm) trong một hoạt động(operation), trì hoãn một số bước được xác định trong các subclass.

Pattern này sử dụng phân cấp class. Chúng ta định nghĩa một abstract method và gọi nó ở đâu đó trong base class. Implementation của method đó sẽ được xác định bởi các subclass.

//java
public abstract class Task {
        protected abstract void work();
        public void execute(){
            beforeWork();
            work();
            afterWork();
        }
    }

Bây giờ chúng ta có thể bắt đầu một Task cụ thể, mà điều thực sự được làm trong work. Cách tiếp cận Template Method sử dụng một top-level function:

//kotlin
fun execute(task: () -> Unit) {
    val startTime = System.currentTimeMillis() //"beforeWork()"
    task()
    println("Work took ${System.currentTimeMillis() - startTime} millis") //"afterWork()"
}

...
//usage:
execute {
    println("I'm working here!")
}

Như bạn có thể thấy, không cần phải có class ở tất cả. Có người sẽ tranh luận rằng điều này giống với pattern Strategy, có lẽ là chính xác. Nhưng một lần nữa StrategyTemplate Method đang giải quyết những vấn đề rất giống nhau.

Strategy

Định nghĩa một tập các thuật toán(algorithms), đóng gói chúng và làm cho chúng có thể hoán đổi nhau.

Giả sử chúng ta có một Customer trả một khoản phí nhất định mỗi tháng. Phí này có thể được chiết khấu. Thay vì có mỗi subclass Customer cho mỗi chiến lược chiết khấu, chúng ta sử dụng pattern Strategy.

class Customer(val name: String, val fee: Double, val discount: (Double) -> Double) {
    fun pricePerMonth(): Double {
        return discount(fee)
    }
}

Lưu ý rằng chúng ta đang sử dụng function (Double) -> Double thay vì một interface cho strategy. Để làm cụ thể hơn, chúng ta có thể đặc tả kiểu alias mà không làm mất tính linh hoạt: typealias Discount = (Double) -> Double.

Dù bằng cách nào, chúng ta có thể định nghĩa nhiều chiến lược để tính toán chiết khấu:

val studentDiscount = { fee: Double -> fee/2 }
val noDiscount = { fee: Double -> fee }
...

val student = Customer("Ned", 10.0, studentDiscount)
val regular = Customer("John", 10.0, noDiscount)

println("${student.name} pays %.2f per month".format(student.pricePerMonth()))
println("${regular.name} pays %.2f per month".format(regular.pricePerMonth()))

Iterator

Cung cấp cách để truy cập các phần tử của một đối tượng tổng hợp theo tuần tự mà không cần biết các đại diện bên dưới của nó.

Viết Iterator là một nhiệm vụ rất hiếm hoi. Hầu hết, nó dễ dàng và thuận tiện hơn để wrap một List và implement interface Iterable.

Trong kotlin, iterator() là một operator function. Có nghĩa là khi một class định nghĩa một function operator fun iterator(), nó có thể được lặp lại sử dụng vòng lặp for.

class Sentence(val words: List<String>)
...
operator fun Sentence.iterator(): Iterator<String> = words.iterator()

Kết luận

Chúng ta đã cùng nhau tìm hiểu một số pattern phổ biến nhưng chưa phải là tất cả pattern trong Gang of Four. Như đã nói trong phần mở đầu, đặc biệt với structural patterns, rất khó hoặc không thể viết theo một cách khác với Java.

Hy vọng bài viết này cung cấp cho bạn cách Kotlin tiếp cận các vấn đề khác nhau.