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:
- ❌ Sử dụng và truy cập các property của
User
trở nên cồng kềnh hơn rất nhiều khi phải unwrap optional.User
càng được sử dụng ở nhiều nơi thì càng tạo ra nhiều code dư thừa. - ❌ Nếu bản chất các property trên trong
User
không phải optional, định nghĩa chúng như vậy sẽ làm nhiễu logic của tất cả các đoạn code tương tác vớiUser
. Điều này làm bẩn codebase, ảnh hưởng trực tiếp đến việc suy luận và đọc hiểu code. Khả năng cao developer đến sau sẽ không thể nắm rõ tại sao các property đó là optional và không dám mạnh tay refactor để cải thiện codebase.
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

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.

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
.

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
.

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 để!