Apakah ekspresi boolean besar lebih mudah dibaca daripada ekspresi yang sama dipecah menjadi metode predikat? [Tutup]

63

Apa yang lebih mudah dipahami, pernyataan boolean besar (cukup kompleks), atau pernyataan yang sama dipecah menjadi metode predikat (banyak kode tambahan untuk dibaca)?

Opsi 1, ekspresi boolean besar:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {

        return propVal.PropertyId == context.Definition.Id
            && !repo.ParentId.HasValue || repo.ParentId == propVal.ParentId
            && ((propVal.SecondaryFilter.HasValue && context.SecondaryFilter.HasValue && propVal.SecondaryFilter.Value == context.SecondaryFilter) || (!context.SecondaryFilter.HasValue && !propVal.SecondaryFilter.HasValue));
    }

Opsi 2, Kondisi dipecah menjadi metode predikat:

    private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
    {
        return MatchesDefinitionId(context, propVal)
            && MatchesParentId(propVal)
            && (MatchedSecondaryFilter(context, propVal) || HasNoSecondaryFilter(context, propVal));
    }

    private static bool HasNoSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);
    }

    private static bool MatchedSecondaryFilter(CurrentSearchContext context, TValToMatch propVal)
    {
        return (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    }

    private bool MatchesParentId(TValToMatch propVal)
    {
        return (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    }

    private static bool MatchesDefinitionId(CurrentSearchContext context, TValToMatch propVal)
    {
        return propVal.PropertyId == context.Definition.Id;
    }

Saya lebih suka pendekatan kedua, karena saya melihat nama metode sebagai komentar, tetapi saya mengerti bahwa itu bermasalah karena Anda harus membaca semua metode untuk memahami apa yang kode lakukan, sehingga abstrak maksud kode.

Willem
sumber
13
Opsi 2 mirip dengan apa yang direkomendasikan Martin Fowler dalam buku refactoring-nya. Ditambah nama metode Anda berfungsi sebagai maksud dari semua ekspresi acak, konten metode hanyalah detail implementasi yang dapat berubah seiring waktu.
programmer
2
Apakah itu benar-benar ungkapan yang sama? "Atau" memiliki prioritas lebih rendah daripada "Dan", Bagaimanapun yang kedua memberitahu niat Anda, yang lain (pertama) adalah teknis.
pembungkus
3
Apa yang dikatakan @thepacker. Fakta bahwa melakukannya dengan cara pertama telah menyebabkan Anda melakukan kesalahan adalah petunjuk yang cukup bagus bahwa cara pertama tidak mudah dimengerti oleh sektor yang sangat penting dari audiens target Anda. Dirimu sendiri!
Steve Jessop
3
Opsi 3: Saya tidak suka keduanya. Yang kedua adalah kata-kata kasar, yang pertama tidak sama dengan yang kedua. Bantuan kurung.
David Hammen
3
Hal ini mungkin menjadi bertele-tele, tetapi Anda tidak memiliki apa pun if pernyataan di salah satu blok kode. Pertanyaan Anda adalah tentang ekspresi Boolean .
Kyle Strand

Jawaban:

88

Apa yang lebih mudah dimengerti

Pendekatan yang terakhir. Ini tidak hanya lebih mudah dimengerti tetapi juga lebih mudah untuk menulis, menguji, refactor dan memperluas juga. Setiap kondisi yang diperlukan dapat dipisahkan secara aman dan ditangani dengan caranya sendiri.

itu bermasalah karena Anda harus membaca semua metode untuk memahami kode

Tidak masalah jika metode diberi nama dengan benar. Bahkan akan lebih mudah dipahami karena nama metode akan menggambarkan maksud dari kondisi.
Untuk seorang penonton if MatchesDefinitionId()lebih jelas daripadaif (propVal.PropertyId == context.Definition.Id)

[Secara pribadi, pendekatan pertama membuat mataku sakit.]

keajaiban
sumber
12
Jika nama metode bagus, maka itu juga lebih mudah dimengerti.
BЈовић
Dan tolong, buat mereka (nama metode) penting dan singkat. 20+ nama metode chars sakit mata saya. MatchesDefinitionId()adalah garis batas.
Mindwin
2
@Mindwin Jika sampai pada pilihan antara menjaga nama metode "pendek" dan membuat mereka bermakna, saya katakan ambil yang terakhir setiap waktu. Pendek itu bagus, tetapi tidak dengan mengorbankan keterbacaan.
Ajedi32
@ Ajedi32 seseorang tidak harus menulis esai tentang apa metode yang dilakukan pada nama metode, atau memiliki nama metode yang terdengar secara gramatik. Jika seseorang membuat standar singkatan jelas (lintas kelompok kerja atau organisasi) tidak akan menjadi masalah dengan nama pendek dan mudah dibaca.
Mindwin
Gunakan hukum Zipf: buat lebih banyak kata untuk mencegah penggunaannya.
hoosierEE
44

Jika ini adalah satu-satunya tempat fungsi predikat ini akan digunakan, Anda juga dapat menggunakan boolvariabel lokal sebagai gantinya:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    bool matchesDefinitionId = (propVal.PropertyId == context.Definition.Id);
    bool matchesParentId = (!repo.ParentId.HasValue || repo.ParentId == propVal.ParentId);
    bool matchesSecondaryFilter = (propVal.SecondaryFilter.HasValue && context.No.HasValue && propVal.SecondaryFilter.Value == context.No);
    bool hasNoSecondaryFilter = (!context.No.HasValue && !propVal.SecondaryFilter.HasValue);

    return matchesDefinitionId
        && matchesParentId
        && matchesSecondaryFilter || hasNoSecondaryFilter;
}

Ini juga dapat dipecah lebih lanjut dan disusun ulang untuk membuatnya lebih mudah dibaca, misalnya dengan

bool hasSecondaryFilter = propVal.SecondaryFilter.HasValue;

dan kemudian mengganti semua instance dari propVal.SecondaryFilter.HasValue. Satu hal yang segera menonjol adalah yang hasNoSecondaryFiltermenggunakan logika AND pada HasValueproperti yang dinegasikan , sementara matchesSecondaryFiltermenggunakan AND yang logis pada yang tidak dinegasikan HasValue- jadi bukan sebaliknya.

Simon Richter
sumber
3
Solusi ini cukup bagus dan saya sudah pasti menulis banyak kode serupa. Ini sangat mudah dibaca. Kelemahannya, dibandingkan dengan solusi yang saya posting, adalah kecepatan. Dengan metode ini, Anda melakukan setumpuk tes bersyarat apa pun yang terjadi. Dalam solusi saya, operasi dapat dikurangi secara dramatis berdasarkan nilai yang diproses.
BuvinJ
5
@ BuvinJ Tes seperti yang ditunjukkan di sini harus cukup murah, jadi kecuali saya tahu beberapa kondisinya mahal atau kecuali ini adalah kode yang sangat sensitif terhadap kinerja, saya akan menggunakan versi yang lebih mudah dibaca.
svick
1
@svick Tidak diragukan lagi, ini hampir tidak mungkin untuk memperkenalkan masalah kinerja. Namun, jika Anda dapat mengurangi operasi tanpa kehilangan keterbacaan, lalu mengapa tidak melakukannya? Saya tidak yakin ini jauh lebih mudah dibaca daripada solusi saya. Itu memberikan "nama" untuk mendokumentasikan diri untuk tes - yang bagus ... Saya pikir itu turun ke kasus penggunaan khusus dan bagaimana dimengerti tes berada dalam hak mereka sendiri.
BuvinJ
Menambahkan komentar juga dapat membantu keterbacaan ...
BuvinJ
@ BuvinJ Apa yang saya sukai dari solusi ini adalah bahwa dengan mengabaikan segalanya kecuali baris terakhir, saya dapat dengan cepat memahami apa yang dilakukannya. Saya pikir ini lebih mudah dibaca.
svick
42

Secara umum, yang terakhir lebih disukai.

Itu membuat situs panggilan lebih dapat digunakan kembali. Ini mendukung KERING (artinya Anda memiliki lebih sedikit tempat untuk berubah ketika kriteria berubah, dan dapat melakukannya dengan lebih andal). Dan sangat sering sub-kriteria itu adalah hal-hal yang akan digunakan kembali secara independen di tempat lain, memungkinkan Anda untuk melakukan itu.

Oh, dan ini membuat hal ini jauh lebih mudah untuk unit test, memberi Anda kepercayaan diri bahwa Anda telah melakukannya dengan benar.

Telastyn
sumber
1
Ya, meskipun jawaban Anda juga harus mengatasi memperbaiki penggunaan repo, yang muncul bidang / properti statis, yaitu variabel global. Metode statika harus deterministik dan tidak menggunakan variabel global.
David Arno
3
@ DavidArno - sementara itu tidak bagus, tampaknya bersinggungan dengan pertanyaan yang ada. Dan tanpa kode lebih masuk akal bahwa ada alasan semi-valid untuk desain untuk bekerja seperti itu.
Telastyn
1
Ya, tidak pernah repo. Saya harus mengaburkan kode sedikit, tidak ingin berbagi kode klien apa adanya di interwebs :)
willem
23

