
Kotlin masih merupakan bahasa yang masih muda, tetapi telah memasuki kehidupan kita. Karena itu, tidak selalu jelas bagaimana menerapkan dengan benar ini atau itu fungsional dan praktik terbaik mana yang diterapkan.
Yang paling sulit adalah kasus dengan fitur-fitur bahasa yang tidak ada di Jawa. Salah satu batu sandungan ini adalah
ekspansi .
Ini adalah alat yang nyaman yang membuat kode lebih mudah dibaca, hampir tidak memerlukan imbalan apa pun. Tetapi pada saat yang sama, saya tahu setidaknya satu orang yang, jika dia tidak menganggap ekspansi sebagai kejahatan, pasti skeptis terhadap mereka. Di bawah ini saya ingin membahas fitur-fitur mekanisme ini, yang dapat menyebabkan kontroversi dan kesalahpahaman.
Ekstensi ke DTO - pelanggaran templat Obyek Transfer Data
Misalnya, ada Pengguna kelas
class User(val name: String, val age: Int, val sex: String)
Cukup DTO! Lebih lanjut, dalam kode di beberapa tempat, diperlukan pemeriksaan untuk menentukan apakah pengguna tersebut orang dewasa. Pilihan termudah adalah membuat kondisi di semua tempat
if (user.age >= 18) { ... }
Tetapi, karena ada banyak tempat yang sewenang-wenang seperti itu, masuk akal untuk memasukkan cek ini ke dalam metode.
Ada tiga opsi di sini:
- Function fun isAdult (pengguna: Pengguna) - kelas utilitas biasanya terdiri dari fungsi-fungsi tersebut.
- Masukkan fungsi isAdult ke dalam kelas Pengguna
class User(val name: String, val age: Int, val sex: String) { fun isAdult() = age >= 18 }
- Tulis pembungkus untuk Pengguna yang akan berisi fungsi serupa.
Ketiga opsi secara teknis berhak untuk hidup. Tetapi yang pertama menambah ketidaknyamanan dalam bentuk kebutuhan untuk mengetahui semua fungsi utilitas, meskipun ini, tentu saja, bukan masalah besar.
Opsi kedua tampaknya melanggar pola Objek Transfer Data, karena kelas tidak hanya getter dan setter. Namun, merusak pola itu buruk.
Opsi ketiga tidak melanggar prinsip-prinsip OOP atau templat, tetapi setiap kali Anda harus membuat pembungkus jika Anda ingin menggunakan fungsi yang sama. Opsi ini juga tidak terlalu suka. Pada akhirnya, ternyata Anda masih harus berkorban.
Menurut pendapat saya, lebih mudah untuk mengorbankan template DTO. Pertama, saya tidak menemukan penjelasan tunggal tentang mengapa fungsi (kecuali getter dan setter) tidak dapat dibuat di DTO. Dan kedua, hanya dari segi makna, kode seperti itu nyaman untuk dimiliki di sebelah data yang kami operasikan.
Tetapi tidak selalu memungkinkan untuk memasukkan kode seperti itu di dalam DTO shek, karena pengembang tidak selalu memiliki kemampuan untuk mengedit kelas yang digunakannya. Sebagai contoh, ini mungkin kelas yang dihasilkan dari xsd. Selain itu, bagi seseorang mungkin tidak biasa dan tidak nyaman untuk menulis kode seperti itu di kelas Data. Kotlin menawarkan solusi untuk situasi seperti itu dalam bentuk fungsi dan bidang ekstensi:
fun User.isAdult() = age >= 18
Kode ini dapat digunakan seolah-olah dinyatakan di dalam kelas Pengguna:
if(user.isAdult()) {...}
Hasilnya adalah solusi yang cukup akurat yang, dengan sedikit kompromi, memuaskan kebutuhan kita. Jika kita berbicara tentang fakta bahwa template DTO dilanggar, maka kita ingin mengingat bahwa di Jawa itu akan menjadi metode statis biasa dari form:
public static final boolean isAdult(@NotNull User receiver)
Seperti yang Anda lihat, secara formal, bahkan templat tidak dilanggar. Menggunakan fungsi ini tampak seolah-olah dideklarasikan di User dan Idea akan menawarkannya secara otomatis. Sangat nyaman.
Ekstensi spesifik. Anda tidak bisa tahu tentang keberadaan mereka dan membingungkan metode dan bidang entitas dengan ekstensi
Idenya adalah bahwa pengembang datang ke proyek, dan sekitar kode yang diterapkan dalam ekstensi, tidak jelas metode mana yang asli dan mana yang merupakan metode ekstensi.
Ini bukan masalah, karena Ide membantu pengembang dalam masalah ini dan menyoroti fungsi-fungsi tersebut. Meskipun secara adil harus dikatakan bahwa perbedaannya lebih terlihat dalam tema Darcula. Jika Anda mengubahnya menjadi Cahaya, semuanya menjadi kurang jelas dan ekstensi hanya berbeda dalam font miring.
Di bawah ini kita melihat contoh memanggil dua metode: isAdult adalah metode ekstensi, isMale adalah metode biasa di dalam kelas Pengguna. Tangkapan layar di sebelah kiri adalah tema Darcula, di sebelah kanan adalah tema Cahaya yang biasa.


