Pemrograman Berorientasi Protokol dalam Swift 5.1

Protokol adalah properti fundamental Swift. Mereka memainkan peran penting dalam pustaka Swift standar dan merupakan cara umum untuk mengkode abstraksi. Dalam banyak hal, mereka mirip dengan antarmuka dalam bahasa pemrograman lain.

Dalam tutorial ini, kami akan memperkenalkan Anda dengan pendekatan pengembangan aplikasi yang disebut pemrograman berorientasi protokol, yang telah menjadi inti dari Swift. Ini benar-benar yang perlu Anda pahami saat mempelajari Swift!

Dalam panduan ini, Anda:

  • memahami perbedaan antara pemrograman berorientasi objek dan pemrograman berorientasi;
  • memahami implementasi protokol standar;
  • Pelajari cara memperluas fungsi pustaka Swift standar;
  • Pelajari cara memperluas protokol dengan obat generik.

Mulai


Bayangkan Anda sedang mengembangkan game balap. Pemain Anda dapat mengendarai mobil, sepeda motor, dan pesawat terbang. Dan bahkan terbang di atas burung, ini adalah permainan, bukan? Hal utama di sini adalah bahwa ada dofiga dari semua jenis "hal-hal" di mana Anda dapat mengemudi, terbang, dll.

Pendekatan umum untuk mengembangkan aplikasi tersebut adalah pemrograman berorientasi objek. Dalam hal ini, kami menyertakan semua logika di beberapa kelas dasar, dari mana kami mewarisi dari sana. Kelas dasar, oleh karena itu, harus mengandung logika "timah" dan "pilot".

Kami memulai pengembangan dengan menciptakan kelas untuk setiap kendaraan. Kami akan menunda "Burung" sampai nanti, kami akan kembali kepada mereka nanti.

Kita melihat Mobil dan Motor itufungsinya sangat mirip, jadi kami membuat kelas dasar MotorVehicle . Mobil dan Sepeda Motor akan diwarisi dari MotorVehicle. Dengan cara yang sama, kita membuat kelas dasar Pesawat , dari mana kita akan membuat kelas Pesawat .

Anda berpikir bahwa semuanya baik-baik saja, tetapi - bam! - Aksi permainan Anda berlangsung di abad XXX, dan beberapa mobil sudah bisa terbang .

Jadi, kami memiliki lelucon. Swift tidak memiliki banyak warisan. Bagaimana mobil terbang Anda diwarisi dari MotorVehicle dan Aircraft? Buat kelas dasar lain yang menggabungkan kedua fungsi? Kemungkinan besar tidak, karena tidak ada cara yang jelas dan sederhana untuk mencapai ini.

Dan apa yang akan menyelamatkan permainan kita dalam situasi yang mengerikan ini? Pemrograman berorientasi protokol sedang terburu-buru untuk membantu!

Seperti apa pemrograman berorientasi protokol?


Protokol memungkinkan Anda mengelompokkan metode, fungsi, dan properti yang serupa untuk kelas, struktur, dan enumerasi. Namun, hanya kelas yang memungkinkan Anda untuk menggunakan warisan dari kelas dasar.

Keuntungan protokol di Swift adalah bahwa suatu objek dapat menyesuaikan diri dengan banyak protokol.

Saat menggunakan metode ini, kode Anda menjadi lebih modular. Pikirkan protokol sebagai blok bangunan fungsionalitas. Ketika Anda menambahkan fungsionalitas baru ke objek, membuatnya sesuai dengan protokol tertentu, Anda tidak membuat objek yang sama sekali baru dari awal, itu akan terlalu lama. Sebagai gantinya, Anda menambahkan blok bangunan berbeda hingga objek siap.

Pindah dari kelas dasar ke protokol akan menyelesaikan masalah kita. Dengan protokol, kita dapat membuat kelas FlyingCar yang cocok dengan MotorVehicle dan Aircraft. Bagus, ya?

Mari kita lakukan kodenya


