Cara elegan untuk menangani if ​​(if else) lain

161

Ini niggle kecil, tetapi setiap kali saya harus kode sesuatu seperti ini, pengulangan mengganggu saya, tapi saya tidak yakin bahwa salah satu solusi tidak lebih buruk.

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}
  • Apakah ada nama untuk jenis logika ini?
  • Apakah saya sedikit terlalu OCD?

Saya terbuka untuk saran kode jahat, jika hanya karena penasaran ...

Benjol
sumber
8
@Emmad Kareem: dua DefaultActionpanggilan melanggar prinsip KERING
Abyx
Terima kasih atas balasan Anda, tetapi saya pikir itu OK, kecuali untuk tidak menggunakan try / catch karena mungkin ada kesalahan yang tidak mengembalikan hasil dan akan menyebabkan abend (tergantung pada bahasa pemrograman Anda).
NoChance
20
Saya pikir masalah utama di sini adalah bahwa Anda bekerja pada tingkat abstraksi yang tidak konsisten . Tingkat abstraksi yang lebih tinggi adalah: make sure I have valid data for DoSomething(), and then DoSomething() with it. Otherwise, take DefaultAction(). Detail penting untuk memastikan Anda memiliki data untuk DoSomething () berada pada level abstraksi yang lebih rendah, dan karenanya harus dalam fungsi yang berbeda. Fungsi ini akan memiliki nama di tingkat abstraksi yang lebih tinggi, dan implementasinya akan tingkat rendah. Jawaban yang baik di bawah mengatasi masalah ini.
Gilad Naor
6
Silakan tentukan bahasa. Kemungkinan solusi, idiom standar, dan norma budaya yang sudah lama berbeda untuk berbagai bahasa dan akan mengarah ke jawaban yang berbeda untuk Q.
Caleb
1
Anda dapat merujuk buku ini "Refactoring: Meningkatkan Desain Kode yang Ada". Ada beberapa bagian tentang struktur if-else, praktik yang benar-benar bermanfaat.
Vacker

Jawaban:

96

Ekstrak untuk memisahkan fungsi (metode) dan gunakan returnpernyataan:

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }
}

DefaultAction();

Atau, mungkin lebih baik, pisahkan konten yang didapat dan prosesnya:

contents_t get_contents(name_t file)
{
    if(!FileExists(file))
        return null;

    contents = OpenFile(file);
    if(!SomeTest(contents)) // like IsContentsValid
        return null;

    return contents;
}

...

contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();

Pembaruan:

Mengapa tidak pengecualian, mengapa OpenFiletidak melempar pengecualian IO:
Saya pikir itu benar-benar pertanyaan umum, bukan pertanyaan tentang file IO. Nama-nama seperti FileExists, OpenFiledapat membingungkan, tetapi jika menggantinya dengan Foo,, Bardll, - akan lebih jelas yang DefaultActiondapat dipanggil sesering mungkin DoSomething, jadi ini mungkin kasus yang tidak biasa. Péter Török menulis tentang ini di akhir jawabannya

Mengapa ada operator kondisional ternary dalam varian ke-2:
Jika akan ada tag [C ++], saya telah menulis ifpernyataan dengan deklarasi contentsdi bagian kondisinya:

if(contents_t contents = get_contents(file))
    DoSomething(contents);
else
    DefaultAction();

Tetapi untuk bahasa lain (seperti C), if(contents) ...; else ...;persis sama dengan pernyataan ekspresi dengan operator kondisional ternary, tetapi lebih lama. Karena bagian utama dari kode adalah get_contentsfungsi, saya hanya menggunakan versi yang lebih pendek (dan juga contentstipe yang dihilangkan ). Bagaimanapun, itu melampaui pertanyaan ini.

Abyx
sumber
93
+1 untuk beberapa pengembalian - ketika metode dibuat cukup kecil , pendekatan ini paling cocok untuk saya
agas
Bukan penggemar berat dari beberapa pengembalian, meskipun saya kadang-kadang menggunakannya. Ini cukup masuk akal pada sesuatu yang sederhana, tetapi tidak skalakan dengan baik. Standar kami adalah untuk menghindarinya kecuali metode-metode sederhana yang gila karena metode-metode cenderung tumbuh dalam ukuran lebih besar daripada menyusut.
Brian Knoblauch
3
Beberapa jalur pengembalian dapat memiliki implikasi kinerja negatif dalam program C ++, mengalahkan upaya pengoptimal untuk menggunakan RVO (juga NRVO, kecuali setiap jalur mengembalikan objek yang sama).
Functastic
Saya akan merekomendasikan membalik logika pada solusi 2: {if (file ada) {set content; if (somethingest) {mengembalikan konten; }} return null; } Ini menyederhanakan aliran dan mengurangi jumlah garis.
Wedge
1
Hai Abyx, saya perhatikan Anda memasukkan beberapa umpan balik dari komentar di sini: terima kasih telah melakukannya. Saya telah membersihkan semua yang ditujukan pada jawaban Anda dan jawaban lainnya.
56