Agak lebih buruk dengan ladang. Jika, misalnya, kami memutuskan untuk mengimplementasikan isAdult sebagai bidang ekstensi, maka kami hanya dapat membedakannya dari bidang biasa dengan jenis font. Dalam contoh ini, nama adalah bidang biasa. Bidang ekstensi hanya menghasilkan font miring.


Lingkungan pengembangan Ide membantu Anda menentukan metode mana yang merupakan ekstensi dan mana yang asli saat pelengkapan otomatis. Ini nyaman.

Situasinya mirip dengan bidang.

"Untuk Pengguna di <root>" berarti ekstensi.
Plus, fakta bahwa Ide “mengikat” ekstensi ke entitas yang dapat diperluas sangat membantu dalam pengembangan, karena metode dan bidang ekstensi diusulkan untuk pelengkapan otomatis.
Ekstensi tersebar di seluruh proyek, membentuk tong sampah
Kami tidak memiliki masalah seperti itu pada proyek, karena kami tidak secara sewenang-wenang menempatkan ekstensi dan mengeluarkan kode dengan ekstensi publik di file atau paket terpisah.
Misalnya, fungsi isAdult dari contoh di atas dapat muncul di file Pengguna paket ekstensi. Jika paket tidak cukup dan Anda hanya ingin tidak bingung di mana kelasnya dan di mana file fungsinya, Anda dapat menamainya, misalnya, _User.kt. Begitu pula para pengembang di JetBrains untuk koleksi. Atau, jika hati nurani melarang memulai file dengan garis bawah, Anda dapat menghubungi user.kt. Faktanya, tidak ada perbedaan cara penggunaan, yang utama adalah bahwa ada keseragaman yang dipatuhi oleh seluruh tim.
Pembuat bahasa, ketika mengembangkan metode ekstensi untuk koleksi, menempatkannya dalam file
_Collections.kt .
Ini umumnya masalah mengatur kode, bukan masalah ekstensi. Fungsi statis di Java, dan tidak hanya yang statis, dapat tersebar tidak kurang dari ekstensi secara acak.
Jangan mengabaikan fungsi ekstensi selama pengujian unit
Menurut pendapat saya, tidak perlu membasahi fungsi ekstensi, sama seperti tidak perlu membasahi metode statis. Dalam fungsi ekspansi, Anda harus meletakkan logika bekerja dengan data yang ada. Sebagai contoh, dalam kasus fungsi isAdult untuk kelas Pengguna, semua yang Anda butuhkan ada di isAdult. Anda tidak perlu basah.
Pertimbangkan contoh yang sedikit lebih rumit. Ada komponen tertentu yang berfungsi untuk mendapatkan pengguna dari sistem eksternal - UserComponent. Metode untuk mendapatkan pengguna disebut getUsers. Misalkan ada kebutuhan untuk mendapatkan semua pengguna aktif dan memutuskan untuk menambahkan logika penyaringan dalam bentuk fungsi - ekstensi. Akibatnya, kami mendapat fungsi:
fun UserComponent.getActiveUsers(): List<Users> = this.getUsers().filter{it.status == “Active”}
Sepertinya ini dia - situasi ketika Anda membutuhkan tiruan untuk ekspansi. Tetapi jika Anda ingat bahwa getActiveUsers hanyalah metode statis, ternyata tiruan itu tidak diperlukan. Dip haruslah metode dan fungsi yang disebut dalam ekstensi, dan tidak lebih.
Ada kemungkinan bahwa fungsi ekstensi tumpang tindih dengan fungsi dari nama yang sama yang terletak di dalam kelas extended
Kami akan mempertimbangkan kasus ini menggunakan contoh dari paragraf pertama. Misalkan ada ekstensi fungsi adalah Dewasa, yang memeriksa apakah pengguna dewasa:
fun User.isAdult() = age >= 18
Setelah itu, kami mengimplementasikan fungsi dengan nama yang sama di dalam Pengguna:
class User(val name: String, val age: Int, val sex: String){ fun isAdult() = age >= 21 }
Ketika user.isAdult () dipanggil, fungsi dari kelas akan dipanggil, meskipun ada ekstensi dengan nama yang sama dan fungsi yang sesuai. Kasus seperti itu dapat membingungkan, karena pengguna yang tidak mengetahui fungsi yang dideklarasikan di dalam kelas akan menunggu untuk fungsi ekstensi selesai. Ini adalah situasi yang tidak menyenangkan yang dapat memiliki konsekuensi yang sangat serius. Dalam hal ini, kami tidak berbicara tentang kemungkinan ketidaknyamanan ulasan atau pelanggaran templat, tetapi tentang perilaku kode yang berpotensi salah.
Situasi yang dijelaskan di atas menunjukkan bahwa ketika menggunakan fungsi ekstensi, masalah nyata dapat muncul.
Untuk menghindarinya, Anda jangan lupa untuk menutupi fungsi ekspansi dengan unit test sebanyak mungkin. Dalam kasus terburuk, jika tes gagal, akan ada dua fungsi yang bekerja dengan cara yang sama. Satu adalah perpanjangan, dan yang lainnya adalah di dalam kelas itu sendiri. Jika tes gagal, itu akan menarik perhatian pada fakta bahwa satu fungsi tumpang tindih lainnya.
Ekstensi terikat ke kelas, bukan objek, dan ini dapat menyebabkan kebingungan
Misalnya, pertimbangkan kelas Pengguna dari paragraf pertama. Mari kita buka dan ciptakan Siswa penggantinya:
class Student(name: String, age: Int, sex: String): User(name, age, sex)
Kami mendefinisikan fungsi ekstensi untuk Siswa, yang juga akan menentukan apakah siswa tersebut orang dewasa atau tidak. Hanya untuk siswa kami mengubah kondisi:
fun Student.isAdult() = this.age >= 16
Dan sekarang kita menulis kode berikut:
val user: User = Student("", 17, "M")
Apa yang akan mengembalikan user.isAdult ())?
Tampaknya objek bertipe Student dan fungsinya harus mengembalikan true. Tapi itu tidak sesederhana itu. Ekstensi dilampirkan ke kelas, bukan ke objek, dan hasilnya akan salah.
Tidak ada yang aneh dalam hal ini, jika kita ingat bahwa ekstensi adalah metode statis, dan entitas yang dapat dikembangkan adalah parameter pertama dalam metode ini. Ini adalah hal lain yang perlu diingat ketika menggunakan mekanisme ini. Jika tidak, Anda bisa mendapatkan efek yang tidak menyenangkan dan tidak terduga.
Alih-alih output
Poin-poin kontroversial ini tampaknya tidak berbahaya, jika Anda ingat bahwa kami mengatakan ekstensi - yang kami maksud adalah metode statis. Plus, mencakup fungsi tersebut dengan unit test akan membantu meminimalkan kemungkinan kebingungan yang terkait dengan sifat statis ekstensi.
Menurut pendapat saya, ekstensi adalah alat yang ampuh dan nyaman yang dapat meningkatkan kualitas dan keterbacaan kode, hampir tidak memerlukan imbalan apa pun. Itu sebabnya saya mencintai mereka:
- Ekstensi memungkinkan Anda untuk menulis logika khusus untuk konteks kelas yang dapat diperluas. Berkat ini, bidang dan metode ekstensi dibaca seolah-olah mereka selalu hadir dalam entitas yang diperluas, yang, pada gilirannya, meningkatkan pemahaman tingkat atas kode. Di Jawa, sayangnya, ini tidak bisa dilakukan. Selain itu, ekstensi memiliki pengubah akses yang sama dengan fungsi biasa. Ini memungkinkan Anda untuk menulis kode serupa dengan ruang lingkup yang benar-benar diperlukan untuk fungsi tertentu.
- Lebih mudah menggunakan fungsi ekstensi untuk pemetaan, yang harus Anda lihat cukup banyak saat menyelesaikan tugas sehari-hari. Sebagai contoh, dalam proyek ada kelas UserFromExternalSystem, yang digunakan saat memanggil sistem eksternal, dan akan lebih bagus untuk menempatkan pemetaan dalam fungsi ekstensi, lupakan dan gunakan seolah-olah itu aslinya di Pengguna.
callExternalSystem(user.getUserFromExternalSystem())
Tentu saja, hal yang sama dapat dilakukan dengan metode biasa, tetapi opsi ini kurang dapat dibaca:
callExternalSystem(getUserFromExternalSystem(user))
atau opsi semacam itu:
val externalUser = getUserFromExternalSystem(user) callExternalSystem(externalUser)
Sebenarnya, tidak ada keajaiban yang terjadi, tetapi berkat hal sepele seperti itu, lebih menyenangkan bekerja dengan kode.
- Dukungan gagasan dan pelengkapan otomatis. Tidak seperti metode dari kelas utilitas, ekstensi didukung dengan baik oleh lingkungan pengembangan. Dengan pelengkapan otomatis, ekstensi ditawarkan oleh lingkungan sebagai fungsi dan bidang "asli". Ini memungkinkan peningkatan produktivitas pengembang yang baik.
- Yang mendukung ekstensi adalah kenyataan bahwa sebagian besar perpustakaan Kotlin ditulis sebagai ekstensi. Banyak metode yang mudah dan favorit untuk bekerja dengan koleksi adalah ekstensi (Filter, Peta, dan sebagainya). Anda dapat memverifikasi ini dengan memeriksa file _Collections.kt .
Keuntungan ekstensi mencakup kemungkinan kerugian. Tentu saja, ada risiko besar penyalahgunaan mekanisme ini dan godaan untuk menjejalkan semua kode ke ekstensi. Namun di sini pertanyaannya lebih lanjut tentang pengorganisasian kode dan penggunaan alat yang kompeten. Ketika digunakan dengan benar, ekstensi akan menjadi teman sejati dan penolong dalam penulisan kode yang dibaca dan dikelola dengan baik.
Di bawah ini adalah tautan ke bahan-bahan yang digunakan untuk mempersiapkan artikel ini:
- proandroiddev.com/kotlin-extension-functions-more-than-sugar-1f04ca7189ff - dari sini diambil pemikiran menarik tentang fakta bahwa, menggunakan ekstensi, kami bekerja lebih dekat dengan konteksnya.
- www.nikialeksey.com/2017/11/14/kotlin-is-bad.html - di sini penulis menentang ekstensi dan memberikan contoh yang menarik, yang dibahas dalam salah satu poin di atas.
- medium.com/@elizarov/i-do-not-see-much-reason-to-mock-extension-func-7-7f24d88a188a - Pendapat Roman Elizarov tentang pembasahan metode ekstensi.
Saya juga ingin mengucapkan terima kasih kepada kolega yang membantu dengan kasus dan pemikiran menarik tentang materi ini.