Pengembalian GOTO

Sekarang semua orang mengerti bahwa menggunakan operator GOTO tidak hanya buruk, tetapi praktik yang mengerikan. Perdebatan tentang penggunaannya berakhir pada tahun 80-an abad XX dan dikeluarkan dari sebagian besar bahasa pemrograman modern. Tetapi, sebagaimana layaknya kejahatan sejati, ia berhasil menyamarkan dirinya dan bangkit kembali pada abad ke-21 dengan kedok pengecualian.


Pengecualian, di satu sisi, adalah konsep yang cukup sederhana dalam bahasa pemrograman modern. Di sisi lain, mereka sering digunakan secara tidak benar. Ada aturan sederhana dan terkenal - pengecualian hanya untuk menangani kerusakan. Dan interpretasi yang terlalu longgar dari konsep "kerusakan" mengarah ke semua masalah menggunakan GOTO.


Contoh teoretis


Perbedaan antara gangguan dan skenario bisnis negatif terlihat jelas pada jendela masuk dengan kasus penggunaan yang sangat sederhana:


  1. Pengguna memasukkan login / kata sandi.
  2. Pengguna mengklik tombol "Login".
  3. Aplikasi klien mengirimkan permintaan ke server.
  4. Server berhasil memeriksa nama pengguna / kata sandi (menganggap kehadiran pasangan yang sesuai sebagai berhasil).
  5. Server mengirim informasi ke klien bahwa otentikasi berhasil dan tautan ke halaman transisi.
  6. Klien pergi ke halaman yang ditentukan.

Dan satu ekstensi negatif:


4.1. Server tidak menemukan pasangan login / kata sandi yang sesuai dan mengirimkan pemberitahuan kepada klien tentang hal ini.


Mempertimbangkan bahwa skenario 4.1 adalah "masalah" dan karenanya harus diimplementasikan menggunakan pengecualian adalah kesalahan yang cukup umum. Ini sebenarnya tidak demikian. Ketidakcocokan login dan kata sandi adalah bagian dari pengalaman pengguna standar kami yang disediakan oleh logika bisnis skrip. Pelanggan bisnis kami mengharapkan perkembangan ini. Karenanya, ini bukan gangguan dan Anda tidak dapat menggunakan pengecualian di sini.


Kerusakan adalah: terputusnya koneksi antara klien dan utara, tidak dapat diaksesnya DBMS, skema yang salah dalam database. Dan sejuta lagi alasan yang memecah aplikasi kita dan tidak ada hubungannya dengan logika bisnis pengguna.


Di salah satu proyek, dalam pengembangan yang saya ikuti, ada logika logon yang lebih kompleks. Dengan memasukkan kata sandi yang salah 3 kali berturut-turut, pengguna diblokir sementara selama 15 menit. Mendapatkan 3 kali berturut-turut dalam kunci sementara, pengguna menerima kunci permanen. Ada juga aturan tambahan tergantung pada jenis pengguna. Implementasi pengecualian telah membuatnya sangat sulit untuk memperkenalkan aturan baru.


Akan menarik untuk mempertimbangkan contoh ini, tetapi terlalu besar dan tidak terlalu visual. Bagaimana kode yang membingungkan dengan logika bisnis tentang pengecualian menjadi jelas dan singkat, saya akan tunjukkan pada Anda dalam contoh lain.


Contoh Memuat Properti


Cobalah untuk melihat kode ini dan mengerti dengan jelas apa fungsinya. Prosedurnya tidak besar dengan logika yang cukup sederhana. Dengan gaya pemrograman yang baik, pemahaman tentang esensinya tidak boleh melebihi lebih dari 2-3 menit (saya tidak ingat berapa lama waktu saya untuk sepenuhnya memahami kode ini, tetapi jelas lebih dari 15 menit).