Jika bahasa pemrograman yang Anda gunakan (0) perbandingan binary hubung singkat (mis. Jika tidak memanggil SomeTestjika FileExistsmengembalikan false) dan (1) tugas mengembalikan nilai (hasil OpenFileditugaskan ke contentsdan kemudian nilai tersebut diteruskan sebagai argumen to SomeTest), Anda dapat menggunakan sesuatu seperti yang berikut ini, tetapi masih disarankan bagi Anda untuk mengomentari kode yang mencatat bahwa single =disengaja.

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

Bergantung pada seberapa berbelit-belitnya jika, mungkin lebih baik memiliki variabel flag (yang memisahkan pengujian kondisi keberhasilan / kegagalan dengan kode yang menangani kesalahan DefaultActiondalam kasus ini)

frozenkoi
sumber
Inilah yang akan saya lakukan.
Anthony
13
Cukup kotor untuk menaruh begitu banyak kode dalam sebuah ifpernyataan, menurut saya.
moteutsch
15
Sebaliknya, saya menyukai pernyataan "jika ada sesuatu dan memenuhi kondisi ini". +1
Gorpik
Saya juga! Saya pribadi tidak suka cara orang menggunakan beberapa pengembalian beberapa tempat tidak terpenuhi. Mengapa Anda tidak membalikkan if dan mengeksekusi kode Anda jika sudah dipenuhi?
klaar
"Jika ada sesuatu dan memenuhi kondisi ini" tidak apa-apa. "Jika ada sesuatu dan melakukan sesuatu yang berhubungan secara tangensial di sini dan memenuhi kondisi ini", OTOH, membingungkan. Dengan kata lain, saya tidak suka efek samping dalam suatu kondisi.
Piskvor
26

Lebih serius daripada pengulangan panggilan ke DefaultAction adalah gaya itu sendiri karena kode ditulis non-ortogonal (lihat jawaban ini untuk alasan yang baik untuk menulis ortogonal).

Untuk menunjukkan mengapa kode non-ortogonal buruk, perhatikan contoh asli, ketika persyaratan baru bahwa kita tidak boleh membuka file jika disimpan pada disk jaringan diperkenalkan. Nah, kalau begitu kita bisa memperbarui kodenya sebagai berikut:

if(FileExists(file))
{
    if(! OnNetworkDisk(file))
    {
        contents = OpenFile(file); // <-- prevents inclusion in if
        if(SomeTest(contents))
        {
            DoSomething(contents);
        }
        else
        {
            DefaultAction();
        }
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Tapi kemudian ada juga persyaratan bahwa kita tidak boleh membuka file besar lebih dari 2Gb. Baiklah, kami baru saja memperbarui lagi:

if(FileExists(file))
{
    if(LessThan2Gb(file))
    {
        if(! OnNetworkDisk(file))
        {
            contents = OpenFile(file); // <-- prevents inclusion in if
            if(SomeTest(contents))
            {
                DoSomething(contents);
            }
            else
            {
                DefaultAction();
            }
        }
        else
        {
            DefaultAction();
        }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Seharusnya sangat jelas bahwa gaya kode seperti itu akan sangat menyulitkan pemeliharaan.

Di antara jawaban di sini yang ditulis dengan benar secara orthogonal adalah contoh kedua Abyx dan jawaban Jan Hudec , jadi saya tidak akan mengulanginya, hanya menunjukkan bahwa menambahkan dua persyaratan dalam jawaban itu hanya akan menjadi

if(! LessThan2Gb(file))
    return null;

if(OnNetworkDisk(file))
    return null;

(atau goto notexists;alih-alih return null;), tidak memengaruhi kode lain selain baris yang ditambahkan . Misal orthogonal.

Saat menguji, aturan umum harus menguji pengecualian, bukan kasus normal .

hlovdal
sumber
8
+1 untuk saya. Pengembalian awal membantu menghindari pola anti panah. Lihat codinghorror.com/blog/2006/01/flattening-arrow-code.html dan lostechies.com/chrismissal/2009/05/27/... Sebelum membaca tentang pola ini, saya selalu berlangganan 1 entri / keluar per fungsi teori karena apa yang saya diajarkan 15 tahun yang lalu. Saya merasa ini hanya membuat kode jadi lebih mudah dibaca dan seperti yang Anda sebutkan lebih mudah dikelola.
Mr Moose
3
@MrMoose: penyebutan Anda tentang anti-pola panah menjawab pertanyaan eksplisit Benjol: "Apakah ada nama untuk jenis logika ini?" Posting sebagai jawaban dan Anda mendapat suara saya.
outis
Ini jawaban yang bagus, terima kasih. Dan @MrMoose: "panah anti pola" mungkin menjawab peluru pertama saya, jadi ya, lakukan posting. Saya tidak bisa berjanji saya akan menerimanya, tetapi itu layak mendapatkan suara!
Benjol
@outis. Terima kasih. Saya sudah menambahkan jawaban. Pola anti panah tentu saja relevan di pos hlovdal dan klausa penjaganya bekerja dengan baik dalam menyiasatinya. Saya tidak tahu bagaimana Anda bisa menjawab poin kedua untuk ini. Saya tidak memenuhi syarat untuk mendiagnosis hal itu :)
Tn. Moose
4
+1 untuk "pengecualian pengujian, bukan kasus normal."
Roy Tinker
25

Jelas:

Whatever(Arguments)
{
    if(!FileExists(file))
        goto notexists;
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(!SomeTest(contents))
        goto notexists;
    DoSomething(contents);
    return;
notexists:
    DefaultAction();
}

Kamu bilang kamu terbuka bahkan untuk solusi jahat, jadi menggunakan hitungan keburukan jahat, bukan?

Sebenarnya tergantung pada konteksnya solusi ini mungkin tidak terlalu jahat daripada kejahatan melakukan tindakan dua kali atau variabel ekstra jahat. Saya membungkusnya dalam suatu fungsi, karena itu pasti tidak akan OK di tengah fungsi yang panjang (paling tidak karena pengembalian di tengah). Tetapi dari fungsi panjang tidak OK, titik.

Ketika Anda memiliki pengecualian, mereka akan lebih mudah dibaca, terutama jika Anda dapat memiliki OpenFile dan DoSomething dengan hanya membuang pengecualian jika kondisinya tidak terpenuhi, jadi Anda tidak memerlukan pemeriksaan eksplisit sama sekali. Di sisi lain dalam C ++, Java dan C # melempar pengecualian adalah operasi yang lambat, jadi dari titik kinerja, goto masih lebih disukai.


Catatan tentang "jahat": C ++ FAQ 6.15 mendefinisikan "jahat" sebagai:

Ini berarti ini dan itu adalah sesuatu yang harus Anda hindari sebagian besar waktu, tetapi bukan sesuatu yang harus Anda hindari sepanjang waktu. Misalnya, Anda akan berakhir menggunakan hal-hal "jahat" ini kapan pun mereka "paling tidak jahat dari alternatif jahat."

Dan itu berlaku gotodalam konteks ini. Konstruksi kontrol aliran terstruktur lebih baik sebagian besar waktu, tetapi ketika Anda mendapatkan dalam situasi di mana mereka mengumpulkan terlalu banyak kejahatan mereka sendiri, seperti penugasan dalam kondisi, bersarang lebih dari sekitar 3 level dalam, kode duplikat atau kondisi panjang, gotomungkin berakhir menjadi kurang jahat.

Jan Hudec
sumber
11
Kursor saya melayang di atas tombol terima ... hanya untuk menolak semua puritan. Oooohh godaan: D
Benjol
2
Ya ya! Ini adalah cara yang benar-benar "benar" untuk menulis kode. Struktur kode sekarang adalah "Jika kesalahan, menangani kesalahan. Tindakan normal. Jika kesalahan, menangani kesalahan. Tindakan normal" yang persis seperti seharusnya. Semua kode "normal" ditulis hanya dengan lekukan satu tingkat sementara semua kode terkait kesalahan memiliki dua tingkat lekukan. Jadi kode normal DAN PALING PENTING mendapatkan tempat visual yang paling menonjol dan dimungkinkan untuk membaca aliran dengan cepat dan mudah secara berurutan. Dengan segala cara menerima jawaban ini.
hlovdal
2
Dan aspek lain adalah bahwa kode yang ditulis dengan cara ini adalah ortogonal. Misalnya dua baris "if (! FileExists (file)) \ n \ tgoto notexists;" sekarang HANYA terkait dengan penanganan aspek kesalahan tunggal (KISS) ini dan yang paling penting itu tidak memengaruhi jalur lain mana pun . Jawaban ini stackoverflow.com/a/3272062/23118 mencantumkan beberapa alasan bagus untuk menjaga kode tetap ortogonal.
hlovdal
5
Berbicara tentang solusi jahat: Saya bisa mendapatkan solusi Anda tanpa kebagian:for(;;) { if(!FileExists(file)) break; contents = OpenFile(file); if(!SomeTest(contents)) break; DoSomething(contents); return; } /* broken out */ DefaultAction();
herby
4
@herby: Solusi Anda lebih jahat daripada goto, karena Anda menyalahgunakan breakdengan cara yang tak seorang pun mengharapkannya untuk disalahgunakan, sehingga orang yang membaca kode akan lebih banyak kesulitan melihat ke mana keretakan membawa mereka daripada dengan goto yang mengatakannya secara eksplisit. Selain itu Anda menggunakan loop tak terbatas yang hanya akan berjalan sekali, yang akan agak membingungkan. Sayangnya do { ... } while(0)tidak sepenuhnya dapat dibaca, karena Anda hanya melihat itu hanya blok lucu ketika Anda sampai di akhir dan C tidak mendukung pemecahan dari blok lain (tidak seperti perl).
Jan Hudec
12
function FileContentsExists(file) {
    return FileExists(file) ? OpenFile(file) : null;
}

...

contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}
herby
sumber
atau lanjutkan pria tambahan dan buat metode (file) FileExistsAndConditionMet tambahan ...
UncleZeiv
@herby SomeTestdapat memiliki semantik yang sama dengan keberadaan file jika SomeTestmemeriksa jenis file, misalnya memeriksa bahwa .gif benar-benar file GIF.
Abyx
1
Iya. Tergantung. @Benjol lebih tahu.
herby
3
... tentu saja maksudku "lanjutkan mIle" ... :)
UncleZeiv
2
Yang mengambil ravioli ke ekstremitas bahkan aku tidak pergi ke (dan saya saya ekstrim dalam hal ini) ... Saya pikir sekarang itu adalah baik dibaca mengingat contents && f(contents). Dua fungsi untuk menyelamatkan yang lain ?!
herby
12

Satu kemungkinan:

boolean handled = false;

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        handled = true;
    }
}
if (!handled)
{
    DefaultAction();
}