Jika ada di antara dua pilihan ini, maka yang terakhir lebih baik. Namun, ini bukan satu-satunya pilihan! Bagaimana dengan memecah fungsi tunggal menjadi beberapa ifs? Tes cara-cara untuk keluar dari fungsi untuk menghindari tes tambahan, kira-kira meniru "hubungan pendek" dalam tes jalur tunggal.

Ini lebih mudah dibaca (Anda mungkin perlu memeriksa logika untuk contoh Anda, tetapi konsepnya benar):

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    if( propVal.PropertyId != context.Definition.Id ) return false;

    if( repo.ParentId.HasValue || repo.ParentId != propVal.ParentId ) return false;

    if( propVal.SecondaryFilter.HasValue && 
        context.SecondaryFilter.HasValue && 
        propVal.SecondaryFilter.Value == context.SecondaryFilter ) return true;

    if( !context.SecondaryFilter.HasValue && 
        !propVal.SecondaryFilter.HasValue) return true;

    return false;   
}
Terima kasih
sumber
3
Mengapa saya mendapatkan downvote untuk ini dalam beberapa detik setelah mempostingnya? Silakan tambahkan komentar saat Anda downvote! Jawaban ini beroperasi dengan cepat dan lebih mudah dibaca. Jadi apa masalahnya?
BuvinJ
2
@ BunvinJ: Sama sekali tidak ada yang salah dengan itu. Sama seperti kode asli, kecuali Anda tidak harus bertarung dengan selusin tanda kurung dan satu baris yang membentang melewati ujung layar. Saya dapat membaca kode itu dari atas ke bawah dan segera memahaminya. Hitung WTF = 0.
gnasher729
1
Mengembalikan selain dari pada akhir fungsi membuat kode kurang dibaca, tidak lebih mudah dibaca, IMO. Saya lebih suka titik keluar tunggal. Beberapa argumen bagus sama-sama mendukung tautan ini. stackoverflow.com/questions/36707/…
Brad Thomas
5
@Brad thomas saya tidak bisa setuju dengan titik keluar tunggal. Biasanya mengarah ke kurung bersarang yang mendalam. Pengembalian mengakhiri jalan sehingga bagi saya jauh lebih mudah dibaca.
Borjab
1
@BradThomas Saya sepenuhnya setuju dengan Borjab. Menghindari sarang yang dalam sebenarnya mengapa saya menggunakan gaya ini lebih sering daripada untuk memecah pernyataan bersyarat panjang. Saya gunakan untuk menemukan diri saya menulis kode dengan banyak sarang. Kemudian, saya mulai mencari cara untuk hampir tidak pernah masuk lebih dari satu atau dua sarang, dan kode saya telah menjadi JAUH lebih mudah untuk dibaca dan dipelihara sebagai hasilnya. Jika Anda dapat menemukan cara untuk keluar dari fungsi Anda, lakukan sesegera mungkin! Jika Anda dapat menemukan cara untuk menghindari sarang yang dalam dan kondisi lama, lakukanlah!
BuvinJ
10

