
Hari ini saya ingin menunjukkan kepada Anda bagaimana cara membuat Publisher Anda sendiri dalam kerangka kerja Combine baru Apple.
Jadi, sebagai permulaan, kita perlu mengingat secara singkat bagaimana bagian mendasar dari Combine, yaitu Publisher, Subscription, Subscriber, saling berinteraksi.
- Pelanggan bergabung dengan Penerbit
- Penerbit mengirimkan Pelanggan Berlangganan
- Pelanggan meminta nilai N dari Langganan
- Penerbit mengirimkan nilai N atau kurang
- Penerbit mengirimkan sinyal penyelesaian
Penerbit
Jadi mari kita mulai membuat Publisher kita. Beralih ke dokumentasi Apple, kita akan melihat bahwa Penerbit adalah protokol.
public protocol Publisher { associatedtype Output associatedtype Failure : Error func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input }
Di mana Keluaran adalah jenis nilai yang diteruskan oleh Penerbit ini, Kegagalan adalah jenis kesalahan yang harus mengikuti protokol Kesalahan.
Dan fungsi terima (_: Pelanggan), yang akan dipanggil untuk menambahkan Pelanggan ke Penerbit ini menggunakan berlangganan (_ :).
Sebagai contoh, kami menerapkan Publisher, yang akan menghasilkan angka Fibonacci untuk kami.
struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never }
Karena urutannya terdiri dari angka, tipe output akan menjadi Output dari tipe Int, dan Kegagalan akan menjadi tipe khusus dari Never, menunjukkan bahwa Penerbit ini tidak akan pernah gagal.
Untuk fleksibilitas, kami menentukan batas jumlah elemen yang ingin kami terima dan bungkus nilai ini di beberapa objek konfigurasi Penerbit kami.
struct FibonacciConfiguration { var count: UInt }
Mari kita melihat lebih dekat pada kode ini, var count: UInt terlihat seperti opsi yang baik, tetapi penggunaannya membatasi kita pada kisaran nilai yang valid dari tipe UInt dan juga tidak sepenuhnya jelas apa yang harus ditunjukkan jika kita masih ingin memiliki urutan tanpa batas.
Alih-alih UInt, kami akan menggunakan tipe Subscribers.Demand, yang didefinisikan dalam Combine, di mana juga digambarkan sebagai tipe yang dikirim dari Pelanggan ke Publisher melalui Berlangganan. Secara sederhana, ini menunjukkan kebutuhan akan elemen, berapa banyak elemen yang diminta oleh Pelanggan. tidak terbatas - tidak terbatas, tidak ada - tidak sama sekali, maks (N) - tidak lebih dari N kali.
public struct Demand : Equatable, Comparable, Hashable, Codable, CustomStringConvertible { public static let unlimited: Subscribers.Demand public static let none: Subscribers.Demand
Kami menulis ulang FibonacciConfiguration yang mengubah jenis menjadi yang baru untuk perhitungan.
struct FibonacciConfiguration { var count: Subscribers.Demand }
Mari kita kembali ke Publisher dan mengimplementasikan metode accept (_: Subscriber), seperti yang kita ingat, metode ini diperlukan untuk menambahkan Subscriber ke Publisher. Dan dia melakukan ini dengan Langganan, Penerbit harus membuat langganan dan mentransfer langganan ke pelanggan.
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) }
Ini adalah fungsi umum yang menggunakan Pelanggan sebagai parameter, dan nilai output Penerbit harus cocok dengan nilai input Pelanggan (Output == S.Input), sama untuk kesalahan. Ini diperlukan untuk "menghubungkan" Publisher'a dan Subscriber'a.
Dalam fungsinya sendiri, buat langganan FibonacciSubscription, di konstruktor kami mentransfer pelanggan dan konfigurasi. Setelah itu, berlangganan ditransfer ke pelanggan.
Penerbit kami siap, pada akhirnya kami memiliki:
struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } }
Seperti yang Anda lihat, Penerbit itu sendiri tidak mengandung logika untuk menghasilkan urutan Fibonacci, semua logika akan berada di kelas berlangganan - FibonacciSubscription.
Seperti yang sudah Anda tebak, kelas FibonacciSubscription akan mengikuti protokol Berlangganan, mari kita lihat definisi protokol ini.
public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible { func request(_ demand: Subscribers.Demand) }
Fungsi request (_: Subscribers.Demand) memberi tahu Publisher bahwa ia dapat mengirim lebih banyak nilai kepada pelanggan. Dalam metode inilah logika pengiriman angka Fibonacci akan terjadi.
Kita juga perlu menerapkan mengikuti protokol Cancellable dan mengimplementasikan fungsi cancel ().
public protocol Cancellable { func cancel() }
Dan cukup ikuti protokol CustomCombineIdentifierConvertible dan tentukan variabel readI only.
public protocol CustomCombineIdentifierConvertible { var combineIdentifier: CombineIdentifier { get } }
Ada klarifikasi di sini, jika Anda menggulir tepat di bawah definisi protokol CustomCombineIdentifierConvertible di Combine, Anda dapat melihat bahwa Combine memberikan ekstensi untuk protokol ini, yang memiliki bentuk -
extension CustomCombineIdentifierConvertible where Self : AnyObject { public var combineIdentifier: CombineIdentifier { get } }
Yang memberitahu kita bahwa definisi dari variabel joinIdentifier: CombineIdentifier disediakan secara default jika tipe yang mengikuti protokol ini juga mengikuti protokol AnyObject, yaitu jika tipe ini adalah kelas. FibonacciSubscription adalah kelas, jadi kami mendapatkan definisi variabel default.
Berlangganan
Maka kami akan mulai menerapkan FibonacciSubscription kami.
private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } ... }
Seperti yang Anda lihat, FibonacciConfiguration berisi tautan yang kuat ke Pelanggan, dengan kata lain, adalah pemilik Pelanggan. Ini adalah poin penting, langganan bertanggung jawab atas retensi pelanggan, dan itu harus menahannya sampai selesai bekerja, selesai dengan kesalahan atau dengan dibatalkan.
Selanjutnya, kami menerapkan metode cancel () dari protokol Cancellable.
func cancel() { subscriber = nil }
Menyetel pelanggan menjadi nol membuatnya tidak dapat diakses untuk berlangganan.
Sekarang kita siap untuk memulai implementasi pengiriman nomor Fibonacci.
kami menerapkan metode permintaan (_: Pelanggan. Permintaan).
func request(_ demand: Subscribers.Demand) {
1) Dari awal, kami memeriksa berapa banyak elemen yang dapat diberikan penerbit, jika tidak sama sekali, maka selesaikan pengiriman dan kirim sinyal kepada Pelanggan bahwa pengiriman angka telah selesai.
2) Jika ada kebutuhan, maka kurangi jumlah angka yang diminta dengan satu, kirim elemen pertama dari deret Fibonacci ke Pelanggan, yaitu 0 dan kemudian periksa lagi berapa banyak elemen yang dapat diberikan oleh Penerbit kepada kami, jika tidak, kirim sinyal ke Pelanggan untuk menyelesaikan .
3) Pendekatan yang sama seperti pada 2) paragraf, tetapi hanya untuk elemen kedua dalam urutan Fibonacci.
4) Jika diperlukan lebih dari 2 elemen, maka kami menerapkan algoritma berulang untuk menemukan angka Fibonacci, di mana pada setiap langkah kami akan mentransfer angka berikutnya dari urutan Fibonacci ke Pelanggan dan juga memeriksa berapa banyak elemen yang masih dapat disediakan Penerbit. Jika Penerbit tidak lagi memberikan nomor baru, kirimkan sinyal penyelesaian kepada Pelanggan.
Saat ini, kami telah menulis kode seperti itu struct FibonacciConfiguration { var count: Subscribers.Demand } struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) {
Tes pertama
Sekarang kami akan menguji apa yang kami miliki, kami memiliki Penerbit dan Langganan kami, kami tidak memiliki cukup pelanggan, Combine menyediakan 2 pelanggan dari kotak: sink dan assign.
- sink - metode ini membuat pelanggan dan segera meminta jumlah nilai yang tidak terbatas .
- assign - set setiap elemen dari Publisher ke properti objek.
wastafel sangat cocok untuk tujuan kita, perhatian khusus harus diberikan pada kenyataan bahwa ia meminta jumlah nilai yang tidak terbatas.
Dan di sini kita perlu membuat perbedaan penting, Penerbit kita dalam variabel jumlah menentukan jumlah elemen yang dapat diberikan Penerbit kita dan ketentuan ini ditentukan oleh kita sendiri. Pada prinsipnya, kita bisa melakukannya tanpa variabel ini dan tidak dibatasi dalam mentransmisikan angka Fibonacci, tetapi tidak lama lagi kita akan melampaui kisaran nilai yang dapat diterima dari tipe Int.
Kasus dengan sink berbeda, setiap Pelanggan menentukan berapa banyak nilai yang ingin diterima, sink meminta jumlah nilai yang tidak terbatas, yang berarti bahwa ia akan menerima nilai sampai menerima sinyal penyelesaian, kesalahan atau pembatalan.
Untuk kenyamanan menggunakan Penerbit kami, kami menambahkan pembuatannya ke ekstensi protokol Penerbit.
extension Publishers { private static func fibonacci(configuration: FibonacciConfiguration) -> FibonacciPublisher { FibonacciPublisher(configuration: configuration) } static func fibonacci(count: Subscribers.Demand = .max(6)) -> FibonacciPublisher { FibonacciPublisher(configuration: FibonacciConfiguration(count: count)) } }
Maka cobalah Penerbit kami
Publishers.fibonacci(count: .max(10)) .sink { value in print(value, terminator: " ") }
Dan sekarang batas kasus
Publishers.fibonacci(count: .max(1)) .sink { value in print(value, terminator: " ") }
Tetapi apa yang terjadi jika Anda menentukan .unlimited?
Publishers.fibonacci(count: .unlimited) .print() .sink { value in print(value, terminator: " ") }
Bagaimana Anda bisa menggunakan .unlimited, tetapi bisa menampilkan beberapa angka? Untuk melakukan ini, kita memerlukan operator .prefix (_), yang bekerja dengan cara yang sama dengan .prefix (_) dari koleksi, yaitu, ia hanya menyisakan elemen N pertama.
Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in }
Apa masalahnya? Mungkin di .prefix (_)? Mari kita lakukan sedikit percobaan pada urutan standar dari Foundation.
Seperti yang dapat kita lihat, kode di atas berfungsi dengan benar, maka masalahnya ada pada implementasi Penerbit kita.
Kami melihat log dari .print () dan melihat bahwa setelah N meminta, dari .prefix (_), kami memanggil cancel () pada FibonacciSubscription kami, tempat kami mengatur pelanggan menjadi nihil.
func cancel() { subscriber = nil }
Jika Anda membuka tumpukan panggilan, Anda dapat melihat bahwa membatalkan () dipanggil dari permintaan (_ :), yaitu selama panggilan ke pelanggan?. Menerima (_). Dari mana kita dapat menyimpulkan bahwa pada suatu saat di dalam permintaan (_ :) pelanggan dapat menjadi nol dan kemudian kita perlu menghentikan pekerjaan menghasilkan nomor baru. Tambahkan kondisi ini ke kode kami.
func request(_ demand: Subscribers.Demand) {
Sekarang jalankan kode pengujian kami.
Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in }
Mendapat perilaku yang diharapkan.
Pelanggan
Dan apakah FibonacciSubscription kami sudah siap? Tidak juga, dalam pengujian kami, kami hanya menggunakan pelanggan wastafel yang meminta jumlah angka yang tidak terbatas, tetapi bagaimana jika kami menggunakan pelanggan yang akan mengharapkan jumlah angka tertentu. Combine tidak menyediakan pelanggan seperti itu, tetapi apa yang menghalangi kami untuk menulis sendiri? Di bawah ini adalah implementasi dari FibonacciSubscriber kami.
class FibonacciSubscriber: Subscriber { typealias Input = Int typealias Failure = Never var limit: Subscribers.Demand init(limit: Subscribers.Demand) { self.limit = limit } func receive(subscription: Subscription) { subscription.request(limit) } func receive(_ input: Input) -> Subscribers.Demand { .none } func receive(completion: Subscribers.Completion<Failure>) { print("Subscriber's completion: \(completion)") } }
Dan FibonacciSubscriber kami memiliki properti batas, yang menentukan berapa banyak elemen yang ingin diterima oleh Pelanggan ini. Dan ini dilakukan dalam metode terima (_: Berlangganan), di mana kami memberi tahu langganan berapa banyak elemen yang kami butuhkan. Penting juga untuk mencatat fungsi terima (_: Input) -> Pelanggan. Permintaan, fungsi ini dipanggil ketika nilai baru diterima, sebagai nilai balik kami menunjukkan berapa banyak elemen tambahan yang ingin kami terima:. Tidak ada - tidak sama sekali, .max (N) N , secara total, jumlah total elemen yang diterima akan sama dengan jumlah nilai langganan yang dikirim dalam penerimaan (_: Berlangganan) dan semua nilai pengembalian dari menerima (_: Input) -> Pelanggan. Permintaan.
Tes kedua
Mari kita coba menggunakan FibonacciSubscriber.
let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber)
Seperti yang kita lihat Penerbit kita mengirim 5 nilai, bukan 3. Mengapa begitu? Karena metode request (_: Subscribers.Demand) dari FibonacciSubscription'a tidak memperhitungkan kebutuhan pelanggan, mari perbaiki, untuk ini kami akan menambahkan properti tambahan yang diminta, di mana kami akan melacak kebutuhan pelanggan.
private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand var requested: Subscribers.Demand = .none
Pengujian ketiga
let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber)
Penerbit sekarang berfungsi dengan benar.
Kode akhir import Foundation import Combine struct FibonacciConfiguration { var count: Subscribers.Demand } struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand var requested: Subscribers.Demand = .none init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) { guard count > .none else { subscriber?.receive(completion: .finished) return } requested += demand count -= .max(1) requested -= .max(1) requested += subscriber?.receive(0) ?? .none guard let _ = subscriber, requested > .none else { return } if count == .none { subscriber?.receive(completion: .finished) return } count -= .max(1) requested -= .max(1) requested += subscriber?.receive(1) ?? .none guard let _ = subscriber, requested > .none else { return } if count == .none { subscriber?.receive(completion: .finished) return } var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber, requested > .none { temp = prev prev = current current += temp requested += subscriber.receive(current) count -= .max(1) requested -= .max(1) if count == .none { subscriber.receive(completion: .finished) return } } } } extension Publishers { private static func fibonacci(configuration: FibonacciConfiguration) -> FibonacciPublisher { FibonacciPublisher(configuration: configuration) } static func fibonacci(count: Subscribers.Demand = .max(6)) -> FibonacciPublisher { FibonacciPublisher(configuration: FibonacciConfiguration(count: count)) } } class FibonacciSubscriber: Subscriber { typealias Input = Int typealias Failure = Never var limit: Subscribers.Demand init(limit: Subscribers.Demand) { self.limit = limit } func receive(subscription: Subscription) { subscription.request(limit) } func receive(_ input: Input) -> Subscribers.Demand { .none } func receive(completion: Subscribers.Completion<Failure>) { print("Subscriber's completion: \(completion)") } } Publishers.fibonacci(count: .max(4)) .print() .sink { _ in } let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber)
Hasil
Saya harap artikel ini memberi Anda lebih banyak pemahaman tentang apa itu Penerbit, Berlangganan dan Pelanggan, bagaimana mereka berinteraksi satu sama lain, dan poin apa yang perlu Anda perhatikan ketika Anda memutuskan untuk mengimplementasikan Penerbit Anda. Ada komentar, klarifikasi untuk artikel ini.