Pengantar Pemrograman Berorientasi Protokol di Swift
Diterbitkan: 2022-03-11Protokol adalah fitur yang sangat kuat dari bahasa pemrograman Swift.
Protokol digunakan untuk mendefinisikan “cetak biru metode, properti, dan persyaratan lain yang sesuai dengan tugas atau fungsi tertentu.”
Swift memeriksa masalah kesesuaian protokol pada waktu kompilasi, memungkinkan pengembang menemukan beberapa bug fatal dalam kode bahkan sebelum menjalankan program. Protokol memungkinkan pengembang untuk menulis kode yang fleksibel dan dapat diperluas di Swift tanpa harus mengorbankan ekspresi bahasa.
Swift mengambil kenyamanan menggunakan protokol selangkah lebih maju dengan menyediakan solusi untuk beberapa kebiasaan umum dan keterbatasan antarmuka yang mengganggu banyak bahasa pemrograman lainnya.
Di versi Swift sebelumnya, hanya dimungkinkan untuk memperluas kelas, struktur, dan enum, seperti yang berlaku di banyak bahasa pemrograman modern. Namun, sejak versi 2 Swift, menjadi mungkin untuk memperluas protokol juga.
Artikel ini membahas bagaimana protokol di Swift dapat digunakan untuk menulis kode yang dapat digunakan kembali dan dipelihara dan bagaimana perubahan pada basis kode berorientasi protokol besar dapat dikonsolidasikan ke satu tempat melalui penggunaan ekstensi protokol.
Protokol
Apa itu protokol?
Dalam bentuknya yang paling sederhana, protokol adalah antarmuka yang menjelaskan beberapa properti dan metode. Setiap jenis yang sesuai dengan protokol harus mengisi properti spesifik yang ditentukan dalam protokol dengan nilai yang sesuai dan menerapkan metode yang diperlukan. Contohnya:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
Protokol Antrian menjelaskan antrian, yang berisi item integer. Sintaksnya cukup mudah.
Di dalam blok protokol, ketika kita mendeskripsikan sebuah properti, kita harus menentukan apakah properti tersebut hanya gettable { get }
atau keduanya gettable dan settable { get set }
. Dalam kasus kami, variabel Count (dari tipe Int
) hanya dapat diperoleh.
Jika sebuah protokol membutuhkan sebuah properti untuk menjadi gettable dan settable, persyaratan tersebut tidak dapat dipenuhi oleh sebuah properti yang disimpan secara konstan atau properti yang dihitung hanya-baca.
Jika protokol hanya membutuhkan properti untuk dapat diperoleh, persyaratan dapat dipenuhi oleh semua jenis properti, dan valid untuk properti juga dapat diatur, jika ini berguna untuk kode Anda sendiri.
Untuk fungsi yang didefinisikan dalam protokol, penting untuk menunjukkan apakah fungsi tersebut akan mengubah konten dengan kata kunci mutating
. Selain itu, tanda tangan dari suatu fungsi sudah cukup sebagai definisi.
Agar sesuai dengan protokol, suatu tipe harus menyediakan semua properti instance dan mengimplementasikan semua metode yang dijelaskan dalam protokol. Di bawah, misalnya, adalah struct Container
yang sesuai dengan protokol Queue
kami. Struktur dasarnya menyimpan mendorong Int
s dalam items
array pribadi .
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
Namun, protokol Antrian kami saat ini memiliki kelemahan besar.
Hanya container yang berhubungan dengan Int
s yang dapat mengikuti protokol ini.
Kami dapat menghapus batasan ini dengan menggunakan fitur "jenis terkait". Jenis terkait bekerja seperti obat generik. Untuk mendemonstrasikan, mari ubah protokol Antrian untuk menggunakan tipe terkait:
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Sekarang protokol Antrian memungkinkan penyimpanan semua jenis item.
Dalam implementasi struktur Container
, kompilator menentukan tipe terkait dari konteks (yaitu, tipe pengembalian metode dan tipe parameter). Pendekatan ini memungkinkan kita untuk membuat struktur Container
dengan tipe item generik. Sebagai contoh:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
Menggunakan protokol menyederhanakan penulisan kode dalam banyak kasus.
Misalnya, objek apa pun yang mewakili kesalahan dapat menyesuaikan diri dengan protokol Error
(atau LocalizedError
, jika kita ingin memberikan deskripsi yang dilokalkan).
Logika penanganan kesalahan yang sama kemudian dapat diterapkan ke salah satu objek kesalahan ini di seluruh kode Anda. Akibatnya, Anda tidak perlu menggunakan objek tertentu (seperti NSError di Objective-C) untuk mewakili kesalahan, Anda dapat menggunakan jenis apa pun yang sesuai dengan protokol Error
atau LocalizedError
.
Anda bahkan dapat memperluas tipe String agar sesuai dengan protokol LocalizedError
dan membuang string sebagai kesalahan.
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }
Ekstensi Protokol
Ekstensi protokol dibangun di atas kehebatan protokol. Mereka memungkinkan kita untuk:
Menyediakan implementasi default metode protokol dan nilai default properti protokol, sehingga menjadikannya "opsional". Tipe yang sesuai dengan protokol dapat menyediakan implementasinya sendiri atau menggunakan implementasi default.
Tambahkan implementasi metode tambahan yang tidak dijelaskan dalam protokol dan "hiasi" semua jenis yang sesuai dengan protokol dengan metode tambahan ini. Fitur ini memungkinkan kita untuk menambahkan metode tertentu ke beberapa jenis yang sudah sesuai dengan protokol tanpa harus memodifikasi setiap jenis satu per satu.
Implementasi Metode Default
Mari kita buat satu protokol lagi:
protocol ErrorHandler { func handle(error: Error) }
Protokol ini menjelaskan objek yang bertugas menangani error yang terjadi pada suatu aplikasi. Sebagai contoh:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
Di sini kami hanya mencetak deskripsi kesalahan yang dilokalkan. Dengan ekstensi protokol, kami dapat menjadikan implementasi ini sebagai default.
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
Melakukan hal ini membuat metode handle
opsional dengan menyediakan implementasi default.
Kemampuan untuk memperluas protokol yang ada dengan perilaku default cukup kuat, memungkinkan protokol untuk tumbuh dan diperpanjang tanpa harus khawatir melanggar kompatibilitas kode yang ada.
Ekstensi Bersyarat
Jadi kami telah menyediakan implementasi default dari metode handle
, tetapi mencetak ke konsol tidak terlalu membantu pengguna akhir.
Kami mungkin lebih suka menunjukkan kepada mereka semacam tampilan peringatan dengan deskripsi yang dilokalkan dalam kasus di mana pengendali kesalahan adalah pengontrol tampilan. Untuk melakukan ini, kita dapat memperluas protokol ErrorHandler
, tetapi dapat membatasi ekstensi hanya berlaku untuk kasus-kasus tertentu (yaitu, ketika jenisnya adalah pengontrol tampilan).
Swift memungkinkan kita untuk menambahkan kondisi seperti itu ke ekstensi protokol menggunakan kata kunci where
.
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }
Self (dengan huruf kapital “S”) pada potongan kode di atas mengacu pada tipe (struktur, kelas atau enum). Dengan menetapkan bahwa kami hanya memperluas protokol untuk tipe yang mewarisi dari UIViewController
, kami dapat menggunakan metode khusus UIViewController
(seperti present(viewControllerToPresnt: animated: completion)
).