Jalankan Xcode, buat taman bermain, simpan sebagai SwiftProtocols.playground, tambahkan kode ini:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}


Kompilasi dengan Command-Shift-Return untuk memastikan semuanya beres.

Di sini kita mendefinisikan protokol Bird sederhana , dengan nama dan properti canFly . Kemudian kita mendefinisikan protokol Flyable dengan properti airspeedVelocity .

Dalam "era pra-protokol", pengembang akan mulai dengan kelas Flyable sebagai kelas dasar, dan kemudian, menggunakan warisan, akan mendefinisikan Bird dan segala sesuatu yang bisa terbang.

Tetapi dalam pemrograman berorientasi protokol, semuanya dimulai dengan protokol. Teknik ini memungkinkan kita untuk merangkum sketsa fungsional tanpa kelas dasar.

Seperti yang akan Anda lihat sekarang, ini membuat proses desain tipe jauh lebih fleksibel.

Tentukan jenis yang sesuai dengan protokol


Tambahkan kode ini di bagian bawah taman bermain:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}


Kode ini mendefinisikan struktur FlappyBird baru yang sesuai dengan protokol Bird dan protokol Flyable. Properti airspeedVelocity adalah karya Flappy Frequency dan flappy Amplitude. Properti canFly mengembalikan true.

Sekarang tambahkan definisi dari dua struktur lagi:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}


Penguin adalah burung tetapi tidak bisa terbang. Adalah baik bahwa kita tidak menggunakan warisan dan tidak membuat semua burung dapat diterbangkan !

Saat menggunakan protokol, Anda mendefinisikan komponen fungsional dan membuat semua objek yang cocok sesuai dengan protokol.Kemudian

kami mendefinisikan SwiftBird , tetapi dalam permainan kami ada beberapa versi yang berbeda. Semakin besar nomor versi, semakin besar kecepatannya , yang didefinisikan sebagai properti yang dihitung.

Namun, ada beberapa redundansi. Setiap jenis Bird harus mendefinisikan definisi eksplisit dari properti canFly, meskipun kami memiliki definisi protokol Flyable. Sepertinya kita perlu cara untuk menentukan implementasi standar metode protokol. Ya, ada ekstensi protokol untuk ini.

Memperluas protokol dengan perilaku default


Ekstensi protokol memungkinkan Anda mengatur perilaku protokol default. Tulis kode ini segera setelah definisi protokol Bird:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}


Kode ini mendefinisikan ekstensi protokol Bird . Ekstensi ini menentukan bahwa properti canFly akan mengembalikan true ketika jenisnya sesuai dengan protokol Flyable . Dengan kata lain, burung yang dapat diterbangkan tidak perlu lagi mengatur secara eksplisit canFly.

Sekarang hapus let canFly = ... dari definisi FlappyBird, Penguin, dan SwiftBird. Kompilasi kodenya dan pastikan semuanya beres.

Mari kita lakukan transfer


Enumerasi dalam Swift dapat sesuai dengan protokol. Tambahkan definisi enumerasi berikut:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}


Dengan mendefinisikan properti yang sesuai, UnladenSwallow sesuai dengan dua protokol - Bird and Flyable. Dengan cara ini, definisi default untuk canFly diimplementasikan.

Mengganti perilaku default


Tipe kami UnladenSwallow , sesuai dengan protokol Bird , secara otomatis menerima implementasi untuk canFly . Kami, bagaimanapun, perlu UnladenSwallow.unknown untuk mengembalikan false untuk canFly.

Tambahkan kode berikut di bawah ini:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

Sekarang hanya .african dan .european akan kembali benar untuk canFly. Coba lihat! Tambahkan kode berikut di bagian bawah taman bermain kami:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

Kompilasi taman bermain dan periksa nilai yang diterima dengan yang ditunjukkan dalam komentar di atas.

Jadi, kita mendefinisikan kembali properti dan metode dengan cara yang sama seperti menggunakan metode virtual dalam pemrograman berorientasi objek.