private WorkspaceProperties(){ Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true); //These mappings will replace any mappings that this hashtable had for any of the //keys currently in the specified map. getProperties().putAll( loadedProperties ); //     loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH, false); if (loadedProperties != null){ getProperties().putAll( loadedProperties ); } System.out.println("Loaded properties:" + getProperties()); } /** *  ,    . * @param filepath * @param throwIfNotFound -  FileNotFoundException,     * @return    null,      !throwIfNotFound * @throws FileNotFoundException throwIfNotFound        * @throws IOException     */ private Properties readPropertiesFromFile(String filepath, boolean throwIfNotExists){ Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); InputStream is = null; InputStreamReader isr = null; try{ int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0){ try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } System.out.println("Found file " + filepath); } catch( FileNotFoundException e) { System.out.println("File not found " + filepath); if (throwIfNotExists) throw new RuntimeException("Can`t load workspace properties." + filepath + " not found", e ); }catch (IOException e){ throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Jadi, mari kita ungkapkan rahasianya - apa yang terjadi di sini. Properti dari dua file WORK_PROPERTIES - WORK_PROPERTIES diperlukan dan MY_WORK_PROPERTIES tambahan, menambah toko properti umum. Ada nuansa - kami tidak tahu persis di mana file properti tertentu berada - itu bisa terletak di direktori saat ini dan di direktori leluhur (hingga tiga tingkat ke atas).


Setidaknya dua hal membingungkan di sini: parameter throwIfNotExists dan blok logika besar dalam catch FileNotFoundException . Semua petunjuk yang tidak jelas ini - pengecualian digunakan untuk mengimplementasikan logika bisnis (tapi bagaimana cara lain untuk menjelaskan bahwa dalam satu skenario, melempar pengecualian adalah kegagalan, dan yang lain tidak?).


Membuat kontrak yang tepat


Pertama, throwIfNotExists kita berurusan dengan throwIfNotExists . Ketika bekerja dengan pengecualian, sangat penting untuk memahami di mana tepatnya itu perlu diproses dalam hal kasus penggunaan. Dalam kasus ini, jelas bahwa metode readPropertiesFromFile tidak dapat memutuskan kapan tidak adanya file "buruk" dan ketika itu "baik". Keputusan semacam itu dibuat pada titik panggilannya. Komentar menunjukkan bahwa kami memutuskan apakah file ini harus ada atau tidak. Namun pada kenyataannya, kami tertarik bukan pada file itu sendiri, tetapi pengaturan dari itu. Sayangnya, ini tidak mengikuti dari kode.


Kami memperbaiki kedua kekurangan ini:


 Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll( loadedProperties ); 

Sekarang semantik ditampilkan dengan jelas -
WORK_PROPERTIES harus ditentukan, tetapi MY_WORK_PROPERTIES tidak MY_WORK_PROPERTIES . Juga, ketika refactoring, saya perhatikan bahwa readPropertiesFromFile tidak pernah dapat mengembalikan null dan mengambil keuntungan dari ini ketika membaca MY_WORK_PROPERTIES .


Kami memeriksa tanpa merusak


Refactoring sebelumnya juga mempengaruhi implementasi, tetapi tidak signifikan. Saya baru saja menghapus throwIfNotExists pemrosesan throwIfNotExists :


 if (throwIfNotExists) throw new RuntimeException(…); 

Setelah memeriksa implementasi lebih dekat, kami mulai memahami logika pembuat kode untuk mencari file. Pertama, diperiksa apakah file tersebut ada di direktori saat ini, jika tidak ditemukan, kami periksa di level yang lebih tinggi, dll. Yaitu menjadi jelas bahwa algoritma menyediakan tidak adanya file. Dalam hal ini, pemeriksaan dilakukan menggunakan pengecualian. Yaitu prinsip tersebut dilanggar - pengecualian dianggap bukan sebagai "sesuatu telah rusak", tetapi sebagai bagian dari logika bisnis.


Ada fungsi untuk memeriksa ketersediaan file untuk membaca File.canRead() . Dengan menggunakannya, Anda dapat menyingkirkan logika bisnis di catch


  try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } 

Mengubah kode, kita mendapatkan yang berikut:


 private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } } System.out.println("Found file " + filepath); } catch (FileNotFoundException e) { System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Saya juga mengurangi tingkat variabel ( is , isr ) ke minimum yang diizinkan.


Refactoring sederhana seperti ini sangat meningkatkan keterbacaan kode. Kode langsung menampilkan algoritme (jika file ada, maka kita membacanya, kalau tidak kita mengurangi jumlah upaya dan melihat di direktori di atas).


Mengungkap GOTO


Pertimbangkan secara rinci apa yang terjadi dalam suatu situasi jika file tidak ditemukan:


 } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } 

