Header Ads

Protocol swift ios

Ứng dụng Protocol vào việc xây dựng kiến trúc ứng dụng

swift protocol

Bài này mình viết nhằm mở rộng cho bài Swift 2: Protocol trước đó. Các bạn có thể nên xem trước về Protocol để tiếp tục theo dõi bài viết này.

Protocol đóng vai trò quan trọng trong việc ẩn đi logic của các đối tượng, giúp chúng “loose coupling” (một nguyên tắc quan trọng trong việc xây dựng kiến trúc ứng dụng). Từ dó chúng ta có thể dễ dàng bảo trì (maintain), mở rộng (extend) cũng như tái sử dụng.

Tôi không biết bạn là ai !! Nhưng tôi biết bạn có khả năng làm được cái tôi cần.
Để giúp mọi người hiểu nguyên lý trên mình sẽ lấy ví dụ mối quan hệ kinh điển là Chủ Nợ và Con Nợ.

1. Hướng đối tượng cơ bản: biết tuốt về nhau, cứ có đối tượng là moi hết ra xài

// Chủ Nợ vs Con Nợ
// Chủ Nợ: có thể đòi nợ con nợ
// Con Nợ: có thể mượn tiền chủ nợ
// Theo cách viết thông thường ta có:
// Class person là 1 super class cho 2 class Borrower (con nợ) và Lender (chủ nợ), mục dích của class này để minh họa cho việc tại sao nên dùng Protocol thay vì cứ đặt hết method vào superclass rồi extend ra.
class Person {
var name:String
var age:Int
var money:Int = 0
init(name:String, age:Int) {
self.name = name
self.age = age
}
}
// Chủ nợ
class Lender:Person {
var borrower:Borrower? // giả sử chủ nợ sẽ có 1 con nợ
func requestPayment() {
if let borrower = self.borrower {
if borrower.money >= borrower.debt {
borrower.money -= borrower.debt
self.money += borrower.debt
borrower.debt = 0
}
}
}
}
// Con nợ
class Borrower:Person {
weak var lender:Lender?
// giả sữ con nợ thì có 1 chủ nợ thôi
//weak ở đây để chống retain cycle vì 2 class tham chiếu vào nhau
var debt:Int = 0
func borrowMoney(lender:Lender,money: Int) {
if lender.money >= money {
lender.money -= money
self.money += money
debt = money
self.lender = lender
lender.borrower = self
}
}
}
// logic vay tiền và đòi tiền đâu đó trong project như sau
// Khởi tạo các đối tượng chủ nợ và con nợ
let lenderObj = Lender(name: "Teo", age: 22)
lenderObj.money = 2000
let borrowerObj = Borrower(name: "Ti", age: 18)
// Mượn chủ nợ 1000
borrowerObj.borrowMoney(lenderObj, money: 1000)
// Một thời gian sau chủ nợ tới đòi tiền
lenderObj.requestPayment()
Đoạn code trên đại khái là ta sẽ viết cho 2 đối tượng có thể gọi đến nhau thông qua các method của chúng để vay và trả tiền. Tới đây chương trình không có lỗi và ta tự tin rằng mình đã có kiến thức tốt về hướng đối tượng.

2. Logic của class nào ra class nấy: tôi biết anh là ai và tôi biết anh có thể làm cái tôi muốn

Nếu chúng ta để ý sẽ thấy rằng hình như có gì đó không ổn .... Người mượn tiền thì biết rõ số tiền của người cho mượn và người cho mượn cũng thế. Giống kiểu bạn giật bóp tiền người ta rồi tự lấy tiền và khi nợ tới hạn người ta cũng đòi tiền theo cách mà bạn mượn tiền họ. Tới đây ta nói 2 đối tượng chủ nợ và con nợ biết quá nhiều về nhau hay nói 1 cách khác là dính chặt vào nhau (tight). Mà đó là điều mà ta nên né tránh trong việc xây dựng ứng dụng có kiến trúc tốt.
Thêm vào đó, cách viết trên ta giả sử nếu mà con nợ (Borrower) có thẻ tín dụng, tiền trong ngân hàng thì sao. Lúc đó ta phải update cả 2 object trên gần như toàn bộ logic cho vay tiền ra sao và trả tiền thế nào !!! Và điều này trong thực tế ta gặp rất nhìu, vd như mối quan hệ giữa đơn hàng (Order) và giỏ hàng (Cart) và Sản Phẩm hoặc Kho ... Theo cách trên là cứ lấy hết ruột gan nhau ra mà xài (dù chúng có được encapsulation hay không).
Ta có thể thay đổi logic cho 2 đối tượng trên như sau:
// Chủ nợ
class Lender:Person {
var borrower:Borrower? // giả sử chủ nợ sẽ có 1 con nợ
// Cho con nợ vay tiền
func lendMoney(borrower:Borrower, money:Int) -> Bool {
// Nếu tiền không đủ để cho mượn thì return false
guard self.money >= money else { return false }
self.borrower = borrower
self.money -= money
// Đưa tiền cho người mượn
borrower.receiveMoney(self, money: money)
return true
}
// Đòi nợ
func requestPayment() {
if let borrower = self.borrower {
if let returnMoney = borrower.payMoneyBack() {
// Con nợ đã trả hết tiền nợ, xong !
self.money += returnMoney
self.borrower = nil
} else {
// Trường hợp returnMoney = nil, con nợ vẫn chưa trả đc tiền \
}
}
}
}
// Con nợ
class Borrower:Person {
weak var lender:Lender?
var debt:Int = 0
// Con nợ nhận tiền từ chủ nợ
func receiveMoney(lender:Lender, money:Int) {
self.lender = lender
debt = money
self.money += money
}
// Trả nợ
func payMoneyBack() -> Int? {
var returnMoney:Int?
// Nếu đủ tiền thì trả hết, hết nợ
if money >= debt {
money -= debt
returnMoney = debt
debt = 0
self.lender = nil
} else {
// Chưa đủ tiển trả nên hẹn lần sau
return nil
}
return returnMoney
}
// Hỏi mượn tiền từ chủ nợ
func askForMoney(lender:Lender, money:Int) {
if lender.lendMoney(self, money: money) {
print("Yeah !!")
} else {
// Ặc phải tìm người khác để mượn rồi
}
}
}
// logic vay tiền và đòi tiền đâu đó trong project như sau
let lenderObj = Lender(name: "Teo", age: 22)
lenderObj.money = 2000
let borrowerObj = Borrower(name: "Ti", age: 18)
// Mượn tiền
borrowerObj.askForMoney(lenderObj, money: 1000)
// Chủ nợ đòi tiền
lenderObj.requestPayment()
Đoạn code sau phức tạp hơn khá nhiều nhưng đại khái lúc này ta nói rằng việc cho vay (của chủ nợ) và trả nợ (của con nợ) sẽ rõ ràng hơn. Đặc biệt ta không còn bị tình trạng mượn tiền kiểu côn đồ như cách đoạn code trước đó :). Logic cho việc lấy tiền ở đâu là việc riêng của cả 2 mà không muốn cho đối phương biết. Cách viết này tốt hơn cách đầu tiên nhưng vẫn chưa thể giải quyết vấn đề Lender - Borrower dính chặt vào nhau.

