Hiểu thêm về Swift với Tuples, Protocols, Delegates

Trong bài Tìm hiểu Swift lần trước chúng ta đã có 1 ứng dụng tính tiền Tip đơn giản trên iOS. Tuy nhiên mỗi khi chúng ta chọn số Tip để tính ra số tiền thì chung ta sẽ lại phải nhớ tổng số tiền (trước thuế) ở trong đầu. Điều này khá là bất tiện.

Nó sẽ tốt hơn nếu method calcTipWithTipPct trả về 2 giá trị là số tiền tip và số tiền trước thuế (số tiền dùng để tính tip).

Đối với Objective-C, nếu bạn muốn làm method trả về 2 giá trị, bạn sẽ phải tạo 1 object với 2 property hoặc trả về 1 dictionary chứa 2 giá trị. Đối với Swift, chúng ta có thêm cách khác nữa là: Tuples.

Chúng ta sẽ cùng thử một chút với Tuples để hiểu thêm về nó. Tạo 1 file Playground mới trong Xcode hoặc bạn cũng có thể dùng file có sẵn của bạn. Xoá tất cả nội dung và chúng ta sẽ cùng làm từ đầu

Unnamed Tuples

Cùng bắt đầu bằng việc tạo ra 1 Unamed Tuples bằng cách gõ dòng sau vào trong file playground:

let tipAndTotal = (4.00, 25.19)

Ở đây bạn đã nhóm 2 giá trị Double (tip và total) vào trong 1 giá trị Tuple. Chúng ta sử dụng inferred syntax vì compiler sẽ tự động xác định được kiểu biến dựa trên giá trị khởi tạo. Ngoài ra chúng ta cũng có thể khai báo theo kiểu explicitly như sau

let tipAndTotal:(Double, Double) = (4.00, 25.19)

Để truy xuất đến giá trị trong Tuple, chúng ta có 2 cách, 1 là theo index

tipAndTotal.0
tipAndTotal.1

2 là theo tên

let (theTipAmt, theTotal) = tipAndTotal
theTipAmt
theTotal

Named Tuples

Sử dụng Unamed Tuples khiến chúng ta mất thêm vài dòng code để có thể truy cập được mỗi item. Sử dụng Named Tuples sẽ làm cho việc đó dễ dàng hơn bằng cách khai báo tên ngay từ khi khởi tạo

let tipAndTotalNamed = (tipAmt:4.00, total:25.19)
tipAndTotalNamed.tipAmt
tipAndTotalNamed.total

Dễ dàng hơn rất nhiều phải không? Nếu bạn chú ý thì chúng ta đã sử dụng inferred syntax. Bạn cũng có thể dùng explicit syntax như sau:

let tipAndTotalNamed:(tipAmt:Double, total:Double) = (4.00, 25.19)

Khi sử dụng explicit syntax thì việc đặt tên cho biến phía bên phải là không cần thiết.

Returning Tuples

Bây giờ bạn cũng hiểu cơ bản về Tuples rồi, chúng ta sẽ cùng thử sử dụng nó trong ứng dụng Tip Calculator để trả về 2 giá trị.

Thêm đoạn code sau vào trong file playground:

let total = 21.19
let taxPct = 0.06
let subtotal = total / (taxPct + 1)
func calcTipWithTipPct(tipPct:Double) -> (tipAmt:Double, total:Double) {
  let tipAmt = subtotal * tipPct
  let finalTotal = total + tipAmt
  return (tipAmt, finalTotal)
}
calcTipWithTipPct(0.20)

method calcTipWithTipPct này cũng giống như method calcTipWithTipPct chúng ta đã sử dụng trong bài trước. Ngoại trừ việc thay vì trả về 1 giá Double, chúng ta trả về (tipAmt:Double, total:Double)

Bây giờ bạn có thể xoá tất cả nội dung trong file playground vì chúng ta sẽ bắt đầu một phần hoàn toàn mới.

Full Prototype

