Perusahaan kami telah menggunakan Kotlin dalam produksi selama lebih dari dua tahun. Secara pribadi, saya menemukan bahasa ini sekitar setahun yang lalu. Ada banyak topik untuk dibicarakan, tetapi hari ini kita akan berbicara tentang penanganan kesalahan, termasuk dalam gaya fungsional. Saya akan memberitahu Anda bagaimana melakukan ini di Kotlin.
(Foto dari pertemuan tentang topik ini, yang terjadi di kantor salah satu perusahaan Taganrog. Alexey Shafranov, pemimpin kelompok kerja (Jawa) di Maxilekt, berbicara)Bagaimana Anda bisa menangani kesalahan pada prinsipnya?
Saya menemukan beberapa cara:
- Anda dapat menggunakan beberapa nilai balik sebagai penunjuk ke fakta bahwa ada kesalahan;
- Anda dapat menggunakan parameter indikator untuk tujuan yang sama,
- masukkan variabel global
- menangani pengecualian
- Tambahkan Kontrak (DbC) .
Mari kita membahas lebih rinci tentang masing-masing opsi.
Nilai pengembalian
Nilai "ajaib" tertentu dikembalikan jika terjadi kesalahan. Jika Anda pernah menggunakan bahasa skrip, Anda harus melihat konstruksi yang serupa.
Contoh 1:
function sqrt(x) { if(x < 0) return -1; else return โx; }
Contoh 2:
function getUser(id) { result = db.getUserById(id) if (result) return result as User else return โCan't find user โ + id }
Parameter indikator
Parameter tertentu yang diteruskan ke fungsi digunakan. Setelah mengembalikan nilai dengan parameter, Anda dapat melihat apakah ada kesalahan di dalam fungsi.
Contoh:
function divide(x,y,out Success) { if (y == 0) Success = false else Success = true return x/y } divide(10, 11, Success) id (!Success) //handle error
Variabel global
Variabel global bekerja dengan cara yang kira-kira sama.
Contoh:
global Success = true function divide(x,y) { if (y == 0) Success = false else return x/y } divide(10, 11, Success) id (!Success) //handle error
Pengecualian
Kita semua terbiasa dengan pengecualian. Mereka digunakan hampir di mana-mana.
Contoh:
function divide(x,y) { if (y == 0) throw Exception() else return x/y } try{ divide(10, 0)} catch (e) {//handle exception}
Kontrak (DbC)
Terus terang, saya belum pernah melihat pendekatan ini secara langsung. Dengan googling lama, saya menemukan bahwa Kotlin 1.3 memiliki perpustakaan yang benar-benar memungkinkan penggunaan kontrak. Yaitu Anda dapat mengatur kondisi pada variabel yang diteruskan ke fungsi, kondisi pada nilai balik, jumlah panggilan, dari mana panggilan itu berasal, dll. Dan jika semua kondisi terpenuhi, diyakini bahwa fungsi tersebut bekerja dengan benar.
Contoh:
function sqrt (x) pre-condition (x >= 0) post-condition (return >= 0) begin calculate sqrt from x end
Jujur, perpustakaan ini memiliki sintaks yang mengerikan. Mungkin itu sebabnya saya belum melihat hal seperti itu secara langsung.
Pengecualian di Jawa
Mari kita beralih ke Jawa dan bagaimana semuanya bekerja dari awal.

Saat mendesain bahasa, ada dua jenis pengecualian:
- diperiksa - diperiksa;
- tidak dicentang - tidak dicentang.
Untuk apa pengecualian diperiksa? Secara teoritis, mereka diperlukan sehingga orang harus memeriksa kesalahan. Yaitu jika pengecualian yang diperiksa tertentu dimungkinkan, maka harus diperiksa nanti. Secara teoritis, pendekatan ini seharusnya mengarah pada tidak adanya kesalahan yang tidak diproses dan kualitas kode yang ditingkatkan. Tetapi dalam praktiknya tidak demikian. Saya pikir semua orang setidaknya satu kali dalam hidup mereka melihat blok penangkap kosong.
Mengapa ini bisa buruk?
Ini adalah contoh klasik langsung dari dokumentasi Kotlin - antarmuka dari JDK yang diimplementasikan dalam StringBuilder:
Appendable append(CharSequence csq) throws IOException; try { log.append(message) } catch (IOException e) { //Must be safe }
Saya yakin Anda telah menemukan cukup banyak kode yang dibungkus dengan try-catch, di mana catch adalah blok kosong, karena situasi seperti itu seharusnya tidak terjadi, menurut pengembang. Dalam banyak kasus, penanganan pengecualian yang diperiksa dilaksanakan dengan cara berikut: mereka hanya membuang RuntimeException dan menangkapnya di suatu tempat di atas (atau tidak menangkapnya ...).
try { // do something } catch (IOException e) { throw new RuntimeException(e); // - ...
Apa yang mungkin terjadi di Kotlin
Dalam hal pengecualian, kompiler Kotlin berbeda dalam hal:
1. Tidak membedakan antara pengecualian yang diperiksa dan yang tidak dicentang. Semua pengecualian hanya tidak dicentang, dan Anda memutuskan sendiri apakah akan menangkap dan memprosesnya.
2. Coba dapat digunakan sebagai ekspresi - Anda dapat menjalankan blok try dan mengembalikan baris terakhir dari itu, atau mengembalikan baris terakhir dari catch catch.
val value = try {Integer.parseInt(โlolโ)} catch(e: NumberFormanException) { 4 } //
3. Anda juga dapat menggunakan konstruksi serupa ketika merujuk ke beberapa objek, yang mungkin dapat dibatalkan:
val s = obj.money ?: throw IllegalArgumentException(โ , โ)
Kompatibilitas Java
Kode Kotlin dapat digunakan di Jawa dan sebaliknya. Bagaimana cara menangani pengecualian?
- Pengecualian yang diperiksa dari Jawa di Kotlin tidak dapat dicek atau dinyatakan (karena tidak ada pengecualian yang dicek di Kotlin).
- Kemungkinan pengecualian yang diperiksa dari Kotlin (misalnya, yang berasal dari Jawa) tidak perlu diperiksa di Jawa.
- Jika perlu untuk memeriksa, pengecualian dapat dibuat diverifikasi menggunakan penjelasan @Throws dalam metode (perlu untuk menunjukkan pengecualian yang dapat dilemparkan metode ini). Anotasi di atas hanya untuk kompatibilitas Java. Namun dalam praktiknya, banyak orang menggunakannya untuk menyatakan bahwa metode semacam itu, pada prinsipnya, dapat menimbulkan semacam pengecualian.
Alternatif untuk coba-tangkap blok
Blok try-catch memiliki kelemahan signifikan. Ketika itu muncul, bagian dari logika bisnis ditransfer ke dalam tangkapan, dan ini bisa terjadi di salah satu dari banyak metode di atas. Saat logika bisnis tersebar di blok atau seluruh rantai panggilan, akan lebih sulit untuk memahami cara kerja aplikasi. Dan blok keterbacaan sendiri tidak menambahkan kode.
try { HttpService.SendNotification(endpointUrl); MarkNotificationAsSent(); } catch (e: UnableToConnectToServerException) { MarkNotificationAsNotSent(); }
Apa alternatifnya?
Satu opsi menawarkan kita pendekatan fungsional untuk penanganan pengecualian. Implementasi serupa terlihat seperti ini:
val result: Try<Result> = Try{HttpService.SendNotification(endpointUrl)} when(result) { is Success -> MarkNotificationAsSent() is Failure -> MarkNotificationAsNotSent() }
Kami memiliki kesempatan untuk menggunakan Try monad. Intinya, ini adalah wadah yang menyimpan beberapa nilai. flatMap adalah metode bekerja dengan wadah ini, yang, bersama dengan nilai saat ini, dapat mengambil fungsi dan, sekali lagi, mengembalikan monad.
Dalam hal ini, panggilan dibungkus dengan Try monad (kami mengembalikan Try). Itu dapat diproses di satu tempat - di mana kita membutuhkannya. Jika output memiliki nilai, kami melakukan tindakan berikut dengan itu, jika pengecualian dilemparkan, kami memprosesnya di akhir rantai.
Penanganan Pengecualian Fungsional
Di mana saya bisa mencoba?
Pertama, ada beberapa implementasi komunitas dari kelas Try and Either. Anda dapat mengambilnya atau bahkan menulis implementasi sendiri. Dalam salah satu proyek "pertempuran", kami menggunakan implementasi Try yang dibuat sendiri - kami berhasil dengan satu kelas dan melakukan pekerjaan yang sangat baik.
Kedua, ada perpustakaan Arrow, yang pada prinsipnya menambahkan banyak fungsi ke Kotlin. Secara alami, ada Try and Either.
Selain itu, kelas Hasil muncul di Kotlin 1.3, yang akan saya bahas lebih rinci nanti.
Coba gunakan perpustakaan Arrow sebagai contoh
Perpustakaan Arrow memberi kita kelas Try. Bahkan, itu bisa di dua negara: Sukses atau Gagal:
- Keberhasilan penarikan yang berhasil akan mempertahankan nilai kami,
- Kegagalan menyimpan pengecualian yang terjadi selama eksekusi blok kode.
Panggilannya adalah sebagai berikut. Secara alami, ini dibungkus dengan try-catch reguler, tetapi ini akan terjadi di suatu tempat di dalam kode kita.
sealed class Try<out A> { data class Success<out A>(val value: A) : Try<A>() data class Failure(val e: Throwable) : Try<Nothing>() companion object { operator fun <A> invoke(body: () -> A): Try<A> { return try { Success(body()) } catch (e: Exception) { Failure(e) } } }
Kelas yang sama harus menerapkan metode flatMap, yang memungkinkan Anda untuk melewati suatu fungsi dan mengembalikan try monad kami:
inline fun <B> map(f: (A) -> B): Try<B> = flatMap { Success(f(it)) } inline fun <B> flatMap(f: (A) -> TryOf<B>): Try<B> = when (this) { is Failure -> this is Success -> f(value) }
Untuk apa ini? Agar tidak memproses kesalahan untuk setiap hasil ketika kami memiliki beberapa dari mereka. Misalnya, kami mendapat beberapa nilai dari berbagai layanan dan ingin menggabungkannya. Sebenarnya, kita dapat memiliki dua situasi: apakah kita berhasil menerima dan menggabungkannya, atau sesuatu jatuh. Karena itu, kita dapat melakukan hal berikut:
val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Success(value=15)
Jika kedua panggilan berhasil dan kami mendapat nilai, kami menjalankan fungsinya. Jika mereka tidak berhasil, Kegagalan akan kembali dengan pengecualian.
Inilah yang terlihat jika sesuatu jatuh:
val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { throw RuntimeException(โOh no!โ) } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no!
Kami menggunakan fungsi yang sama, tetapi hasilnya adalah Kegagalan dari RuntimeException.
Juga, perpustakaan Arrow memungkinkan Anda untuk menggunakan konstruksi yang sebenarnya gula sintaksis, khususnya yang mengikat. Semua hal yang sama dapat ditulis ulang melalui flatMap serial, tetapi mengikat memungkinkan Anda untuk membuatnya dapat dibaca.
val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val result3: Try<Int> = Try { throw RuntimeException(โOh no, again!โ) } val sum = binding { val (one) = result1 val (two) = result2 val (three) = result3 one + two + three } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no, again!
Mengingat bahwa salah satu hasil telah jatuh, kami mendapatkan kesalahan pada output.
Monad serupa dapat digunakan untuk panggilan tidak sinkron. Misalnya, berikut adalah dua fungsi yang berjalan secara tidak sinkron. Kami menggabungkan hasil mereka dengan cara yang sama, tanpa secara terpisah memeriksa status mereka:
fun funA(): Try<Int> { return Try { 1 } } fun funB(): Try<Int> { Thread.sleep(3000L) return Try { 2 } } val a = GlobalScope.async { funA() } val b = GlobalScope.async { funB() } val sum = runBlocking { a.await().flatMap { one -> b.await().map {two -> one + two } } }
Dan ini adalah contoh "pertarungan" yang lebih banyak. Kami memiliki permintaan ke server, kami memprosesnya, mendapatkan tubuh dari itu dan mencoba memetakannya ke kelas kami, dari mana kami sudah mengembalikan data.
fun makeRequest(request: Request): Try<List<ResponseData>> = Try { httpClient.newCall(request).execute() } .map { it.body() } .flatMap { Try { ObjectMapper().readValue(it, ParsedResponse::class.java) } } .map { it.data } fun main(args : Array<String>) { val response = makeRequest(RequestBody(args)) when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } }
Coba-tangkap akan membuat blok ini jauh lebih mudah dibaca. Dan dalam hal ini, kita mendapatkan response.data pada output, yang dapat kita proses tergantung pada hasilnya.
Hasil dari Kotlin 1.3
Kotlin 1.3 memperkenalkan kelas Hasil. Bahkan, itu mirip dengan Coba, tetapi dengan sejumlah keterbatasan. Ini awalnya dimaksudkan untuk digunakan untuk berbagai operasi asinkron.
val result: Result<VeryImportantData> = Result.runCatching { makeRequest() } .mapCatching { parseResponse(it) } .mapCatching { prepareData(it) } result.fold{ { data -> println(โWe have $dataโ) }, exception -> println(โThere is no any data, but it's your exception $exceptionโ) } )
Jika tidak salah, kelas ini sedang dalam tahap percobaan. Pengembang bahasa dapat mengubah tanda tangannya, perilaku, atau menghapusnya sama sekali, sehingga saat ini dilarang untuk menggunakannya sebagai nilai balik dari metode atau variabel. Namun, ini dapat digunakan sebagai variabel lokal (pribadi). Yaitu pada kenyataannya, ini dapat digunakan sebagai percobaan dari contoh.
Kesimpulan
Kesimpulan yang saya buat untuk diri saya sendiri:
- penanganan kesalahan fungsional di Kotlin sederhana dan nyaman;
- tidak ada yang mengganggu untuk memprosesnya melalui try-catch dalam gaya klasik (keduanya itu dan yang memiliki hak untuk hidup; keduanya itu dan yang nyaman);
- tidak adanya pengecualian yang diperiksa tidak berarti bahwa kesalahan tidak dapat ditangani;
- pengecualian produksi yang tanpa tertangkap menyebabkan konsekuensi yang menyedihkan.
Penulis artikel: Alexey Shafranov, pemimpin kelompok kerja (Jawa), Maxilect
PS Kami menerbitkan artikel kami di beberapa situs Runet. Berlangganan ke halaman kami di
VK ,
FB atau
saluran Telegram untuk mencari tahu tentang semua publikasi kami dan berita Maxilect lainnya.