
Halo semuanya! Nama saya Ilya, saya adalah pengembang iOS di Tinkoff.ru. Pada artikel ini saya ingin berbicara tentang cara mengurangi duplikasi kode di lapisan presentasi menggunakan protokol.
Apa masalahnya?
Ketika proyek tumbuh, jumlah duplikasi kode tumbuh. Ini tidak segera menjadi jelas, dan menjadi sulit untuk memperbaiki kesalahan masa lalu. Kami memperhatikan masalah ini pada proyek kami dan menyelesaikannya menggunakan satu pendekatan, sebut saja, kondisional, sifat.
Contoh hidup
Pendekatan ini dapat digunakan dengan berbagai solusi arsitektur yang berbeda, tetapi saya akan mempertimbangkannya menggunakan VIPER sebagai contoh.
Pertimbangkan metode yang paling umum di router - metode yang menutup layar:
func close() { self.transitionHandler.dismiss(animated: true, completion: nil) }
Ini hadir di banyak router, dan lebih baik menulisnya sekali saja.
Warisan akan membantu kita dalam hal ini, tetapi di masa depan, ketika kita memiliki semakin banyak kelas dengan metode yang tidak perlu dalam aplikasi kita, atau kita tidak akan dapat membuat kelas yang kita butuhkan karena metode yang diperlukan ada di kelas dasar yang berbeda, yang besar akan muncul masalah.
Akibatnya, proyek ini akan tumbuh menjadi banyak kelas dasar dan kelas turunan dengan metode berlebihan. Warisan tidak akan membantu kita.
Apa yang lebih baik dari warisan? Tentu komposisi.
Anda dapat membuat kelas terpisah untuk metode yang menutup layar dan menambahkannya ke setiap router yang membutuhkannya:
struct CloseRouter { let transitionHandler: UIViewController func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
Kita masih harus mendeklarasikan metode ini dalam protokol input router dan mengimplementasikannya di router itu sendiri:
protocol SomeRouterInput { func close() } class SomeRouter: SomeRouterInput { var transitionHandler: UIViewController! lazy var closeRouter = { CloseRouter(transitionHandler: self. transitionHandler) }() func close() { self.closeRouter.close() } }
Ternyata terlalu banyak kode yang hanya proksi panggilan ke metode tutup. Pemrogram
Malas yang Baik tidak akan menghargai.
Solusi Protokol
Protokol datang untuk menyelamatkan. Ini adalah alat yang cukup kuat yang memungkinkan Anda untuk mengimplementasikan komposisi dan mungkin berisi metode implementasi dalam ekstensi. Jadi kita bisa membuat protokol yang berisi metode tutup dan mengimplementasikannya dalam ekstensi.
Beginilah tampilannya:
protocol CloseRouterTrait { var transitionHandler: UIViewController! { get } func close() } extension CloseRouterTrait { func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
Pertanyaannya adalah, mengapa kata sifat muncul dalam nama protokol? Ini sederhana - Anda dapat menentukan bahwa protokol ini mengimplementasikan metodenya dalam ekstensi dan harus digunakan sebagai campuran ke tipe lain untuk memperluas fungsinya.
Sekarang, mari kita lihat bagaimana penggunaan protokol tersebut akan terlihat:
class SomeRouter: CloseRouterTrait { var transitionHandler: UIViewController! }
Ya, itu saja. Tampak hebat :). Kami mendapat komposisi dengan menambahkan protokol ke kelas router, tidak menulis satu baris tambahan dan mendapat kesempatan untuk menggunakan kembali kode.
Apa yang tidak biasa tentang pendekatan ini?
Anda mungkin sudah menanyakan pertanyaan ini. Menggunakan protokol sebagai suatu sifat cukup umum. Perbedaan utama adalah menggunakan pendekatan ini sebagai solusi arsitektur dalam lapisan presentasi. Seperti halnya solusi arsitektur, harus ada aturan dan rekomendasi mereka sendiri.
Ini daftar saya:
- Trait seharusnya tidak menyimpan dan mengubah status. Mereka hanya dapat memiliki dependensi dalam bentuk layanan, dll., Yang merupakan properti get-only.
- Sifat tidak boleh memiliki metode yang tidak diterapkan dalam ekstensi, karena ini melanggar konsep mereka
- Nama-nama metode dalam sifat harus secara eksplisit mencerminkan apa yang mereka lakukan, tanpa terikat dengan nama protokol. Ini akan membantu untuk menghindari tabrakan nama dan membuat kode lebih jelas.
Dari VIPER ke MVP
Jika Anda benar-benar beralih menggunakan pendekatan ini dengan protokol, maka kelas router dan interaksor akan terlihat seperti ini:
class SomeRouter: CloseRouterTrait, OtherRouterTrait { var transitionHandler: UIViewController! } class SomeInteractor: SomeInteractorTrait { var someService: SomeServiceInput! }
Ini tidak berlaku untuk semua kelas, dalam kebanyakan kasus, proyek hanya akan memiliki router dan interaksor kosong. Dalam hal ini, Anda dapat mengganggu struktur modul VIPER dan dengan lancar beralih ke MVP dengan menambahkan protokol pengotor ke presenter.
Sesuatu seperti ini:
class SomePresenter: CloseRouterTrait, OtherRouterTrait, SomeInteractorTrait, OtherInteractorTrait { var transitionHandler: UIViewController! var someService: SomeSericeInput! }
Ya, kemampuan untuk mengimplementasikan router dan interaksor sebagai dependensi hilang, tetapi dalam beberapa kasus ini adalah kasusnya.
Satu-satunya kelemahan adalah transitionHandler = UIViewController. Dan menurut aturan VIPER Presenter, tidak ada yang harus diketahui tentang layer View dan bagaimana itu diimplementasikan menggunakan teknologi apa. Ini diselesaikan dalam kasus ini hanya - metode transisi dari UIViewController "ditutup" oleh protokol, misalnya, TransitionHandler. Jadi Presenter akan berinteraksi dengan abstraksi.
Mengubah perilaku sifat
Mari kita lihat bagaimana Anda dapat mengubah perilaku dalam protokol semacam itu. Ini akan menjadi analog dari penggantian beberapa bagian modul, misalnya, untuk tes atau rintisan sementara.
Sebagai contoh, ambil interaksor sederhana dengan metode yang melakukan permintaan jaringan:
protocol SomeInteractorTrait { var someService: SomeServiceInput! { get } func performRequest(completion: @escaping (Response) -> Void) } extension SomeInteractorTrait { func performRequest(completion: @escaping (Response) -> Void) { someService.performRequest(completion) } }
Ini adalah kode abstrak, misalnya. Misalkan kita tidak perlu mengirim permintaan, tetapi hanya perlu mengembalikan semacam rintisan. Di sini kita pergi ke trik - buat protokol kosong bernama Mock dan lakukan hal berikut:
protocol Mock {} extension SomeInteractorTrait where Self: Mock { func performRequest(completion: @escaping (Response) -> Void) { completion(MockResponse()) } }
Di sini, implementasi metode performRequest telah diubah untuk tipe yang mengimplementasikan protokol Mock. Sekarang Anda perlu mengimplementasikan protokol Mock untuk kelas yang akan mengimplementasikan SomeInteractor:
class SomePresenter: SomeInteractorTrait, Mock { // Implementation }
Untuk kelas SomePresenter, implementasi metode performRequest akan dipanggil, terletak di ekstensi, di mana Self memenuhi protokol Mock. Ada baiknya menghapus protokol Mock dan implementasi metode performRequest akan diambil dari ekstensi biasa ke SomeInteractor.
Jika Anda menggunakan ini hanya untuk pengujian, lebih baik menempatkan semua kode yang terkait dengan substitusi implementasi dalam target pengujian.
Untuk meringkas
Sebagai kesimpulan, perlu dicatat pro dan kontra dari pendekatan ini dan dalam kasus mana, menurut pendapat saya, itu layak digunakan.
Mari kita mulai dengan kontra:
- Jika Anda menyingkirkan perute dan interaksor, seperti yang ditunjukkan dalam contoh, maka kemampuan untuk mengimplementasikan dependensi ini hilang.
- Kekurangan lainnya adalah meningkatnya jumlah protokol.
- Terkadang kode mungkin tidak terlihat sejelas menggunakan pendekatan konvensional.
Aspek positif dari pendekatan ini adalah sebagai berikut:
- Yang paling penting dan jelas, duplikasi sangat berkurang.
- Ikatan statis diterapkan pada metode protokol. Ini berarti bahwa penentuan implementasi metode akan terjadi pada tahap kompilasi. Oleh karena itu, selama pelaksanaan program, waktu tambahan tidak akan dihabiskan untuk mencari implementasi (meskipun waktu ini tidak terlalu signifikan).
- Karena protokolnya adalah "batu bata" kecil, komposisi apa pun dapat dengan mudah dibuat darinya. Ditambah dalam karma untuk fleksibilitas dalam penggunaan.
- Kemudahan refactoring, tidak ada komentar di sini.
- Anda dapat mulai menggunakan pendekatan ini di setiap tahap proyek, karena itu tidak mempengaruhi keseluruhan proyek.
Mempertimbangkan keputusan ini baik atau tidak adalah masalah pribadi bagi semua orang. Pengalaman kami dengan pendekatan ini adalah masalah positif dan terselesaikan.
Itu saja!