Protokol yang berkembang


Anda juga dapat membuat protokol Anda sendiri sesuai dengan protokol lain dari pustaka standar Swift dan menentukan perilaku default. Ganti deklarasi protokol Burung dengan kode berikut:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

Kepatuhan dengan protokol CustomStringConvertible berarti bahwa tipe Anda harus memiliki properti deskripsi. Alih-alih menambahkan properti ini dalam jenis Burung dan dalam semua turunannya, kami mendefinisikan ekstensi protokol CustomStringConvertible, yang akan dikaitkan hanya dengan jenis Burung.

Ketik UnladenSwallow.african di bagian bawah taman bermain. Kompilasi dan Anda akan melihat "Saya bisa terbang".

Protokol di Perpustakaan Standar Swift


Seperti yang Anda lihat, protokol adalah cara yang efektif untuk memperluas dan menyesuaikan tipe. Di perpustakaan standar Swift, properti ini juga banyak digunakan.

Tambahkan kode ini ke taman bermain:

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

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

Anda mungkin tahu kode apa yang akan ditampilkan, tetapi Anda mungkin akan terkejut dengan jenis yang digunakan di sini.

Misalnya, slice bukan Array, tetapi ArraySlice. Ini adalah "pembungkus" khusus yang menyediakan cara efisien untuk bekerja dengan bagian-bagian array. Oleh karena itu, reversedSlice adalah ReversedCollection <ArraySlice>.

Untungnya, fungsi peta didefinisikan sebagai ekstensi dari protokol Sequence, yang sesuai dengan semua jenis koleksi. Ini memungkinkan kita untuk menerapkan fungsi peta ke Array dan ReversedCollection dan tidak melihat perbedaannya. Segera Anda akan memanfaatkan trik yang bermanfaat ini.

Pada tanda Anda


Sejauh ini, kami telah mengidentifikasi beberapa jenis yang sesuai dengan protokol Bird. Sekarang kita akan menambahkan sesuatu yang sama sekali berbeda:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

Jenis ini tidak ada hubungannya dengan burung dan penerbangan. Kami ingin mengatur balap motor dengan penguin. Sudah waktunya untuk membawa perusahaan aneh ini ke awal.

Menyatukan semuanya


Untuk menyatukan pembalap yang berbeda, kita perlu protokol umum untuk balap. Kita dapat melakukan semua ini tanpa menyentuh semua jenis yang kita buat sebelumnya, dengan bantuan hal yang luar biasa yang disebut pemodelan retroaktif. Cukup tambahkan ini ke taman bermain:

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

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

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

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

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

extension Motorcycle: Racer {}

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

Inilah yang kami lakukan di sini: pertama, tentukan protokol Racer. Ini semua yang bisa berpartisipasi dalam balapan. Kemudian kami membuang semua tipe yang sebelumnya kami buat ke protokol Racer. Dan akhirnya, kami membuat Array yang berisi instance dari masing-masing tipe kami.

Kompilasi taman bermain agar semuanya beres.

Kecepatan tertinggi


Kami menulis fungsi untuk menentukan kecepatan maksimum pengendara. Tambahkan kode ini di ujung taman bermain:

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

topSpeed(of: racers) // 5100

Di sini kita menggunakan fungsi max untuk menemukan pengendara dengan kecepatan maksimum dan mengembalikannya. Jika array kosong, maka 0,0 dikembalikan.

Menjadikan fungsi lebih umum


Misalkan array Racers cukup besar, dan kita perlu menemukan kecepatan maksimum tidak di seluruh array, tetapi di beberapa bagiannya. Solusinya adalah mengubah topSpeed ​​(dari :) sehingga dibutuhkan sebagai argumen tidak secara khusus array, tetapi segala sesuatu yang sesuai dengan protokol Sequence.

Ganti implementasi topSpeed ​​(of :) kami sebagai berikut:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

  1. RacersType adalah tipe argumen umum dari fungsi kita. Itu bisa apa saja yang sesuai dengan protokol Sequence.
  2. di mana menentukan bahwa konten Sequence harus mematuhi protokol Racer.
  3. Tubuh fungsi itu sendiri tetap tidak berubah.