Saya suka opsi 2 lebih baik, tetapi akan menyarankan satu perubahan struktural. Gabungkan kedua cek pada baris terakhir dari conditional menjadi satu panggilan.

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal)
{
    return MatchesDefinitionId(context, propVal)
        && MatchesParentId(propVal)
        && MatchesSecondaryFilterIfPresent(context, propVal);
}

private static bool MatchesSecondaryFilterIfPresent(CurrentSearchContext context, 
                                                    TValToMatch propVal)
{
    return MatchedSecondaryFilter(context, propVal) 
               || HasNoSecondaryFilter(context, propVal);
}

Alasan saya menyarankan ini adalah bahwa dua pemeriksaan adalah unit fungsional tunggal, dan tanda kurung bersarang dalam kondisi adalah rentan kesalahan: Baik dari sudut pandang awalnya menulis kode dan dari sudut pandang orang yang membacanya. Ini terutama terjadi jika sub-elemen dari ekspresi tidak mengikuti pola yang sama.

Saya tidak yakin apakah MatchesSecondaryFilterIfPresent()itu nama terbaik untuk kombinasi; tetapi tidak ada yang lebih baik segera muncul dalam pikiran.

Dan Neely
sumber
Sangat bagus, mencoba menjelaskan apa yang sedang dilakukan di dalam metode sebenarnya lebih baik daripada hanya merestrukturisasi panggilan.
klaar
2