Tentu saja, ini membuat kode sedikit lebih kompleks dengan cara yang berbeda. Jadi itu sebagian besar pertanyaan gaya.

Pendekatan yang berbeda akan menggunakan pengecualian, misalnya:

try
{
    contents = OpenFile(file); // throws IO exception if file not found
    DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
    DefaultAction();
    // and the exception should be at least logged...
}

Ini terlihat lebih sederhana, namun itu hanya berlaku jika

  • kami tahu persis seperti apa pengecualian yang diharapkan, dan DefaultAction()cocok untuk masing-masing
  • kami berharap pemrosesan file berhasil, dan file yang hilang atau gagal SomeTest()jelas merupakan kondisi yang salah, sehingga sangat cocok untuk melempar pengecualian.
Péter Török
sumber
19
Tidaaaak ~! Bukan variabel flag, ini jelas cara yang salah, karena mengarah ke kompleks, sulit dipahami (di mana-bendera-menjadi-benar) dan sulit untuk memperbaiki kode.
Abyx
Tidak jika Anda membatasi ruang lingkup lokal. (function () { ... })()dalam Javascript, { flag = false; ... }dalam C-like dll.
herby
+1 untuk logika pengecualian, yang bisa menjadi solusi yang paling cocok tergantung pada skenario.
Steven Jeuris
4
+1 Rekan ini, 'Tidaaaak!' itu lucu. Saya pikir variabel status dan pengembalian awal keduanya masuk akal dalam kasus-kasus tertentu. Dalam rutinitas yang lebih kompleks, saya akan memilih variabel status karena, alih-alih menambahkan kompleksitas, yang sebenarnya dilakukan adalah membuat logika eksplisit. Tidak ada yang salah dengan itu.
grossvogel
1
Ini adalah format pilihan kami tempat saya bekerja. 2 opsi utama yang dapat digunakan tampaknya adalah "beberapa pengembalian" dan "variabel bendera". Kelihatannya tidak ada yang benar-benar memiliki keunggulan sejati, tetapi keduanya cocok dengan keadaan tertentu lebih baik daripada yang lain. Harus pergi dengan kasing khas Anda. Hanya "Emacs" vs. "Vi" perang agama. :-)
Brian Knoblauch
11

Ini berada pada level abstraksi yang lebih tinggi:

if (WeCanDoSomething(file))
{
   DoSomething(contents);
}
else
{
   DefaultAction();
} 

Dan ini mengisi rinciannya.

boolean WeCanDoSomething(file)
{
    if FileExists(file)
    {
        contents = OpenFile(file);
        return (SomeTest(contents));
    }
    else
    {
        return FALSE;
    }
}
Jeanne Pindar
sumber
11

Fungsi harus melakukan satu hal. Mereka harus melakukannya dengan baik. Mereka seharusnya melakukannya saja.
- Robert Martin dalam Kode Bersih

Beberapa orang menganggap pendekatan itu sedikit ekstrem, tetapi juga sangat bersih. Izinkan saya memberi ilustrasi dengan Python:

def processFile(self):
    if self.fileMeetsTest():
        self.doSomething()
    else:
        self.defaultAction()

def fileMeetsTest(self):
    return os.path.exists(self.path) and self.contentsTest()

