Introducing Protocol-Oriented Programming in Swift 3 part II

Theo tài liệu: Protocol và tiếp theo từ Phần trước

Effects on the Swift Standard Library

Bạn đã thấy protocol extensions là 1 trong những cách rất hay để customize và mở rộng mọi thứ. Và bạn sẽ còn ngạc nhiên hơn nữa khi thấy Swift team sử dụng protocols để cải tiến Swift standard library. Thêm đoạn code sau vào cuối playground của bạn:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlide = slice.reversed()

let answer = reversedSlide.map { $0 * 10 }
print(answer)

Ví dụ này nhìn rất là đơn giản, và thậm chí bạn có thể đoán được answer sẽ được in ra màn hình ntn. Nhưng bạn sẽ ngạc nhiên với những types liên quan. Ví dụ như slice, không phải là 1 Array của các số integers mà là 1 ArraySlice<Int>. Kiểu wrapper đặc biệt này hoạt động như một Cách nhìn(view into) vào mảng ban đầu , và tránh được việc phân bổ bộ nhớ tốn kém mà có thể bổ sung nhanh chóng. tương tự, reversedSlice cũng thực chất là ReversedRandomAccessCollection<ArraySlice<Int>> tức cũng là một wrapper type view in to the original array. May thay, các thiên tài =)) phát triển thư viện chuẩn đã định nghĩa method map như 1 extension đối với protocol Sequence và tất cả các kiểu collection wrappers (có khoảng hàng chục loại) để conform protocol đó. Điều đó cho phép bạn gọi map ở trên Array cũng đơn giản như trên ReversedRandomAccessCollection mà ko thấy gì khác biệt. Và chúng ta sẽ học tập cái design pattern rất quan trọng này trong những phần sau.

Off to the Races

Ở phần trước, chúng ta đã định nghĩa 1 số loại Bird, bây h hãy add thêm vào 1 thứ hoàn toàn khác biệt:

class Motorcycle {
    init(name: String) {
        self.name = name
        speed = 200
    }
    var name: String
    var speed: Double
}

Class này hoàn toàn không liên quan gì đến chim bướm hay những thứ biết bay mà bạn đã định nghĩa trước đó. Nhưng nếu như bạn muốn xe máy chạy đua với chym cánh cụt thì sao? Đây là lúc ta ghép lại các mảnh ghép mà đã phân tích trước đó.

Bringing it Together

Đã đến lúc thống nhất tất cả các kiểu riêng lẻ vào 1 protocol chung cho cuộc đua (racing). Bạn có thể làm điều này mà ko cần phải quay lại và thay đổi các model đã đc define trước đó. Thuật ngữ thường được sử dụng cho cách làm này đc gọi là retroactive modeling. Add đoạn code sau vào playground:

protocol Racer {
    var speed: Double { get } // speed is the only thing racers care about
}

extension FlappyBird: Racer {
    var speed: Double {
        return airspeedVelocity
    }
}

extension SwiftBird: Racer {
    internal var speed: Double {
        return airspeedVelocity
    }
}

extension Penguin: Racer {
    var speed: Double {
        return 42 // full waddle speed
    }
}

extension UnladenSwallow: Racer {
    var speed: Double {
        return canFly ? airspeedVelocity : 0
    }
}

extension Motorcycle: Racer {}

let racers: [Racer] =   [UnladenSwallow.african,
                         UnladenSwallow.european,
                         UnladenSwallow.unknown,
                         Penguin(name: "King Penguin"),
                         SwiftBird(version: 3.0),
                         FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
                         Motorcycle(name: "Giacomo")]

Trong đoạn code này, đầu tiên, bạn define 1 protocol là Racer rồi sau đó, bạn bắt tất cả các types khác conform theo nó. 1 số types - như Motorcycle - thì conform 1 cách ko đáng kể (trivially) và đơn giản. Một số khác như UnladenSwallow cần 1 chút logic. Cuối cùng bạn đã có 1 loạt các types mà cùng conform với protocol Racer. Với tất cả các types đó, bạn đã tạo ra 1 array chứa các racers.

