Toàn tập Protocols trong Swift 3
Miễn trừ trách nhiệm
Đọc xong bài này bạn sẽ:
Hiểu rõ Protocol là gì?
Hiểu được cách khai báo, sử dụng Protocols
Hiểu được tại sao mấy ông Apple gọi Swift là Protocol Oriented Programming
Code bạn tổ chức sẽ "more elegent" hơn
Nếu không hiểu 3 điều trên, mình không chịu trách nhiệm
À bạn phải biết lập trình hướng đối tượng (OOP) trước đọc mới hiểu. Chứ hem là như tình yêu thiếu tình dục vậy. Đọc sẽ cảm thấy thiếu thiếu gì đó.
Tại sao có OOP rồi lại sinh ra Protocol Oriented Programming (POP)
Nói chung POP trong Swift giống khái niệm Interface ở những ngôn ngữ khác như C#, Java chẳng hạn. Tư tưởng không có gì mới nhưng cách thể hiện lại khác
Apple là vậy, họ muốn sản phẩm của họ có gì đó bí ẩn, khác biệt. Thay vì để là Interface, họ xài một từ mới là Protocol cho nó phong cách. Rồi thêm bớt chút đỉnh nữa.
Dưới đây là video quảng cáo về Protocol của bác Dave Abrahams. Nói chung là không biết sản phẩm ngon hay dởm, nhưng một điều chắc chắc là WWDC là show thuyết trình hay nhất mọi thời đại rồi.
<iframe width="800" height="600" src="//www.youtube.com/embed/g2LwFZatfTI" allowfullscreen="allowfullscreen"></iframe>
Vấn đề của OOP ?
Nhiều tài liệu sẽ hướng dẫn bạn xài POP luôn mà không nói đến tại sao phải xài. Mình khác, mình luôn đặt câu hỏi tại sao phải xài POP trong khi đã có OOP. Vậy OOP có nhược điểm là gì mà phải sinh ra POP?
Trời đã sinh OOP sao còn có POP? - Gia Cát Lượng
Cùng tìm hiểu qua ví dụ kinh điển: Tính lương nhân viên: Công ty có hai loại nhân viên là Nhân viên công nhật ( SalariedEmployee ) và nhân viên Partime ( PartimeEmployee )
class Employee{ var name: String
init(name: String) {
self.name = name
}
}
class SalariedEmployee: Employee{ var salary = 200000.0 var bonus = 10000.0 }
class ParttimeEmployee: Employee{ var hoursWorked = 0.0 var hourlyWage = 20.0 }
Bây giờ, ta muốn tính tổng chi phí công ty bỏ ra cho nhân sự qua một hàm dưới đây:
func pay(employees: [Employee]){ for employee in employees{ // employee.pay() } }
Nhưng trong lớp Employee chưa có hàm pay(). Ta phải quay lại thêm hàm này vào lớp cha:
class Employee{ var name: String
init(name: String) {
self.name = name
}
func pay() -> Double { return 0.0 }
}
Ta override lại hàm tính tiền này ở lớp 2 lớp con:
class SalariedEmployee: Employee{ var salary = 200000.0 var bonus = 10000.0
override func pay() -> Double {
return salary \* bonus
}
}
class ParttimeEmployee: Employee{ var hoursWorked = 0.0 var hourlyWage = 20.0
override func pay() -> Double {
return hoursWorked \* hourlyWage
}
}
Oke vậy là xong, sửa lại hàm tính tiền một chút, rồi tạo thử hai nhân viên:
let employee1 = SalariedEmployee(name: "Khoa dep trai") let employee2 = ParttimeEmployee(name: "Khoa thanh lich")
let employees = [employee1,employee2]
func pay(employees: [Employee]) -> Double{ var total = 0.0 for employee in employees{ total += employee.pay() } return total }
pay(employees: employees)
Như vậy là đã xong những gì ta cần.
Có hai vấn đề xảy ra:
1. Giả sử trường hợp ta có nhiều loại nhân viên.
Và ta quên cài đặt phương thức pay() ở một class nhân viên nào đó thì sai mọi chuyện vẫn diễn ra bình thường như ví dụ dưới đây.
class SalariedEmployee: Employee{ var salary = 200000.0 var bonus = 10000.0
// override func pay() -> Double { // return salary * bonus // } }
Thực tế là chương trình đã chạy sai.
2. Chẳng hạn công ty có tuyển thêm thực tập sinh.Tuy không phải nhân viên nhưng công ty vẫn trả phụ cấp cho thực tập sinh. Ta thêm một class mới vào:
class Intern: Employee{ var daysOfIntership = 69.0
override func pay() -> Double {
return daysOfIntership \* 10000
}
}
Bây giờ nếu Intern không kế thừa Employee. Khi tính tổng chi phí công ty bỏ ra cho nhân sự, ta phải viết 2 vòng lặp tính cho nhân viên và tính cho thực tập
Nếu Intern kế thừa Employee thì không hợp lý lắm. Đương nhiên với ví dụ nhỏ này, không thể lột tả hết vấn đề.
Nhưng chốt lại: Sẽ đôi lúc bạn do dự class này có kế thừa class kia không chỉ để dùng lại một ( vài ) hàm của class cha.
Để giải quyết vấn đề này, Protocols ra đời
Giới thiệu Protocols
Syntax
protocol SomeProtocol { // đinh nghĩa cho protocol có thể là properties hoặc method
}
Conform Protocol
Khi sử dụng protocol, ta phải đảm bảo dùng hết những gì đã khai báo trong protocol, nếu không, Swift complier sẽ báo lỗi thế này:
Mô hình hóa dữ liệu với Protocol
Quay lại ví dụ trả lương ban nãy, ta hãy mô hình lại dữ liệu với protocol:
Đầu tiên tạo một protocol mô hình lại những thứ phát sinh tiền cho công ty:
protocol Payable{ func pay() -> Double }
Trong class Employee không cần hàm pay() nữa
class Employee{ var name: String
init(name: String) {
self.name = name
}
}
Ở hai lớp con, bây giờ vừa cho kế thừa từ Employee, Payable. Nhớ là cài đặt hàm pay() để conform protocol Payable nhé
class SalariedEmployee: Employee, Payable{
var salary = 200000.0
var bonus = 10000.0
func pay() -> Double {
return salary \* bonus
}
}
class ParttimeEmployee: Employee, Payable{ var hoursWorked = 0.0 var hourlyWage = 20.0
func pay() -> Double {
return hoursWorked \* hourlyWage
}
}
Bây giờ, ta không còn đắn đó xem Intern có kế thừa từ Employee nữa hay không rồi:
class Intern: Payable{ var name: String var daysOfIntership: Int
init(name: String, daysOfIntership: Int) {
self.name = name
self.daysOfIntership = daysOfIntership
}
func pay() -> Double {
return Double(daysOfIntership) \* 10000
}
}
Ok, ta thêm dữ liệu để tính thử:
let employee1 = SalariedEmployee(name: "Khoa dep trai") let employee2 = ParttimeEmployee(name: "Khoa thanh lich") let intern1 = Intern(name: "Khoa handsome", daysOfIntership: 69)
Đoạn code này sẽ bị lỗi:
let numberOfPeople = [employee1,employee2,intern1]
Bời vì lúc này có 3 phần tử khác kiểu dữ liệu trong mảng.
Protocols as Types
Protocol được xem như một kiểu dữ liệu, tức là có thể sử dụng protocols:
Tham số truyền vào và return type trong hàm, phương thức, hàm khởi tạo
Protocol là kiểu dữ liệu cho biến hằng và thuộc tính của class
Kiểu dữ liểu của Array, Dictionary
Quay lại ví dụ trên, do cả 3 class SalariedEmployee, ParttimeEmployee và Intern đều thuộc kiểu protocol là Payable. Vì thế ta có thể bỏ cả các đối tượng khởi tạo từ class này thành chung 1 array:
let numberOfPeople: [Payable] = [employee1,employee2,intern1]
Và công việc tính toán tổng chi cho nhân sự:
func pay(employees: [Payable]) -> Double{ var total = 0.0 for employee in employees{ total += employee.pay() } return total } pay(employees: numberOfPeople)
Kế thừa của Protocol
Protocol cũng có kế thừa như class. Điều này làm việc thiết kế các lớp trở nên ảo diệu hơn.
Hãy xem qua ví dụ dưới đây:
protocol DebugLogger{ func printLog() -> String } struct User: DebugLogger { var username: String var age: Int var address: String
func printLog() -> String{
return ("\\(username) \\(age) \\(address)")
}
}
let user = User(username: "Khoa dep trai", age: 21, address: "Ho Chi Minh") user.printLog()
Bạn thêm một protocol là DetailDebugLogger kế thừa từ DebugLogger:
protocol DetailDebugLogger: DebugLogger{ func detailLog() -> String }
Và khi đó, User nếu chọn protocol DetailDebugLogger thì phải conform cả hai phương thức là detailLog() và printLog():
struct User: DetailDebugLogger { var username: String var age: Int var address: String
func printLog() -> String{
return "\\(username) \\(age) \\(address)"
}
func detailLog() -> String {
return "username: \\(username) - age: \\(age) - address: \\(address) "
}
}
OOP vs POP
Protocols có kế thừa. Vậy câu hỏi đặt ra là khi nào chọn class, khi nào chọn protocol?
Lúc này ra sẽ nhớ lại kiến thức mẫu giáo là IS-A và HAS-A.
Ví dụ Bird IS-A Animal, Bird kế thừa từ Animal.
Bird và Airplane HAS-A chức năng là Fly. Ta có thể tạo một protocol để gom nhóm những class có tính năng chung thế này:
protocol Flyable{ var velocity: Double { get } }
Tại sao Apple nói Swift là Protocol Oriented Programming?
Thực sự mình biết những ví dụ ở trên như tính tiền lương, chim, máy bay khá là dễ và đơn giản. Có thể bạn chưa thấy sự ảo dịu của Protocols qua những ví dụ trên. Ok. Chúng ta sẽ lấy những ví dụ cụ thể của ngôn ngữ Swift, xem thử nó được thiết kế như thế nào?
3 nhóm Protocols trong Swift's Standard Library
Can Do
Is A
Can Be
Can Do
Nhóm này thường có hậu tố -able phía sau. Nhóm protocols này thể hiện Object có thể làm được gì đó. Ví dụ từ đầu đến giờ như Payable.
Bạn gõ dòng này vào Playround
Equatable
Nhấn đè Command + trái chuột đề xem được phần định nghĩa protocol Equatable
Protocol này dùng để so sánh 2 đối tượng do người dùng khởi tạo:
struct User: DetailDebugLogger, Equatable { var username: String var age: Int var address: String
func printLog() -> String{
return "\\(username) \\(age) \\(address)"
}
func detailLog() -> String {
return "username: \\(username) - age: \\(age) - address: \\(address) "
}
static func ==(lhs: User, rhs: User) -> Bool{
return lhs.username == rhs.username
}
}
let user = User(username: "Khoa dep trai", age: 21, address: "Ho Chi Minh")
let user2 = User(username: "Khoa dep trai 2", age: 21, address: "Ho Chi Minh") print(user != user2)
Một số protocol khác thuộc loại này:
Strideable Hashable Comparable
Is A
Sẽ hơi rối với IS-A ở trên mình giải thích. Nhưng loại này chỉ là các protocol biểu diễn những kiểu dữ liệu mặc định trong Swift: Collection, Integer
Ở nhóm này bạn sẽ thấy Apple thiết kế theo hướng POP. Bên trái là nhỏ nhất
Integer -> Strideable -> Comparable -> Equatable
Can Be
Loại này thường bắt đầu ExpressibleBy
Trên hình bạn có thể thấy chủ yếu loại này dùng để chuyển đổi kiểu dữ liệu, biểu diễn dạng dữ liệu hiện tại sang loại khác.
Ok. Tóm váy lại là Swift được thiết kế theo POP. Các bạn cứ Command + Chuột trái vào các lớp để xem chuỗi kế thừa protocol nó thế nào.
Tổng kết:
Bài cũng khá dài rồi, gần 2000 từ chưa tính code. Hy vọng mọi người đã đọc đến đây và cảm thấy hài lòng.
Trước khi chia tay nhau, hãy cùng ôn lại những gì ta biết về protocol nhé:
Protocol như bản thiết kế các hành động object có thể thực hiện.
Dùng HAS-A để xác định khi nào xài protocol
Luôn nhớ phải conform protocol
Protocol as value type nha
Rảnh thì có thể đọc Swift's Standard Library để thấy Swift được thiết kế như thế nào.
Rất mong mọi đóng góp từ bạn, có gì cứ comment nhé!