def contentsTest(self):
    with open(self.path) as file:
        line = file.readline()
        return self.firstLineTest(line)

Ketika dia mengatakan fungsi harus melakukan satu hal, dia berarti satu hal. processFile()memilih tindakan berdasarkan hasil tes, dan hanya itu yang dilakukannya. fileMeetsTest()menggabungkan semua kondisi tes, dan hanya itu yang dilakukannya. contentsTest()mentransfer baris pertama ke firstLineTest(), dan hanya itu yang dilakukannya.

Sepertinya banyak fungsi, tetapi bisa dibilang seperti bahasa Inggris langsung:

Untuk memproses file, periksa apakah memenuhi tes. Jika ya, maka lakukan sesuatu. Jika tidak, ambil tindakan default. File memenuhi tes jika ada dan lulus tes konten. Untuk menguji konten, buka file dan uji baris pertama. Tes untuk baris pertama ...

Memang, itu sedikit bertele-tele, tetapi perhatikan bahwa jika pengelola tidak peduli dengan detailnya, ia dapat berhenti membaca setelah hanya 4 baris kode processFile(), dan ia masih akan memiliki pengetahuan tingkat tinggi yang baik tentang apa fungsi tersebut.

Karl Bielefeldt
sumber
5
+1 Ini saran yang bagus, tetapi apa yang merupakan "satu hal" tergantung pada lapisan abstraksi saat ini. processFile () adalah "satu hal," tetapi dua hal: fileMeetsTest () dan doSomething () atau defaultAction (). Saya takut bahwa aspek "satu hal" dapat membingungkan pemula yang tidak a priori memahami konsep.
Caleb
1
Itu tujuan yang baik ... Itu saja yang saya katakan tentang hal itu ... ;-)
Brian Knoblauch
1
Saya tidak suka menyampaikan argumen secara implisit sebagai variabel instan seperti itu. Anda penuh dengan variabel instance "tidak berguna" dan ada banyak cara untuk merusak negara Anda dan menghancurkan invarian.
hugomg
@ Caleb, ProcessFile () memang melakukan satu hal. Seperti yang dikatakan Karl dalam posnya, ia menggunakan tes untuk memutuskan tindakan mana yang akan diambil, dan menunda implementasi aktual dari kemungkinan tindakan ke metode lain. Jika Anda menambahkan lebih banyak tindakan alternatif, kriteria tujuan tunggal untuk metode ini masih akan dipenuhi selama tidak ada nesting logika terjadi dalam metode langsung.
S.Robins
6

Berkenaan dengan apa ini disebut, itu dapat dengan mudah berkembang menjadi pola anti panah saat kode Anda tumbuh untuk menangani lebih banyak persyaratan (seperti yang ditunjukkan oleh jawaban yang diberikan di https://softwareengineering.stackexchange.com/a/122625/33922 ) dan kemudian jatuh ke dalam perangkap memiliki bagian besar kode dengan pernyataan kondisional bersarang yang menyerupai panah.

Lihat Tautan seperti;

http://codinghorror.com/blog/2006/01/flattening-arrow-code.html

http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/

Ada banyak hal lain tentang ini dan pola anti lainnya yang dapat ditemukan di Google.

Beberapa tips hebat yang Jeff berikan di blog-nya tentang ini adalah;

1) Ganti kondisi dengan klausa penjaga.

2) Dekomposisi blok bersyarat menjadi fungsi terpisah.

3) Ubah cek negatif menjadi cek positif

4) Selalu kembali sesegera mungkin dari fungsi secara oportunistik.

Lihat beberapa komentar di blog Jeff mengenai saran Steve McConnells tentang pengembalian awal juga;

"Gunakan pengembalian ketika meningkatkan keterbacaan: Dalam rutinitas tertentu, setelah Anda tahu jawabannya, Anda ingin segera mengembalikannya ke rutinitas pemanggilan. Jika rutin didefinisikan sedemikian rupa sehingga tidak memerlukan pembersihan lebih lanjut setelah itu mendeteksi kesalahan, tidak segera kembali berarti Anda harus menulis lebih banyak kode. "

...

"Minimalkan jumlah pengembalian di setiap rutin: Lebih sulit untuk memahami rutin ketika, membacanya di bagian bawah, Anda tidak menyadari kemungkinan itu kembali di suatu tempat. Untuk alasan itu, gunakan pengembalian dengan bijak - hanya ketika mereka meningkatkan keterbacaan. "

Saya selalu berlangganan 1 entri / keluar per teori fungsi karena apa yang saya diajarkan 15 tahun yang lalu. Saya merasa ini hanya membuat kode jadi lebih mudah dibaca dan seperti yang Anda sebutkan lebih mudah dikelola

Tuan Moose
sumber
6

Ini sesuai dengan aturan KERING, no-goto, dan no-multiple-return, dapat diukur dan dibaca menurut saya:

success = FileExists(file);
if (success)
{
    contents = OpenFile(file);
    success = SomeTest(contents);
}
if (success)
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}
mouviciel
sumber
1
Sesuai dengan standar tidak selalu sama dengan kode yang baik. Saat ini saya ragu-ragu pada cuplikan kode ini.
Brian Knoblauch
ini hanya menggantikan 2 defaultAction (); dengan 2 kondisi if identik dan menambahkan variabel flag yang imo jauh lebih buruk.
Ryathal
3
Manfaat menggunakan konstruk seperti ini adalah karena jumlah tes meningkat kode tidak mulai bersarang lebih banyak ifdi dalam yang lain if. Juga, kode untuk menangani kasus yang gagal ( DefaultAction()) hanya di satu tempat dan untuk tujuan debugging kode tidak melompat di sekitar fungsi pembantu dan menambahkan breakpoints ke garis-garis di mana successvariabel diubah dengan cepat dapat menunjukkan tes mana yang lulus (di atas dipicu breakpoint) dan mana yang belum diuji (di bawah).
frozenkoi
1
Yeeaah, saya agak menyukainya, tapi saya pikir saya akan mengganti nama successmenjadi ok_so_far:)
Benjol
Ini sangat mirip dengan apa yang saya lakukan ketika (1) prosesnya sangat linier ketika semuanya berjalan dengan baik dan (2) Anda akan memiliki panah anti-pola. Saya, bagaimanapun, mencoba untuk menghindari menambahkan variabel tambahan, yang biasanya mudah jika Anda berpikir dalam hal prasyarat untuk langkah selanjutnya (yang agak berbeda dari bertanya apakah langkah sebelumnya gagal). Jika file ada, buka file tersebut. Jika file terbuka, baca isinya. Jika saya memiliki konten, proseskan, jika tidak lakukan tindakan default.
Adrian McCarthy
3

Saya akan mengekstraknya ke metode terpisah dan kemudian:

if(!FileExists(file))
{
    DefaultAction();
    return;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}

DoSomething(contents);

yang juga memungkinkan untuk

if(!FileExists(file))
{
    DefaultAction();
    return Result.FileNotFound;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return Result.TestFailed;
}

DoSomething(contents);
return Result.Success;            

maka mungkin Anda bisa menghapus DefaultActionpanggilan dan meninggalkan mengeksekusi DefaultActionuntuk si penelepon:

Result OurMethod(file)
{
    if(!FileExists(file))
    {
        return Result.FileNotFound;
    }

    contents = OpenFile(file);
    if(!SomeTest(contents))
    {
        return Result.TestFailed;
    }

    DoSomething(contents);
    return Result.Success;            
}

void Caller()
{
    // something, something...

    var result = OurMethod(file);
    // if (result == Result.FileNotFound || result == Result.TestFailed), or just
    if (result != Result.Success)        
    {
        DefaultAction();
    }
}

Saya suka pendekatan Jeanne Pindar juga.

Konrad Morawski
sumber
3

Untuk kasus khusus ini, jawabannya cukup mudah ...

Ada kondisi lomba antara FileExistsdan OpenFile: apa yang terjadi jika file dihapus?

Satu-satunya cara yang waras untuk menangani kasus khusus ini adalah dengan melewati FileExists:

contents = OpenFile(file);
if (!contents) // open failed
    DefaultAction();
else (SomeTest(contents))
    DoSomething(contents);

Ini dengan rapi menyelesaikan masalah ini dan membuat kode lebih bersih.

Secara umum: Cobalah untuk memikirkan kembali masalah dan mencari solusi lain yang menghindari masalah sepenuhnya.

Martin Wickman
sumber
2

Kemungkinan lain jika Anda tidak suka melihat terlalu banyak yang lain adalah untuk menghentikan penggunaan yang lain sama sekali dan memberikan pernyataan pengembalian ekstra. Lain adalah jenis berlebihan kecuali Anda memerlukan logika yang lebih kompleks untuk menentukan apakah ada lebih dari dua kemungkinan tindakan.

Dengan demikian contoh Anda bisa menjadi:

void DoABunchOfStuff()
{
    if(FileExists(file))
    {
        DoSomethingWithFileContent(file);
        return;
    }

    DefaultAction();
}

void DoSomethingWithFileContent(file)
{        
    var contents = GetFileContents(file)

    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }

    DefaultAction();
}

AReturnType GetFileContents(file)
{
    return OpenFile(file);
}

Secara pribadi saya tidak keberatan menggunakan klausa lain karena menyatakan secara eksplisit bagaimana logika seharusnya bekerja, dan dengan demikian meningkatkan keterbacaan kode Anda. Namun beberapa alat kode kecantikan lebih suka untuk menyederhanakan pernyataan tunggal jika untuk mencegah logika bersarang.