Dapat dilihat bahwa di sini pengecualian digunakan untuk mengganggu siklus eksekusi dan benar-benar melakukan fungsi GOTO.


Bagi yang ragu, kami akan membuat perubahan lain. Alih-alih menggunakan kruk kecil dalam bentuk loadingTryLeft = 0 (kruk, karena sebenarnya upaya yang berhasil tidak loadingTryLeft = 0 jumlah upaya yang tersisa), kami secara eksplisit menunjukkan bahwa membaca file akan keluar dari fungsi (tanpa lupa menulis pesan):


 try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { 

Ini memungkinkan kami untuk mengganti kondisi while (loadingTryLeft > 0) dengan while(true) :


 try { int loadingTryLeft = 3; String relativePath = ""; while (true) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); // GOTO: FFN } } } } catch (FileNotFoundException e) { // LABEL: FFN System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } 

Untuk menyingkirkan throw new FileNotFoundException berbau busuk, Anda perlu mengingat fungsi kontraknya. Bagaimanapun, fungsi mengembalikan satu set properti, jika mereka tidak bisa membaca file, kami mengembalikannya kosong. Karena itu, tidak ada alasan untuk melempar pengecualian dan menangkapnya. Kondisi while (loadingTryLeft > 0) biasa while (loadingTryLeft > 0) cukup:


 private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Pada prinsipnya, dari sudut pandang pekerjaan yang benar dengan pengecualian, semuanya ada di sini. Ada keraguan tentang perlunya melempar RuntimeException jika terjadi masalah IOException, tetapi kami akan membiarkannya karena itu demi kompatibilitas.



Ada beberapa hal kecil yang tersisa sehingga kita dapat membuat kode lebih fleksibel dan mudah dimengerti:


  • Nama metode readPropertiesFromFile memperlihatkan implementasinya (by the way, serta melempar FileNotFoundException). Lebih baik menyebutnya lebih netral dan ringkas - loadProperties (...)
  • Metode ini secara simultan mencari dan membaca. Bagi saya, ini adalah dua tanggung jawab berbeda yang dapat dibagi dalam metode yang berbeda.
  • Kode ini awalnya ditulis di bawah Java 6, tetapi sekarang digunakan di Java 7. Ini memungkinkan penggunaan sumber daya yang dapat ditutup.
  • Saya tahu dari pengalaman bahwa ketika menampilkan informasi tentang file yang ditemukan atau tidak ditemukan, lebih baik menggunakan path lengkap ke file, daripada relatif.
  • if (loadingTryLeft > 0) relativePath += "../"; - jika Anda hati-hati melihat kode, Anda dapat melihat - pemeriksaan ini tidak perlu, karena jika batas pencarian habis, nilai baru tidak akan digunakan. Dan jika ada sesuatu yang berlebihan dalam kode, ini adalah sampah yang harus dibuang.

Versi terakhir dari kode sumber:


 private WorkspaceProperties() { super(new Properties()); if (defaultInstance != null) throw new IllegalStateException(); Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } getProperties().putAll(loadedProperties); loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll(loadedProperties); System.out.println("Loaded properties:" + getProperties()); } private Properties readPropertiesFromFile(String filepath) { System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { return read(file); } else { relativePath += "../"; loadingTryLeft -= 1; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return new Properties(); } private Properties read(File file) throws IOException { try (InputStream is = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(is, "UTF-8")) { Properties loadedProperties = new Properties(); loadedProperties.load(isr); System.out.println("Found file " + file.getAbsolutePath()); return loadedProperties; } } 

Ringkasan


Contoh yang diuraikan jelas menggambarkan penanganan kode sumber yang ceroboh. Alih-alih menggunakan pengecualian untuk menangani kerusakan, diputuskan untuk menggunakannya untuk menerapkan logika bisnis. Ini segera menyebabkan kompleksitas dukungannya, yang tercermin dalam pengembangan lebih lanjut untuk memenuhi persyaratan baru dan, sebagai akibatnya, penyimpangan dari prinsip-prinsip pemrograman struktural. Menggunakan aturan sederhana - pengecualian hanya untuk gangguan - akan membantu Anda menghindari kembali ke era GOTO dan menjaga kode Anda tetap bersih, dapat dimengerti, dan dapat dikembangkan.

Source: https://habr.com/ru/post/id484840/


All Articles