Top Speed

Cùng viết 1 function mà tính toán tốc độ cao nhất của các racers:

func topSpeed(of racers: [Racer]) -> Double {
  return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
 
topSpeed(of: racers) // 3000

function này sử dụng hàm max của standard library để tìm ra racer có speed lớn nhất và return. Bạn sẽ return 0 nếu như user truyền vào 1 array rỗng. Có vẻ như đó là Swift 3 FTW =))

Making it more generic

Có 1 vấn đề ở đây: giả sử như bạn muốn tìm racer có tốc độ cao nhất trong 1 phần của array racers. Thử thêm đoạn code sau vào playground:

topSpeed(of: racers[1...3]) // ERROR

Swift sẽ báo lỗi: "it cannot subscript a value of type [Racer] with an index of type CountableClosedRange". Như vậy khi lấy ra 1 phần của mảng racers, cái chúng ta nhận đc cũng là 1 kiểu wrapper. Để giải quyết vấn đề này, chúng ta sẽ viết lại hàm topSpeed mà ko dành cho 1 mảng cụ thể nữa mà dành cho 1 protocol phổ biến:

func topSpeed<RacerType: Sequence>(of racers: RacerType) -> Double
    where RacerType.Iterator.Element == Racer {
  return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}

Nhìn function này thể hơi đáng sợ 1 chút với <> nên hãy chia nhỏ nó ra và cùng xem xét: RacerTypegeneric type của function này và nó có thể bất cứ kiểu nào, miễn là nó conforms lại protocol Sequence. Mệnh đề where chỉ ra rằng mỗi 1 element ở trong kiểu tập hợp RacerType phải conform protocol Racer. Tất cả Sequence types có 1 associated type với tên là Iterator mà có thể loop qua tất cả các kiểu của Element. Body thực sự method cũng vẫn chỉ như trước đó. Và method topSpeed đã hoạt động với bất kỳ kiểu Sequence nào bao gồm trong array racers.

topSpeed(of: racers[1...3]) // 42

Make it More Swifty

Để làm cho mọi thứ có vẻ hướng đối tượng và có tính đóng gói hơn, bạn có thể extend Sequence type và viết như sau:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    return self.max(by: { $0.speed < $1.speed })?.speed ?? 0
  }
}
 
racers.topSpeed()        // 3000
racers[1...3].topSpeed() // 42

Bây giờ bạn đã có 1 method dễ dàng sử dụng nhắc lệnh mà có thể thực thi với mọi loại sequences của racers.

Protocol Comparators

Thêm đoạn code sau vào cuối playground:

protocol Score {
  var value: Int { get }
}
 
struct RacingScore: Score {
  let value: Int
}

Với 1 Score protocol, bạn có thể viết code để xử lý tất cả các kiểu scores theo cùng 1 cách. Tuy nhiên, với việc có nhiều types khác nhau, như RacingScore bạn sẽ ko muốn nó lẫn lộn với StyleScores hay CutenessScores. Bạn thực sự muốn socres là comparable để bạn có thể biết được ai cao điểm nhất. Trước swift 3, bạn sẽ cần phải add global operator functions để conform những protocol này. Bây giờ bạn chỉ cần define 1 số static method như sau:

protocol Score: Equatable, Comparable {
  var value: Int { get }
}
 
struct RacingScore: Score {
  let value: Int
 
  static func ==(lhs: RacingScore, rhs: RacingScore) -> Bool {
    return lhs.value == rhs.value
  }
 
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    return lhs.value < rhs.value
  }
}

Bạn đã đóng gói hoàn toàn logic của RacingScore trong nơi. Bây giờ bạn có thể so sánh scores, và thậm chí sử dụng các phép toán nhưgreater-than-or-equal-to` mà bạn ko cần phải defined 1 cách rõ ràng.

RacingScore(value: 150) >= RacingScore(value: 130)  // true