S.Robins
sumber
2

Kasus yang ditunjukkan dalam kode sampel biasanya dapat direduksi menjadi satu ifpernyataan. Pada banyak sistem, fungsi buka file akan mengembalikan nilai yang tidak valid jika file belum ada. Terkadang ini adalah perilaku default; di lain waktu, harus ditentukan melalui argumen. Ini berarti FileExistspengujian dapat dibatalkan, yang juga dapat membantu dengan kondisi balapan yang dihasilkan dari penghapusan file antara pengujian keberadaan dan pembukaan file.

file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
    DoSomething(file);
} else {
    DefaultAction();
}

Ini tidak secara langsung mengatasi masalah pencampuran tingkat abstraksi karena menghindari masalah beberapa tes yang tidak dapat dipasangkan secara keseluruhan, meskipun menghapus dengan uji keberadaan file tidak tidak kompatibel dengan pemisahan tingkat abstraksi. Dengan asumsi bahwa pegangan file tidak valid sama dengan "false" dan handle file ditutup ketika mereka keluar dari ruang lingkup:

OpenFileIfSomething(path:String) : FileHandle {
    file = OpenFile(path);
    if (file && SomeTest(file)) {
        return file;
    }
    return null;
}

...

if ((file = OpenFileIfSomething(path))) {
    DoSomething(file);
} else {
    DefaultAction();
}
outis
sumber
2

Saya setuju dengan frozenkoi, bagaimanapun, untuk C # lagian, saya pikir itu akan membantu untuk mengikuti sintaks dari metode TryParse.

if(FileExists(file) && TryOpenFile(file, out contents))
    DoSomething(contents);
else
    DefaultAction();
bool TryOpenFile(object file, out object contents)
{
    try{
        contents = OpenFile(file);
    }
    catch{
        //something bad happened, computer probably exploded
        return false;
    }
    return true;
}
Biff MaGriff
sumber
1

Kode Anda jelek karena Anda melakukan terlalu banyak dalam satu fungsi. Anda ingin memproses file atau mengambil tindakan default, jadi mulailah dengan mengatakan bahwa:

if (!ProcessFile(file)) { 
  DefaultAction(); 
}

Programmer Perl dan Ruby menulis processFile(file) || defaultAction()

Sekarang, tulislah ProcessFile:

if (FileExists(file)) { 
  contents = OpenFile(file);
  if (SomeTest(contents)) {
    processContents(contents);
    return true;
  }
}
return false;
kevin cline
sumber
1

Tentu saja Anda hanya bisa melangkah sejauh ini dalam skenario seperti ini, tetapi berikut ini caranya:

interface File<T> {
    function isOK():Bool;
    function getData():T;
}

var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Anda mungkin ingin filter tambahan. Kemudian lakukan ini:

var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Meskipun ini mungkin masuk akal juga:

function eat(apple:Apple) {
     if (isEdible(apple)) 
         digest(apple);
     else
         die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(appleFile.getData());
else
    cry();

Mana yang terbaik? Itu tergantung pada masalah dunia nyata yang Anda hadapi.
Tetapi hal yang perlu diambil adalah: Anda dapat melakukan banyak hal dengan komposisi dan polimorfisme.

back2dos
sumber
1

Apa yang salah dengan yang jelas

if(!FileExists(file)) {
    DefaultAction();
    return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}        
DoSomething(contents);

Sepertinya cukup standar bagi saya? Untuk prosedur besar semacam itu di mana banyak hal kecil harus terjadi, kegagalan yang mana pun akan mencegah yang terakhir. Pengecualian membuatnya sedikit lebih bersih jika itu pilihan.

Steve Bennett
sumber
0

Saya tahu bahwa ini adalah pertanyaan lama, tetapi saya perhatikan pola yang belum disebutkan; terutama, menetapkan variabel untuk kemudian menentukan metode yang ingin Anda panggil (di luar if ... else ...).

Ini sama seperti sudut lain yang harus dilihat untuk membuat kode lebih mudah untuk dikerjakan. Ini juga memungkinkan ketika Anda mungkin ingin menambahkan metode lain untuk dipanggil atau mengubah metode yang sesuai yang perlu dipanggil dalam situasi tertentu.

Daripada harus mengganti semua penyebutan metode (dan mungkin melewatkan beberapa skenario), mereka semua tercantum di akhir blok if ... else ... dan lebih mudah dibaca dan diubah. Saya cenderung menggunakan ini ketika misalnya, beberapa metode dapat dipanggil, tetapi di dalam bersarang jika ... lain ... metode dapat dipanggil dalam beberapa pertandingan.

Jika Anda menetapkan variabel yang menentukan status, Anda bisa memiliki banyak opsi yang bersarang secara mendalam, dan memperbarui status saat sesuatu akan dilakukan (atau tidak dilakukan).

Ini dapat digunakan seperti dalam contoh yang diajukan dalam pertanyaan di mana kami memeriksa apakah 'DoSomething' telah terjadi, dan jika tidak, lakukan tindakan default. Atau Anda dapat meminta negara untuk setiap metode yang ingin Anda panggil, atur jika berlaku, lalu panggil metode yang berlaku di luar ... jika ... lain ...

Di akhir pernyataan bersarang jika ... lain ..., Anda memeriksa status dan bertindak sesuai. Ini berarti Anda hanya perlu menyebutkan satu metode saja daripada semua lokasi yang harus diterapkan.

bool ActionDone = false;

if (Method_1(object_A)) // Test 1
{
    result_A = Method_2(object_A); // Result 1

    if (Method_3(result_A)) // Test 2
    {
        Method_4(result_A); // Action 1
        ActionDone = true;
    }
}

if (!ActionDone)
{
    Method_5(); // Default Action
}
Steve Padmore
sumber
0

Untuk mengurangi IF bersarang:

1 / pengembalian awal;

2 / ekspresi majemuk (sirkuit pendek sadar)

Jadi, contoh Anda dapat di refactored seperti ini:

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
    return;
}
DefaultAction();
DQ_vn
sumber
0