Periksa dengan menambahkan ini di akhir taman bermain kami:

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

Sekarang fungsi kami bekerja dengan semua jenis yang memenuhi protokol Sequence, termasuk ArraySlice.

Membuat fungsi lebih "cepat"


Rahasia: Anda dapat melakukan yang lebih baik lagi. Tambahkan ini di bagian paling bawah:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

Dan sekarang kami telah memperluas protokol Sequence itu sendiri dengan topSpeed ​​(). Itu hanya berlaku ketika Sequence berisi tipe Racer.

Pembanding Protokol


Fitur lain dari protokol Swift adalah bagaimana Anda mendefinisikan operator objek yang setara atau perbandingannya. Kami menulis yang berikut ini:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

Dengan protokol Skor, Anda dapat menulis kode yang memperlakukan semua elemen jenis ini dalam satu cara. Tetapi jika Anda mendapatkan jenis yang sangat spesifik, seperti RacingScore, maka Anda tidak akan mengacaukannya dengan turunan lain dari protokol Skor.

Kami ingin skor dibandingkan untuk melihat siapa yang memiliki skor tertinggi. Sebelum Swift 3, pengembang perlu menulis fungsi global untuk mendefinisikan operator untuk protokol. Sekarang kita dapat mendefinisikan metode statis ini dalam model itu sendiri. Kami melakukan ini dengan mengganti definisi Score dan RacingScore sebagai berikut:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

Kami memiliki semua logika untuk RacingScore di satu tempat. Protokol Sebanding mengharuskan Anda untuk menentukan implementasi hanya untuk fungsi yang kurang dari itu. Semua fungsi perbandingan lainnya akan diimplementasikan secara otomatis, berdasarkan pada implementasi fungsi "kurang dari" yang kami buat.

Pengujian:

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

Membuat perubahan pada objek


Sejauh ini, setiap contoh telah menunjukkan cara menambahkan fungsionalitas. Tetapi bagaimana jika kita ingin membuat protokol yang mengubah sesuatu pada suatu objek? Ini dapat dilakukan dengan menggunakan metode mutasi dalam protokol kami.

Tambahkan protokol baru:

protocol Cheat {
  mutating func boost(_ power: Double)
}

Di sini kita mendefinisikan protokol yang memungkinkan kita untuk menipu. Bagaimana? Mengubah isi dorongan secara sewenang-wenang.

Sekarang buat ekstensi SwiftBird yang sesuai dengan protokol Cheat:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

Di sini kita menerapkan fungsi boost (_ :), meningkatkan speedFactor dengan nilai yang dikirimkan. Kata kunci yang bermutasi membuat struktur mengerti bahwa salah satu nilainya akan diubah oleh fungsi ini.

Coba lihat!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Kesimpulan


Di sini Anda dapat mengunduh kode sumber lengkap untuk taman bermain.

Anda belajar tentang kemungkinan pemrograman berorientasi protokol dengan membuat protokol sederhana dan meningkatkan kemampuannya dengan bantuan ekstensi. Menggunakan implementasi default, Anda memberikan protokol "perilaku" yang sesuai. Hampir suka dengan kelas dasar, tetapi hanya lebih baik, karena semua ini juga berlaku untuk struktur dan enumerasi.

Anda juga melihat bahwa ekstensi protokol berlaku untuk protokol Swift yang mendasarinya.

Di sini Anda akan menemukan panduan protokol resmi .

Anda juga dapat menonton kuliah WWDC yang luar biasa tentang pemrograman berorientasi protokol.

Seperti halnya paradigma pemrograman, ada bahaya terbawa dan mulai menggunakan protokol kiri dan kanan. Berikut ini catatan menarik tentang bahaya keputusan gaya peluru perak.

Source: https://habr.com/ru/post/undefined/


All Articles