Linh Tạ

Hoàn thiện model với closure

Thu thập data qua nhiều màn riêng biệt để gộp lại thành model hoàn chỉnh là một bài toán khá phổ biến.


struct User {
    let email: String
    let password: String
    let name: String
    let address: String
}

//ViewControllerA => input email
//...

//ViewControllerB => input password
//...

//ViewControllerC => input name
//...

//ViewControllerD => input address => create User
//...

Ví dụ ở luồng login, người dùng sẽ nhập thông tin như email, password, name, address ở 4 controller khác nhau trước khi dùng những thông tin đó để tạo thành User cụ thể.

Giải pháp thường gặp

1. Optional properties

Chính vì toàn bộ data cần để khởi tạo User chỉ đầy đủ tại controller cuối cùng, một phương án thường gặp là định nghĩa các optional property trong model User.


struct User {
    var email: String
    var password: String?
    var name: String?
    var address: String?
}

Hướng tiếp cận này giải quyết vấn đề hiện tại nhưng tồn tại 2 nhược điểm lớn:

2. Default values

Một cách tiếp cận khác là cung cấp giá trị mặc định cho các property của User.


struct User {
    var email: String = ""
    var password: String = ""
    var name: String = ""
    var address: String = ""
}

Cách này tốt hơn optional property nhưng không chặt chẽ về mặt logic bởi rõ ràng các property password, email, name không thể rỗng. Ta hoàn toàn có thể quên gán giá trị cho password hay email và vô tình tạo ra một User vô nghĩa. Hơn nữa, không phải lúc nào ta cũng có thể cung cấp giá trị mặc định nếu type của property là một struct hay class phức tạp.

3. Các biến/model trung gian

Phương pháp thứ ba và cũng thủ công nhất là tạo ra các biến hoặc model ở các controller trung gian để truyền data đến controller cuối cùng.


class ViewControllerA {
    var email: String?
    
    func goToVCB() { //... }
}

class ViewControllerB {
    var email: String
    var password: String?
    
    func goToC() { //... }
}

class ViewControllerC {
    var email: String
    var password: String
    var name: String?
    
    func goToD() { //... }
}

class ViewControllerD {
    // ... bạn hiểu vấn đề rồi đấy 😥 😥 😥
}

Nếu controller không tương tác với các property trên mà chỉ đơn giản là truyền chúng đến màn tiếp theo thì cách này không hề tối ưu và làm nhiễu code với những thông tin không cần thiết. Lặp data cũng khiến việc refactor User vất vả hơn rất nhiều. Những nhược điểm này sẽ càng bộc lộ rõ khi số property hoặc số màn trong luồng tăng lên.

Tổng hợp data với closure

1. Đặc tính của function

Trước khi tìm hiểu mẹo dùng closure để tổng hợp data tạo nên User hoàn chỉnh ta cần biết một tính chất quan trọng của function

Về bản chất, function là một dạng đặc biệt của closure. Ngoại trừ protocol, mọi function trong swift đều tồn tại một phiên bản closure tương ứng. Ngoài ra, tất cả instance function của class/struct/enum đều có thể được sử dụng dưới dạng type function là một closure nhận instance của type đó làm input đầu tiên.

Ví dụ với instance method dequeueReusableCell của UITableView

Instance function of UITableView in type function form
dequeueReusableCell dưới dạng type function của UITableView

UITableView.dequeueReusableCell(withIdentifier:for:) sẽ là phiên bản type function ở dạng closure. Closure này được định nghĩa (UITableView) -> (String, IndexPath) -> UITableViewCell. Nếu bạn để ý, ouput của nó mô phỏng chính xác hình dạng của function ban đầu.

Instance function of UITableView in type function form after passing in an instance of UITableView
Sau khi truyền vào UITableView object, closure trả ra có hình dạng của instance function dequeueReusableCell

Tính chất trên cũng đúng với các function khởi tạo của class/struct. Hàm init của User nhận 4 String input là email, password, name, address, nên closure sẽ có dạng (String, String, String, String) -> User.

Closure form of User init function
Dạng closure của hàm khởi tạo của struct User

Ta có thể sử dụng typealias để dễ hình dung hơn


struct User {
    typealias Email = String
    typealias Address = String
    typealias Name = String
    typealias Address = String
    
    let email: String
    let password: String
    let name: String
    let address: String
}

//User.init có dạng (Email, Password, Name, Address) -> User


2. Curry

Curry là phương pháp biến closure với nhiều input thành closure mới dưới dạng chuỗi function nhận vào từng input một. Áp dụng curry cho closure init (Email, Password, Name, Address) -> User ở trên, ta sẽ đạt được closure mới (Email) -> (Password) -> (Name) -> (Address) -> User.

Curried form of User.init
Áp dụng curry với User.init

Lợi ích lớn nhất của curry là việc ta có thể truyền vào từng input một trước khi chạy function. Hay nói cách khác, thay vì phải truyền toàn bộ input tại cùng thời điểm, giờ đây ta đã có khả năng tổng hợp data theo từng bước 💡💡💡!

Trong swift, function curry dùng để transform closure với 4 input được định nghĩa


func curry<A, B, C, D, E>(_ f: @escaping (A, B, C, D) -> E) -> (A) -> (B) -> (C) -> (D) -> E {
    return { a in
        return { b in
            return { c in
                return { d in f(a, b, c, d) }
            }
        }
    }
}


Tổng kết

Với sức mạnh của closure và curry, thay vì lặp data ở các biến/model trung gian, ta chỉ cần lưu closure đại diện cho từng bước cấu tạo nên User và truyền nó sang màn tiếp theo.


typealias UserEmailResult = (User.Password) -> (User.Name) -> (User.Address) -> User
typealias UserPasswordResult = (User.Name) -> (User.Address) -> User
typealias UserNameResult = (User.Address) -> User

//ViewControllerA => input email
func get(email: String) -> UserEmailResult {
    let curriedInit = curry(User.init)
    return curriedInit(email)
}

//ViewControllerB => input password
func get(password: String, currentResult: UserEmailResult) -> UserPasswordResult {
    return currentResult(password)
}

//ViewControllerC => input name
func get(name: String, currentResult: UserPasswordResult) -> UserNameResult {
    return currentResult(name)
}

//ViewControllerD => input address => create User
func get(address: String, currentResult: UserNameResult) -> User {
    return currentResult(address)
}

Phương pháp này vừa khiến code tối giản hơn, vừa giúp ta mô tả luồng tạo User một cách logic và trực quan. Ta cũng không còn phải chắp vá User với optional properties hoặc các default value vô nghĩa. Tất cả các nhược điểm của 3 phương pháp nêu ở phần trước đều được giải quyết triệt để!

  1. https://oleb.net/blog/2014/07/swift-instance-methods-curried-functions/
  2. https://stackoverflow.com/a/27646945/10002969