Saya melihat banyak contoh dengan "kembali" yang saya gunakan juga, tetapi kadang-kadang saya ingin menghindari membuat fungsi baru dan menggunakan loop:

while (1) {
    if (FileExists(file)) {
        contents = OpenFile(file);
        if (SomeTest(contents)) {
           DoSomething(contents);
           break;
        } 
    }
    DefaultAction();
    break;
}

Jika Anda ingin menulis lebih sedikit baris atau Anda membenci loop tak terbatas seperti saya, Anda dapat mengubah jenis loop menjadi "do ... while (0)" dan hindari "break" terakhir.

XzKto
sumber
0

Bagaimana dengan solusi ini:

content = NULL; //I presume OpenFile returns a pointer 
if(FileExists(file))
    contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
    DoSomething(contents);
else
    DefaultAction();

Saya membuat asumsi bahwa OpenFile mengembalikan sebuah pointer, tetapi ini bisa bekerja juga dengan tipe nilai kembali dengan menetapkan beberapa nilai default tidak dapat dikembalikan (kode kesalahan atau sesuatu seperti itu).

Tentu saja saya tidak mengharapkan beberapa tindakan yang mungkin melalui metode SomeTest pada pointer NULL (tetapi Anda tidak pernah tahu), jadi ini bisa juga dilihat sebagai pemeriksaan tambahan untuk pointer NULL untuk panggilan SomeTest (isi).

chedi
sumber
0

Jelas, solusi yang paling elegan dan ringkas adalah menggunakan makro preprosesor.

#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }

Yang memungkinkan Anda untuk menulis kode yang indah seperti ini:

if(FileExists(file))
{
    contents = OpenFile(file);
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    DOUBLE_ELSE(DefaultAction();)

Mungkin sulit untuk mengandalkan pemformatan otomatis jika Anda sering menggunakan teknik ini, dan beberapa IDE mungkin sedikit berteriak kepada Anda tentang apa yang salah duga. Dan seperti kata pepatah, semuanya merupakan tradeoff, tapi saya kira itu bukan harga yang buruk untuk membayar untuk menghindari kejahatan kode berulang.

Peter Olson
sumber
Bagi sebagian orang, dan dalam beberapa bahasa, macro preprocessor adalah kode jahat :)
Benjol
@ Benjol Anda bilang Anda terbuka pada saran jahat, bukan? ;)
Peter Olson
ya, tentu saja, itu hanya wrt Anda "menghindari kejahatan" :)
Benjol
4
Ini sangat mengerikan, saya hanya perlu mengunggahnya: D
back2dos
Shirley, kamu tidak serius !!!!!!
Jim In Texas
-1

Karena Anda bertanya karena penasaran, dan pertanyaan Anda tidak ditandai dengan bahasa tertentu (meskipun jelas Anda memiliki bahasa yang sangat penting), mungkin ada baiknya menambahkan bahwa bahasa yang mendukung evaluasi malas memungkinkan untuk pendekatan yang sama sekali berbeda. Dalam bahasa-bahasa itu, ekspresi hanya dievaluasi saat dibutuhkan, sehingga Anda dapat mendefinisikan "variabel" dan menggunakannya hanya ketika masuk akal untuk melakukannya. Misalnya, dalam bahasa fiksi dengan malas let/ instruktur Anda lupa tentang kontrol aliran dan tulis:

let
  contents = ReadFile(file)
in
  if FileExists(file) && SomeTest(contents) 
    DoSomething(contents)
  else 
    DefaultAction()
tokland
sumber