Meskipun dalam C #, kode tidak terlalu berorientasi objek. Itu menggunakan metode statis dan apa yang tampak seperti bidang statis (misalnya repo). Secara umum diyakini bahwa statika membuat kode Anda sulit untuk diperbaiki dan sulit untuk diuji, sementara menghambat penggunaan kembali, dan, untuk pertanyaan Anda: penggunaan statis seperti ini kurang dapat dibaca & dipelihara daripada konstruksi berorientasi objek.

Anda harus mengonversi kode ini ke bentuk yang lebih berorientasi objek. Ketika Anda melakukannya, Anda akan menemukan bahwa ada tempat yang masuk akal untuk meletakkan kode yang melakukan perbandingan objek, bidang, dll. Kemungkinan Anda kemudian dapat meminta objek untuk membandingkan diri mereka sendiri, yang akan mengurangi besar Anda jika pernyataan ke permintaan sederhana untuk membandingkan (misalnya if ( a.compareTo (b) ) { }, yang dapat mencakup semua perbandingan bidang.)

C # memiliki banyak antarmuka dan utilitas sistem untuk melakukan perbandingan pada objek dan bidangnya. Di luar jelas .Equalsmetode, untuk pemula, melihat ke dalam IEqualityComparer, IEquatabledan utilitas seperti System.Collections.Generic.EqualityComparer.Default.

Erik Eidt
sumber
0

Yang terakhir ini jelas disukai, saya telah melihat kasus dengan cara pertama dan hampir selalu mustahil untuk dibaca. Saya telah melakukan kesalahan dengan melakukannya dengan cara pertama dan diminta untuk mengubahnya menjadi metode predikat.

Mengintip
sumber
0

Saya akan mengatakan bahwa keduanya hampir sama, JIKA Anda menambahkan beberapa ruang kosong untuk dibaca dan beberapa komentar untuk membantu pembaca untuk bagian yang lebih tidak jelas.

Ingat: komentar yang baik memberi tahu pembaca apa yang Anda pikirkan ketika Anda menulis kode.

Dengan perubahan seperti yang saya sarankan, saya mungkin akan pergi dengan pendekatan sebelumnya, karena kurang berantakan dan menyebar. Panggilan subrutin seperti catatan kaki: mereka memberikan informasi yang bermanfaat tetapi mengganggu aliran bacaan. Jika predikatnya lebih kompleks, maka saya akan memecahnya menjadi metode terpisah sehingga konsep yang mereka wujudkan dapat dibangun dalam potongan yang dapat dimengerti.

Mark Wood
sumber
Layak diberi +1. Makanan yang baik untuk dipikirkan, meskipun pendapat tidak populer berdasarkan jawaban yang lain. Terima kasih :)
willem
1
@willem Tidak, itu tidak layak +1. Dua pendekatan tidak sama. Komentar tambahan itu bodoh dan tidak dibutuhkan.
BЈовић
2
Kode yang baik TIDAK PERNAH tergantung pada komentar yang bisa dimengerti. Sebenarnya komentar adalah kekacauan terburuk yang dimiliki sebuah kode. Kode harus berbicara sendiri. Juga, dua pendekatan yang ingin dievaluasi OP tidak akan pernah bisa "hampir sama", tidak peduli berapa banyak spasi putih yang ditambahkan.
wonderbell
Lebih baik memiliki nama fungsi yang bermakna daripada harus membaca komentar. Seperti yang dinyatakan dalam buku "Kode Bersih" komentar adalah kegagalan untuk mengekspresikan kode melempar. Mengapa menjelaskan apa yang Anda lakukan ketika fungsi tersebut dapat menyatakannya dengan lebih jelas.
Borjab
0