Sekarang, pengontrol tampilan apa pun yang sesuai dengan protokol ErrorHandler
memiliki implementasi default mereka sendiri dari metode handle
yang menunjukkan tampilan peringatan dengan deskripsi yang dilokalkan.
Implementasi Metode Ambigu
Mari kita asumsikan bahwa ada dua protokol, keduanya memiliki metode dengan tanda tangan yang sama.
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
Kedua protokol memiliki ekstensi dengan implementasi default dari metode ini.
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
Sekarang mari kita asumsikan bahwa ada tipe, yang sesuai dengan kedua protokol.
struct S: P1, P2 { }
Dalam hal ini, kami memiliki masalah dengan implementasi metode yang ambigu. Jenisnya tidak menunjukkan dengan jelas implementasi metode mana yang harus digunakan. Akibatnya, kami mendapatkan kesalahan kompilasi. Untuk memperbaikinya, kita harus menambahkan implementasi metode ke tipe.
struct S: P1, P2 { func method() { print("Method S") } }
Banyak bahasa pemrograman berorientasi objek diganggu dengan keterbatasan seputar resolusi definisi ekstensi yang ambigu. Swift menangani ini dengan cukup elegan melalui ekstensi protokol dengan memungkinkan pemrogram untuk mengambil kendali di mana kompiler gagal.
Menambahkan Metode Baru
Mari kita lihat protokol Queue
sekali lagi.
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Setiap jenis yang sesuai dengan protokol Queue
memiliki properti instance count
yang menentukan jumlah item yang disimpan. Ini memungkinkan kami, antara lain, membandingkan tipe-tipe tersebut untuk memutuskan mana yang lebih besar. Kita dapat menambahkan metode ini melalui ekstensi protokol.
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
Metode ini tidak dijelaskan dalam protokol Queue
itu sendiri karena tidak terkait dengan fungsionalitas antrian.
Oleh karena itu, ini bukan implementasi default dari metode protokol, melainkan implementasi metode baru yang "menghias" semua jenis yang sesuai dengan protokol Queue
. Tanpa ekstensi protokol, kami harus menambahkan metode ini ke setiap jenis secara terpisah.
Ekstensi Protokol vs. Kelas Dasar
Ekstensi protokol mungkin tampak sangat mirip dengan menggunakan kelas dasar, tetapi ada beberapa manfaat menggunakan ekstensi protokol. Ini termasuk, tetapi tidak terbatas pada:
Karena kelas, struktur, dan enum dapat menyesuaikan dengan lebih dari satu protokol, mereka dapat mengambil implementasi default dari beberapa protokol. Ini secara konseptual mirip dengan pewarisan berganda dalam bahasa lain.
Protokol dapat diadopsi oleh kelas, struktur, dan enum, sedangkan kelas dasar dan pewarisan hanya tersedia untuk kelas.
Ekstensi Pustaka Standar Swift
Selain memperluas protokol Anda sendiri, Anda dapat memperluas protokol dari pustaka standar Swift. Misalnya, jika kita ingin mencari ukuran rata-rata kumpulan antrian, kita dapat melakukannya dengan memperluas protokol Collection
standar.
Struktur data urutan yang disediakan oleh pustaka standar Swift, yang elemennya dapat dilalui dan diakses melalui subskrip yang diindeks, biasanya sesuai dengan protokol Collection
. Melalui ekstensi protokol, dimungkinkan untuk memperluas semua struktur data perpustakaan standar tersebut atau memperluas beberapa di antaranya secara selektif.
Catatan: Protokol yang sebelumnya dikenal sebagai
CollectionType
di Swift 2.x diubah namanya menjadiCollection
di Swift 3.
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }
Sekarang kita dapat menghitung ukuran rata-rata dari setiap kumpulan antrian ( Array
, Set
, dll.). Tanpa ekstensi protokol, kami perlu menambahkan metode ini ke setiap jenis koleksi secara terpisah.
Di pustaka standar Swift, ekstensi protokol digunakan untuk mengimplementasikan, misalnya, metode seperti map
, filter
, reduce
, dll.
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
Ekstensi Protokol dan Polimorfisme
Seperti yang saya katakan sebelumnya, ekstensi protokol memungkinkan kita untuk menambahkan implementasi default dari beberapa metode dan menambahkan implementasi metode baru juga. Tapi apa perbedaan antara kedua fitur ini? Mari kembali ke penangan kesalahan, dan cari tahu.
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
Hasilnya adalah kesalahan fatal.
Sekarang hapus deklarasi metode handle(error: Error)
dari protokol.
protocol ErrorHandler { }
Hasilnya sama: kesalahan fatal.
Apakah ini berarti tidak ada perbedaan antara menambahkan implementasi default dari metode protokol dan menambahkan implementasi metode baru ke protokol?
Tidak! Perbedaan memang ada, dan Anda dapat melihatnya dengan mengubah tipe handler
variabel dari Handler
ke ErrorHandler
.
let handler: ErrorHandler = Handler()
Sekarang output ke konsol adalah: Operasi tidak dapat diselesaikan. (ApplicationError kesalahan 0.)
Tetapi jika kita mengembalikan deklarasi metode handle(error: Error) ke protokol, hasilnya akan berubah kembali menjadi kesalahan fatal.
protocol ErrorHandler { func handle(error: Error) }
Mari kita lihat urutan apa yang terjadi dalam setiap kasus.
Ketika deklarasi metode ada dalam protokol:
Protokol mendeklarasikan metode handle(error: Error)
dan menyediakan implementasi default. Metode ini diganti dalam implementasi Handler
. Jadi, implementasi metode yang benar dipanggil saat runtime, apa pun jenis variabelnya.
Ketika deklarasi metode tidak ada dalam protokol:
Karena metode tidak dideklarasikan dalam protokol, tipe tidak dapat menimpanya. Itu sebabnya implementasi metode yang dipanggil tergantung pada jenis variabel.
Jika variabel bertipe Handler
, implementasi metode dari tipe tersebut akan dipanggil. Jika variabel bertipe ErrorHandler
, implementasi metode dari ekstensi protokol akan dipanggil.
Kode berorientasi protokol: Aman namun Ekspresif
Dalam artikel ini, kami mendemonstrasikan beberapa kekuatan ekstensi protokol di Swift.
Tidak seperti bahasa pemrograman lain dengan antarmuka, Swift tidak membatasi protokol dengan batasan yang tidak perlu. Swift bekerja di sekitar kebiasaan umum dari bahasa pemrograman tersebut dengan memungkinkan pengembang untuk menyelesaikan ambiguitas yang diperlukan.
Dengan protokol Swift dan ekstensi protokol, kode yang Anda tulis dapat menjadi ekspresif seperti kebanyakan bahasa pemrograman dinamis dan tetap aman untuk mengetik pada waktu kompilasi. Ini memungkinkan Anda memastikan penggunaan kembali dan pemeliharaan kode Anda dan membuat perubahan pada basis kode aplikasi Swift Anda dengan lebih percaya diri.
Kami berharap artikel ini bermanfaat bagi Anda dan menerima umpan balik atau wawasan lebih lanjut.