Ở thời điểm này, bạn đã sẵn sàng cho việc sử dụng thứ mới học được vào trong class TipCalculatorModel. Nhưng trước khi thực sự thay đổi class đó trong project, hãy thử một chút những thay đổi này trong file playground nhé. Copy nội dung của class TipCalculatorModel trong project TipCalculator vào file playground và thử thay đổi method calcTipWithTipPct giống như bạn đã làm trước đó. Sau đó thay đổi returnPossibleTips trả về Dictionary Ints và Tuples thay vì Ints và Double như trước đó.

Hãy thử xem thế nào nhé, bạn cũng có thể tham khảo đoạn code dưới đây:

import Foundation

class TipCalculatorModel {

  var total: Double
  var taxPct: Double
  var subtotal: Double {
    get {
      return total / (taxPct + 1)
    }
  }

  init(total: Double, taxPct: Double) {
    self.total = total
    self.taxPct = taxPct
  }

    func calcTipWithTipPct(tipPct:Double) -> (tipAmt:Double, total:Double) {
      let tipAmt = subtotal * tipPct
      let finalTotal = total + tipAmt
      return (tipAmt, finalTotal)
    }

  func returnPossibleTips() -> [Int: (tipAmt:Double, total:Double)] {

    let possibleTipsInferred = [0.15, 0.18, 0.20]
    let possibleTipsExplicit:[Double] = [0.15, 0.18, 0.20]

    var retval = Dictionary<Int, (tipAmt:Double, total:Double)>()
    for possibleTip in possibleTipsInferred {
      let intPct = Int(possibleTip*100)
      retval[intPct] = calcTipWithTipPct(possibleTip)
    }
    return retval

  }

}

Save file này lại và khởi tạo một file playground mới. Chúng ta sẽ dùng tới file playground sau.

Protocols

Một Protocol là một list các method chỉ định một "contract" hoặc "interface". Thêm dòng sau vào file playground mới tạo để hiểu rõ hơn:

protocol Speaker {
  func Speak()
}

Protocol này khai báo 1 method là Speak(). Bất kỳ 1 class nào "conform" protocol này đều phải khai báo method này. Add thêm đoạn code sau vào file để hiểu hơn:

class Vicki: Speaker {
  func Speak() {
    println("Hello, I am Vicki!")
  }
}

class Ray: Speaker {
  func Speak() {
    println("Yo, I am Ray!")
  }
}

Để 1 class conform 1 protocol, bạn khai báo : sau tên class và tiếp theo là tên protocol. 2 class trên không kế thừa class nào nên bạn chỉ việc thêm trực tiếp tên của protocol vào sau :. Nếu class của bạn conform protocol nhưng không implement method của protocol thì sẽ có lỗi. Hãy thử xoá method Speak() ở class Ray bạn sẽ thấy lỗi xuất hiện.

Bây giờ hãy thử đối với class kế thừa:

class Animal {
}
class Dog : Animal, Speaker {
  func Speak() {
    println("Woof!")
  }
}

Class Dog kế thừa class Animal và conform protocol Speaker. 1 class chỉ có thể kế thừa 1 class nhưng có thể conform nhiều protocol.

Optional Protocols

Bạn có thể đánh dấu 1 method của protocol trở thành optional bằng việc thêm keyword optional trước method và thêm tag @objc trước protocol. Hãy thử nó bằng cách thay thế protocol Speaker như sau:

@objc protocol Speaker {
  func Speak()
  optional func TellJoke()
}

Nếu bạn thấy báo lỗi ở @objc hãy thêm dòng sau vào dòng đầu tiên của file playground

import Foundation

và thêm tag @objc trước tất cả các method func Speak() của mỗi class conform protocol Speaker

Bạn có thể thấy rằng không có lỗi cho cả class Vicki, Ray hay Dog tuy rằng không class nào khai báo method TellJoke()

Trong ví dụ này, VickiRay có thể "joke" còn Dog thì tất nhiên là không rồi nên chúng ta sẽ chỉ implement method TellJoke() trên 2 class này:

class Vicki: Speaker {
  @objc func Speak() {
    println("Hello, I am Vicki!")
  }
  func TellJoke() {
    println("Q: What did Sushi A say to Sushi B?")
  }
}