Nah, jika ada bagian-bagian yang mungkin ingin Anda gunakan kembali, memisahkannya menjadi fungsi-fungsi yang disebut dengan benar jelas merupakan ide terbaik.
Bahkan jika Anda mungkin tidak pernah menggunakannya kembali, melakukan hal itu memungkinkan Anda untuk menyusun kondisi Anda dengan lebih baik dan memberi mereka label yang menjelaskan apa artinya .

Sekarang, mari kita lihat opsi pertama Anda, dan mengakui bahwa tidak ada lekukan Anda dan pemecah baris semua yang berguna, juga kondisional tidak terstruktur dengan baik:

private static bool ContextMatchesProp(CurrentSearchContext context, TValToMatch propVal) {
    return propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue
        || repo.ParentId == propVal.ParentId
        && propVal.SecondaryFilter.HasValue == context.SecondaryFilter.HasValue
        && (!propVal.SecondaryFilter.HasValue || propVal.SecondaryFilter.Value == context.SecondaryFilter.Value);
}
Deduplicator
sumber
0

Yang pertama benar-benar mengerikan. Anda telah menggunakan || untuk dua hal pada baris yang sama; itu adalah bug dalam kode Anda atau niat untuk mengaburkan kode Anda.

    return (   (   propVal.PropertyId == context.Definition.Id
                && !repo.ParentId.HasValue)
            || (   repo.ParentId == propVal.ParentId
                && (   (   propVal.SecondaryFilter.HasValue
                        && context.SecondaryFilter.HasValue 
                        && propVal.SecondaryFilter.Value == context.SecondaryFilter)
                    || (   !context.SecondaryFilter.HasValue
                        && !propVal.SecondaryFilter.HasValue))));

Setidaknya setengah jalan diformat dengan sopan (jika formatnya rumit, itu karena jika-kondisi rumit), dan Anda setidaknya memiliki kesempatan untuk mencari tahu apakah ada sesuatu di sana omong kosong. Dibandingkan dengan sampah Anda yang diformat jika, ada yang lebih baik. Tetapi Anda tampaknya hanya mampu melakukan ekstrem: Entah kekacauan pernyataan if, atau empat metode yang sama sekali tidak berguna.

Perhatikan bahwa (cond1 && cond2) || (! cond1 && cond3) dapat ditulis sebagai

cond1 ? cond2 : cond3

yang akan mengurangi kekacauan. Saya akan menulis

if (propVal.PropertyId == context.Definition.Id && !repo.ParentId.HasValue) {
    return true;
} else if (repo.ParentId != propVal.ParentId) {
    return false;
} else if (propVal.SecondaryFilter.HasValue) {
    return (   context.SecondaryFilter.HasValue
            && propVal.SecondaryFilter.Value == context.SecondaryFilter); 
} else {
    return !context.SecondaryFilter.HasValue;
}
gnasher729
sumber
-4

Saya tidak suka salah satu dari solusi itu, keduanya sulit untuk dipikirkan, dan sulit dibaca. Abstraksi ke metode yang lebih kecil hanya demi metode yang lebih kecil tidak selalu menyelesaikan masalah.

Idealnya, saya pikir Anda metaprogrmatically membandingkan properti, sehingga Anda tidak memiliki mendefinisikan metode baru atau jika cabang setiap kali Anda ingin membandingkan satu set properti baru.

Saya tidak yakin tentang c #, tetapi dalam javascript sesuatu seperti ini akan JAUH lebih baik dan setidaknya bisa menggantikan MatchesDefinitionId dan MatchesParentId

function compareContextProp(obj, property, value){
  if(obj[property])
    return obj[property] == value
  return false
}
pengguna1152226
sumber
1
Seharusnya tidak menjadi masalah untuk mengimplementasikan sesuatu seperti ini di C #.
Mengintai
Saya tidak melihat bagaimana kombinasi boolean dari ~ 5 panggilan compareContextProp(propVal, "PropertyId", context.Definition.Id)akan lebih mudah dibaca daripada kombinasi boolean dari ~ 5 perbandingan bentuk propVal.PropertyId == context.Definition.Id. Ini secara signifikan lebih lama dan menambahkan lapisan tambahan tanpa benar-benar menyembunyikan kompleksitas dari situs panggilan. (jika itu penting, saya tidak downvote)
Ixrec