3. Tôi không biết bạn là ai nhưng tôi biết bạn có thể làm được cái tôi cần

Tới đây ta có thể giải quyết vấn đề trên bằng Protocol
protocol LenderBehavior:class {
func lendMoney(borrower:BorrowerBehavior, money:Int) -> Bool
func requestPayment()
}
protocol BorrowerBehavior {
func askForMoney(lender:LenderBehavior, money:Int)
func receiveMoney(lender:LenderBehavior, money:Int)
func payMoneyBack() -> Int?
}
// Chủ nợ
class Lender:Person, LenderBehavior {
var borrower:BorrowerBehavior? // thay vì là Borrower, thay lại BorrowerBehavior
func lendMoney(borrower:BorrowerBehavior, money:Int) -> Bool {
// Code như cũ
}
func requestPayment() {
// Code như cũ
}
}
// Con nợ
class Borrower:Person, BorrowerBehavior {
weak var lender:LenderBehavior?
var debt:Int = 0
func receiveMoney(lender:LenderBehavior, money:Int) {
// code như cũ
}
func payMoneyBack() -> Int? {
// code như cũ
}
func askForMoney(lender:LenderBehavior, money:Int) {
// Code như cũ
}
}
// logic vay tiền và đòi tiền đâu đó trong project như sau
let lenderObj = Lender(name: "Teo", age: 22)
lenderObj.money = 2000
let borrowerObj = Borrower(name: "Ti", age: 18)
// Mượn tiền
borrowerObj.askForMoney(lenderObj, money: 1000)
// Chủ nợ đòi tiền
lenderObj.requestPayment()
Điều gì xảy ra nếu ra có class Worker nào đó mà muốn vay tiền ?? Nếu Worker là subclass của Person thì mọi thứ khá đơn giản, nhưng nếu nó đang là subclass của 1 class khác thì sao ?? Ta biết trong hướng đối tượng, 1 class không thể kế thừa từ 2 class. Vì có Protocol rồi thì mọi thứ sẽ đơn giản hơn nhiều:
class Worker:SomeClass, BorrowerBehavior {
// mọi người thử viết cho class này nhé
}
// Hay thậm chí là 1 Company vừa phải vay tiền và lại vừa có thể đầu tư (cho vay)
class Company:OtherClass, BorrowerBehavior, LenderBehavior {
// ...
}
Ta thấy rằng dù class Worker có nhìu logic của nó đến chừng nào thì việc mượn tiền chỉ cần adopt protocol BorrowerBehavior và viết chi tiết các method bên trong. Với Company cũng thế :).
Lúc này 2 đối tượng Lender - Borrower không còn kết dính nữa, chúng cũng chẳng biết ai là ai, con nợ và chủ nợ lúc này chỉ còn là “người có khả năng cho vay” và “người có thể sẽ phải vay tiền”. Từ đó ta có thể linh hoạt sử dụng các đối tượng hơn, không cứ phải là Lender hay Borrower nữa.

4. Tầm quan trọng của Protocol

Protocol đóng vai trò như một chất phân giải các đối tượng, giúp chúng độc lập với nhau. Vì vậy nó được xem như là nền tảng của các kiến trúc ứng dụng đương đại như: VIPER, Clean Architect, ...
Protocol hầu như xuất hiện mọi nơi trong ứng dụng iOS, điển hình như: UITableView và UICollectionView chỉ làm nhiệm vụ layout các cell, scroll để thấy các cell tiếp theo. Nhưng chúng sẽ không tự giải quyết được có bao nhiêu item, section trong chúng, cũng như cell cho một vị trí cụ thế (indexPath) sẽ ra sao.... Những cái đó sẽ được “ủy thác" cho datasource. Tới đây mọi người cũng hiểu luôn cái delegate (cũng là dùng Protocol) của 2 view kinh điển này rồi nhé :)
Nguồn: IDE Academy

No comments