class Ray: Speaker {
  @objc func Speak() {
    println("Yo, I am Ray!")
  }
  func TellJoke() {
    println("Q: Whats the object-oriented way to become wealthy?")
  }
  func WriteTutorial() {
    println("I'm on it!")
  }
}

Sử dụng Protocols

Chúng ta đã tạo ra 1 protocol và vài class conform nó, bây giờ hãy thử sử dụng chúng. Thêm đoạn code sau vào file playgrund của bạn:

var speaker: Speaker
speaker = Ray()
speaker.Speak()
// speaker.WriteTutorial() // error!
(speaker as! Ray).WriteTutorial()
speaker = Vicki()
speaker.Speak()

Chú ý rằng thay vì khai báo speaker như là Ray thì chúng ta lại khai báo Speaker. Điều đó có nghĩa rằng chúng ta chỉ có thể gọi đến những method được implement trong Speaker ngay cả khi thực sự speaker là kiểu Ray. Để gọi đến những method khác của class Ray chúng ta sẽ phải tạm thời cast speaker sang Ray như bên trên.

Bây giờ thì thêm những dòng sau để thử với optional method:

speaker.TellJoke?()
speaker = Dog()
speaker.TellJoke?()

Delegates

1 delegate thực tế là 1 biến mà conform 1 protocol, thực hiện việc thông báo 1 sự kiến xảy ra hoặc thực hiện nhiều sub-tasks. Để hiểu thêm về nó. bạn hãy từ từ thử theo từng bước sau.

Thêm 1 class mới DateSimulator vào file playground của bạn:

class DateSimulator {

  let a:Speaker
  let b:Speaker

  init(a:Speaker, b:Speaker) {
    self.a = a
    self.b = b
  }

  func simulate() {
    println("Off to dinner...")
    a.Speak()
    b.Speak()
    println("Walking back home...")
    a.TellJoke?()
    b.TellJoke?()
  }
}

let sim = DateSimulator(a:Vicki(), b:Ray())
sim.simulate()

Tưởng tượng bạn có thể thông báo cho class khác khi cuộc hẹn bắt đầu hoặc kết thúc. Điều này có thể hữu ích, ví dụ nếu bạn muốn status thông báo xuất hiện hoặc biến mất khi những sự kiện này xảy ra.

Trước tiên chúng ta cần tạo ra protocol vói các sự kiện mà bạn muốn thông báo. Thêm đoạn code sau trước class DateSimulator

protocol DateSimulatorDelegate {
  func dateSimulatorDidStart(sim:DateSimulator, a:Speaker, b:Speaker)
  func dateSimulatorDidEnd(sim:DateSimulator, a: Speaker, b:Speaker)
}

Sau đó tạo 1 class conform delegate này:

class LoggingDateSimulator:DateSimulatorDelegate {
  func dateSimulatorDidStart(sim:DateSimulator, a:Speaker, b:Speaker) {
    println("Date started!")
  }
  func dateSimulatorDidEnd(sim:DateSimulator, a: Speaker, b: Speaker)  {
    println("Date ended!")
  }
}

Tiếp theo chúng ta thêm 1 property mới cho class DateSimulator

var delegate:DateSimulatorDelegate?

Ngay phía trước dòng

sim.simulate()

thêm dòng sau:

sim.delegate = LoggingDateSimulator()

Cuối cùng, thay đổi 1 chút simulate() function để gọi delegate lúc bắt đầu và kết thúc của method:

  func simulate() {
    delegate?.dateSimulatorDidStart(self, a: a, b: b)
    println("Off to dinner...")
    a.Speak()
    b.Speak()
    println("Walking back home...")
    a.TellJoke?()
    b.TellJoke?()
    delegate?.dateSimulatorDidEnd(self, a: a, b: b)
  }

Đến đây chắc các bạn cũng đã hiểu được cơ bản về Tuples, Protocol cũng như Delegate rồi đúng không? Hẹn gặp lại các bạn trong các bài tới.


All Rights Reserved