Ini adalah situasi yang umum dan ada banyak cara umum untuk menghadapinya. Inilah upaya saya untuk jawaban kanonik. Beri komentar jika saya melewatkan sesuatu dan saya akan terus memperbarui postingan ini.
Ini adalah Panah
Apa yang Anda diskusikan dikenal sebagai panah anti-pola . Disebut panah karena rantai ifs bersarang membentuk blok kode yang meluas lebih jauh ke kanan dan kemudian kembali ke kiri, membentuk panah visual yang "menunjuk" ke sisi kanan panel editor kode.
Ratakan Panah dengan Penjaga
Beberapa cara umum untuk menghindari Panah dibahas di sini . Metode yang paling umum adalah dengan menggunakan pola penjaga , di mana kode menangani aliran pengecualian pertama dan kemudian menangani aliran dasar, misalnya alih-alih
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... Anda akan menggunakan ....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Ketika ada serangkaian panjang penjaga, ini meratakan kode karena semua penjaga muncul jauh ke kiri dan jika Anda tidak bersarang. Selain itu, Anda secara visual memasangkan kondisi logika dengan kesalahan yang terkait, yang membuatnya jauh lebih mudah untuk mengetahui apa yang sedang terjadi:
Panah:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Menjaga:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
Ini secara obyektif dan kuantitatif lebih mudah dibaca karena
- Karakter {dan} untuk blok logika yang diberikan lebih dekat bersama-sama
- Jumlah konteks mental yang dibutuhkan untuk memahami garis tertentu lebih kecil
- Keseluruhan logika yang terkait dengan kondisi if lebih cenderung berada pada satu halaman
- Kebutuhan bagi pembuat kode untuk menggulir halaman / jalur mata sangat berkurang
Cara menambahkan kode umum di akhir
Masalah dengan pola penjaga adalah bahwa ia bergantung pada apa yang disebut "pengembalian oportunistik" atau "keluar oportunistik." Dengan kata lain, itu memecah pola bahwa setiap fungsi harus memiliki tepat satu titik keluar. Ini masalah karena dua alasan:
- Itu menggosok beberapa orang dengan cara yang salah, misalnya orang yang belajar kode pada Pascal telah belajar bahwa satu fungsi = satu titik keluar.
- Itu tidak menyediakan bagian kode yang dieksekusi saat keluar tidak peduli apa , yang merupakan subjek di tangan.
Di bawah ini saya telah menyediakan beberapa opsi untuk mengatasi keterbatasan ini baik dengan menggunakan fitur bahasa atau dengan menghindari masalah sama sekali.
Opsi 1. Anda tidak dapat melakukan ini: gunakan finally
Sayangnya, sebagai pengembang c ++, Anda tidak dapat melakukan ini. Tetapi ini adalah jawaban nomor satu untuk bahasa-bahasa yang mengandung kata kunci terakhir, karena inilah yang tepat untuk itu.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Opsi 2. Hindari masalah: Merestrukturisasi fungsi Anda
Anda dapat menghindari masalah dengan memecah kode menjadi dua fungsi. Solusi ini memiliki manfaat bekerja untuk bahasa apa pun, dan selain itu dapat mengurangi kompleksitas siklomatik , yang merupakan cara yang terbukti untuk mengurangi tingkat cacat Anda, dan meningkatkan kekhususan setiap tes unit otomatis.
Ini sebuah contoh:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Opsi 3. Trik bahasa: Gunakan loop palsu
Trik umum lainnya yang saya lihat adalah menggunakan while (true) dan break, seperti yang ditunjukkan pada jawaban lain.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Meskipun ini kurang "jujur" daripada menggunakan goto
, itu kurang cenderung kacau ketika refactoring, karena jelas menandai batas-batas ruang lingkup logika. Seorang programmer naif yang memotong dan menempelkan label Anda atau goto
pernyataan Anda dapat menyebabkan masalah besar! (Dan terus terang polanya sangat umum sekarang saya pikir itu jelas mengomunikasikan maksud, dan karena itu tidak "tidak jujur" sama sekali).
Ada varian lain dari opsi ini. Sebagai contoh, seseorang bisa menggunakan switch
bukan while
. Konstruk bahasa apa pun dengan break
kata kunci mungkin akan berfungsi.
Opsi 4. Memanfaatkan siklus hidup objek
Satu pendekatan lain memanfaatkan siklus hidup objek. Gunakan objek konteks untuk membawa parameter Anda (sesuatu yang kurang naif dari contoh mencurigakan kami) dan buang ketika Anda selesai.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Catatan: Pastikan Anda memahami siklus hidup objek bahasa pilihan Anda. Anda memerlukan semacam pengumpulan sampah deterministik agar ini berfungsi, yaitu Anda harus tahu kapan destruktor akan dipanggil. Dalam beberapa bahasa Anda harus menggunakan Dispose
alih-alih destruktor.
Opsi 4.1. Memanfaatkan siklus hidup objek (pola pembungkus)
Jika Anda akan menggunakan pendekatan berorientasi objek, sebaiknya lakukan dengan benar. Opsi ini menggunakan kelas untuk "membungkus" sumber daya yang memerlukan pembersihan, serta operasi lainnya.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Sekali lagi, pastikan Anda memahami siklus hidup objek Anda.
Opsi 5. Trik bahasa: Gunakan evaluasi hubung singkat
Teknik lain adalah memanfaatkan evaluasi hubung singkat .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Solusi ini memanfaatkan cara kerja operator &&. Ketika sisi kiri && dievaluasi salah, sisi kanan tidak pernah dievaluasi.
Trik ini paling berguna ketika kode kompak diperlukan dan ketika kode tidak mungkin melihat banyak pemeliharaan, misalnya Anda menerapkan algoritma yang terkenal. Untuk pengkodean yang lebih umum, struktur kode ini terlalu rapuh; bahkan perubahan kecil pada logika dapat memicu penulisan ulang total.