Validasi parameter input pada pemanggil: duplikasi kode?

16

Di mana tempat terbaik untuk memvalidasi parameter input fungsi: di pemanggil atau dalam fungsi itu sendiri?

Karena saya ingin meningkatkan gaya pengkodean saya, saya mencoba menemukan praktik terbaik atau beberapa aturan untuk masalah ini. Kapan dan apa yang lebih baik.

Dalam proyek saya sebelumnya, kami biasa memeriksa dan memperlakukan setiap parameter input di dalam fungsi, (misalnya jika tidak nol). Sekarang, saya telah membaca di sini dalam beberapa jawaban dan juga dalam buku Programmer Pragmatis, bahwa validasi parameter input adalah tanggung jawab pemanggil.

Jadi itu berarti, bahwa saya harus memvalidasi parameter input sebelum memanggil fungsi. Di mana-mana fungsi dipanggil. Dan itu menimbulkan satu pertanyaan: bukankah itu membuat duplikasi kondisi pemeriksaan di mana fungsi dipanggil?

Saya tidak tertarik hanya dalam kondisi nol, tetapi dalam validasi variabel input apa pun (nilai negatif ke sqrt berfungsi, bagi dengan nol, kombinasi salah kode negara dan kode ZIP, atau apa pun)

Apakah ada beberapa aturan bagaimana memutuskan di mana memeriksa kondisi input?

Saya sedang memikirkan beberapa argumen:

  • ketika memperlakukan variabel yang tidak valid dapat bervariasi, baik untuk memvalidasinya di sisi pemanggil (misalnya sqrt()fungsi - dalam beberapa kasus saya mungkin ingin bekerja dengan nomor kompleks, jadi saya memperlakukan kondisi di pemanggil)
  • ketika kondisi cek sama di setiap penelepon, lebih baik periksa di dalam fungsi, untuk menghindari duplikasi
  • validasi parameter input pada pemanggil berlangsung hanya satu sebelum memanggil banyak fungsi dengan parameter ini. Oleh karena itu validasi parameter di setiap fungsi tidak efektif
  • solusi yang tepat tergantung pada kasus tertentu

Saya harap pertanyaan ini bukan duplikat dari yang lain, saya mencari masalah ini dan saya menemukan pertanyaan serupa tetapi mereka tidak menyebutkan secara persis kasus ini.

srnka
sumber

Jawaban:

15

Tergantung. Memutuskan di mana harus memasukkan validasi harus didasarkan pada uraian dan kekuatan kontrak yang tersirat (atau didokumentasikan) dengan metode ini. Validasi adalah cara yang baik untuk meningkatkan kepatuhan terhadap kontrak tertentu. Jika karena alasan apa pun metode ini memiliki kontrak yang sangat ketat, maka ya, terserah Anda untuk memeriksanya sebelum menelepon.

Ini adalah konsep yang sangat penting ketika Anda membuat metode publik , karena pada dasarnya Anda beriklan bahwa beberapa metode melakukan beberapa operasi. Lebih baik melakukan apa yang Anda katakan!

Ambil metode berikut sebagai contoh:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

Apa kontrak yang tersirat DeletePerson? Programmer hanya dapat berasumsi bahwa jika ada Personyang lewat, itu akan dihapus. Namun, kita tahu bahwa ini tidak selalu benar. Bagaimana jika psuatu nullnilai? Bagaimana jika ptidak ada dalam database? Bagaimana jika basis data terputus? Karenanya, DeletePerson tampaknya tidak memenuhi kontraknya dengan baik. Kadang-kadang, itu menghapus seseorang, dan kadang-kadang ia melempar NullReferenceException, atau DatabaseNotConnectedException, atau kadang-kadang tidak melakukan apa-apa (seperti jika orang tersebut sudah dihapus).

API seperti ini terkenal sulit digunakan, karena ketika Anda menyebut ini "kotak hitam" dari suatu metode, segala macam hal buruk bisa terjadi.

Berikut adalah beberapa cara Anda dapat meningkatkan kontrak:

  • Tambahkan validasi dan tambahkan pengecualian pada kontrak. Ini membuat kontrak lebih kuat , tetapi mengharuskan pemanggil melakukan validasi. Namun perbedaannya adalah sekarang mereka tahu persyaratannya. Dalam hal ini saya mengkomunikasikan ini dengan komentar C # XML, tetapi Anda bisa menambahkan throws(Java), menggunakan Assert, atau menggunakan alat kontrak seperti Kontrak Kode.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    Catatan: Argumen yang menentang gaya ini sering menyebabkan pra-validasi berlebihan oleh semua kode panggilan, tetapi menurut pengalaman saya, ini sering tidak terjadi. Pikirkan skenario di mana Anda mencoba untuk menghapus Orang nol. Bagaimana itu bisa terjadi? Dari mana Orang null itu berasal? Jika ini adalah UI, misalnya, mengapa tombol Delete ditangani jika tidak ada pilihan saat ini? Jika sudah dihapus, bukankah seharusnya sudah dihapus dari tampilan? Jelas ada pengecualian untuk ini, tetapi ketika sebuah proyek tumbuh Anda akan sering berterima kasih kode seperti ini untuk mencegah bug menyebar jauh ke dalam sistem.

  • Tambahkan validasi dan kode secara defensif. Ini membuat kontrak lebih longgar , karena sekarang metode ini lebih dari sekadar menghapus orang. Saya mengubah nama metode untuk mencerminkan ini, tetapi mungkin tidak diperlukan jika Anda konsisten dalam API Anda. Pendekatan ini memiliki pro dan kontra. Pro adalah bahwa Anda sekarang dapat memanggil TryDeletePersonlewat semua jenis input tidak valid dan tidak pernah khawatir tentang pengecualian. Yang pasti, tentu saja, adalah bahwa pengguna kode Anda mungkin akan memanggil metode ini terlalu banyak, atau mungkin membuat debugging sulit dalam kasus di mana p adalah nol. Ini bisa dianggap pelanggaran ringan terhadap Prinsip Tanggung Jawab Tunggal , jadi ingatlah itu jika perang api meletus.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Gabungkan pendekatan. Kadang-kadang Anda ingin sedikit dari keduanya, di mana Anda ingin penelepon eksternal mengikuti aturan dengan cermat (untuk memaksa mereka bertanggung jawab atas kode), tetapi Anda ingin kode pribadi Anda fleksibel.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

Dalam pengalaman saya, berkonsentrasi pada kontrak yang Anda implikasikan daripada aturan yang keras adalah yang terbaik. Pengkodean defensif tampaknya berfungsi lebih baik jika penelepon sulit atau sulit menentukan apakah suatu operasi valid atau tidak. Kontrak ketat tampaknya berfungsi lebih baik di mana Anda mengharapkan penelepon hanya membuat panggilan metode ketika mereka benar-benar masuk akal.

Kevin McCormick
sumber
Terima kasih atas jawaban yang sangat bagus dengan contoh. Saya suka pendekatan "defensif" dan "kontrak ketat".
srnka
7

Ini masalah konvensi, dokumentasi dan kasus penggunaan.

Tidak semua fungsi sama. Tidak semua persyaratan sama. Tidak semua validasi sama.

Misalnya, jika proyek Java Anda mencoba menghindari null pointer kapan pun memungkinkan (lihat rekomendasi gaya Guava , misalnya), apakah Anda masih memvalidasi setiap argumen fungsi untuk memastikan itu bukan nol? Ini mungkin tidak perlu, tetapi kemungkinan Anda masih melakukannya, untuk membuatnya lebih mudah menemukan bug. Tapi Anda bisa menggunakan pernyataan di mana Anda sebelumnya melemparkan NullPointerException.

Bagaimana jika proyek ini dalam C ++? Konvensi / tradisi dalam C ++ adalah untuk mendokumentasikan prasyarat, tetapi hanya memverifikasi mereka (jika sama sekali) di build debug.

Dalam kedua kasus, Anda memiliki prasyarat yang terdokumentasi pada fungsi Anda: tidak ada argumen yang bisa null. Sebagai gantinya, Anda dapat memperluas domain fungsi untuk memasukkan nulls dengan perilaku yang ditentukan, misalnya "jika ada argumen yang null, berikan pengecualian". Tentu saja, itu lagi warisan C ++ saya yang berbicara di sini - di Jawa, cukup umum untuk mendokumentasikan prasyarat dengan cara ini.

Tetapi tidak semua prasyarat bahkan dapat diperiksa secara wajar. Sebagai contoh, algoritma pencarian biner memiliki prasyarat bahwa urutan yang akan dicari harus diurutkan. Tetapi memverifikasi bahwa itu pasti adalah operasi O (N), jadi melakukan itu pada setiap panggilan agak mengalahkan titik menggunakan algoritma O (log (N)) di tempat pertama. Jika Anda memprogram defensif, Anda dapat melakukan pengecekan yang lebih rendah (mis. Memverifikasi bahwa untuk setiap partisi yang Anda cari, nilai awal, pertengahan dan akhir diurutkan), tetapi itu tidak menangkap semua kesalahan. Biasanya, Anda hanya harus bergantung pada prasyarat yang dipenuhi.

Satu-satunya tempat nyata di mana Anda memerlukan pemeriksaan eksplisit adalah pada batas. Input eksternal ke proyek Anda? Validasi, validasikan, validasikan. Area abu-abu adalah batas API. Itu benar-benar tergantung pada seberapa besar Anda ingin mempercayai kode klien, berapa banyak kerusakan input yang tidak valid tidak, dan berapa banyak bantuan yang ingin Anda berikan dalam menemukan bug. Setiap batasan hak istimewa harus dihitung sebagai eksternal, tentu saja - syscall, misalnya, dijalankan dalam konteks privilege yang tinggi dan karenanya harus sangat hati-hati untuk memvalidasi. Setiap validasi semacam itu tentu saja harus internal ke syscall.

Sebastian Redl
sumber
Terima kasih atas jawaban anda. Bisakah Anda, tolong, berikan tautan ke rekomendasi gaya Jambu? Saya tidak bisa google dan mencari tahu apa yang Anda maksud dengan itu. +1 untuk memvalidasi batas.
srnka
Tautan yang ditambahkan. Ini sebenarnya bukan panduan gaya penuh, hanya bagian dari dokumentasi utilitas non-null.
Sebastian Redl
6

Validasi parameter harus menjadi perhatian fungsi yang dipanggil. Fungsi harus tahu apa yang dianggap sebagai input yang valid dan apa yang tidak. Penelepon mungkin tidak mengetahui hal ini, terutama ketika mereka tidak tahu bagaimana fungsi diimplementasikan secara internal. Fungsi ini diharapkan menangani kombinasi nilai parameter apa pun dari penelepon.

Karena fungsi bertanggung jawab untuk memvalidasi parameter, Anda dapat menulis unit test terhadap fungsi ini untuk memastikan berperilaku sebagaimana dimaksud dengan nilai parameter yang valid dan tidak valid.

Bernard
sumber
Terima kasih atas jawabannya. Jadi menurut Anda, fungsi itu harus memeriksa parameter input yang valid dan tidak valid dalam setiap kasus. Sesuatu yang berbeda dari penegasan buku Pragmatic Programmer: "validasi parameter input adalah tanggung jawab pemanggil". Ini adalah pemikiran yang bagus "Fungsi harus tahu apa yang dianggap valid ... Penelepon mungkin tidak tahu ini" ... Jadi Anda tidak suka menggunakan pra-kondisi?
srnka
1
Anda dapat menggunakan pra-kondisi jika Anda mau (lihat jawaban Sebastian ), tetapi saya lebih suka bersikap defensif dan menangani segala macam input yang mungkin.
Bernard
4

Dalam fungsi itu sendiri. Jika fungsi digunakan lebih dari sekali, Anda tidak ingin memverifikasi parameter untuk setiap panggilan fungsi.

Selain itu, jika fungsi diperbarui sedemikian rupa sehingga akan memengaruhi validasi parameter, Anda harus mencari setiap kemunculan validasi pemanggil untuk memperbaruinya. Itu tidak indah :-).

Anda bisa merujuk ke Klausul Penjaga

Memperbarui

Lihat balasan saya untuk setiap skenario yang Anda berikan.

  • ketika memperlakukan variabel yang tidak valid dapat bervariasi, baik untuk memvalidasinya di sisi pemanggil (misalnya sqrt()fungsi - dalam beberapa kasus saya mungkin ingin bekerja dengan nomor kompleks, jadi saya memperlakukan kondisi di pemanggil)

    Menjawab

    Mayoritas bahasa pemrograman mendukung bilangan bulat dan bilangan asli secara default, bukan bilangan kompleks, karenanya penerapannya sqrthanya menerima angka non-negatif. Satu-satunya kasus Anda memiliki sqrtfungsi yang mengembalikan bilangan kompleks adalah ketika Anda menggunakan bahasa pemrograman yang berorientasi pada matematika, seperti Mathematica

    Selain itu, sqrtuntuk sebagian besar bahasa pemrograman sudah diterapkan, maka Anda tidak dapat memodifikasinya, dan jika Anda mencoba untuk mengganti implementasi (lihat tambalan monyet), maka kolaborator Anda akan sangat terkejut mengapa sqrttiba - tiba menerima angka negatif.

    Jika Anda menginginkannya, Anda dapat membungkusnya dengan sqrtfungsi kustom Anda yang menangani angka negatif dan mengembalikan angka kompleks.

  • ketika kondisi cek sama di setiap penelepon, lebih baik periksa di dalam fungsi, untuk menghindari duplikasi

    Menjawab

    Ya, ini adalah praktik yang baik untuk menghindari hamburan validasi parameter di kode Anda.

  • validasi parameter input pada pemanggil berlangsung hanya satu sebelum memanggil banyak fungsi dengan parameter ini. Oleh karena itu validasi parameter di setiap fungsi tidak efektif

    Menjawab

    Akan lebih baik jika penelepon adalah fungsi, bukan begitu?

    Jika fungsi dalam pemanggil digunakan oleh pemanggil lain, apa yang mencegah Anda memvalidasi parameter dalam fungsi yang dipanggil oleh pemanggil?

  • solusi yang tepat tergantung pada kasus tertentu

    Menjawab

    Bertujuan untuk kode yang dapat dipelihara. Memindahkan validasi parameter Anda memastikan satu sumber kebenaran tentang fungsi yang dapat diterima atau tidak.

OnesimusUnbound
sumber
Terima kasih atas jawabannya. Sqrt () hanyalah sebuah contoh, perilaku yang sama dengan parameter input dapat digunakan oleh banyak fungsi lainnya. "jika fungsi diperbarui sedemikian rupa sehingga akan memengaruhi validasi parameter, Anda harus mencari setiap kemunculan validasi pemanggil" - Saya tidak setuju dengan ini. Kita kemudian dapat mengatakan hal yang sama untuk nilai kembali: jika fungsi diperbarui sedemikian rupa sehingga akan mempengaruhi nilai balik, Anda harus memperbaiki setiap penelepon ... Saya pikir fungsi harus memiliki satu tugas yang didefinisikan dengan baik untuk dilakukan ... Jika tidak perubahan penelepon tetap diperlukan.
srnka
2

Suatu fungsi harus menyatakan kondisi sebelum dan sesudahnya.
Pra-kondisi adalah kondisi yang harus dipenuhi oleh pemanggil sebelum dapat menggunakan fungsi dengan benar dan dapat (dan sering dilakukan) memasukkan validitas parameter input.
Post-condition adalah janji yang dibuat oleh fungsi untuk peneleponnya.

Ketika validitas parameter fungsi adalah bagian dari pra-kondisi, maka penelepon bertanggung jawab untuk memastikan parameter tersebut valid. Tetapi itu tidak berarti setiap penelepon harus secara eksplisit memeriksa setiap parameter sebelum panggilan. Dalam kebanyakan kasus, tidak diperlukan tes eksplisit karena logika internal dan prasyarat pemanggil sudah memastikan bahwa parameternya valid.

Sebagai langkah keamanan terhadap kesalahan pemrograman (bug), Anda dapat memeriksa bahwa parameter yang dilewatkan ke fungsi benar-benar memenuhi pra-kondisi yang dinyatakan. Karena tes ini bisa mahal, itu ide yang baik untuk dapat mematikannya untuk rilis rilis. Jika tes ini gagal, maka program harus dihentikan, karena telah terbukti mengalami bug.

Meskipun pada pandangan pertama pemeriksaan di pemanggil tampaknya mengundang duplikasi kode, sebenarnya sebaliknya. Pemeriksaan di callee menghasilkan duplikasi kode dan banyak pekerjaan yang tidak dibutuhkan dilakukan.
Coba pikirkan, seberapa sering Anda melewati parameter melalui beberapa lapisan fungsi, hanya membuat perubahan kecil pada beberapa dari mereka di sepanjang jalan. Jika Anda secara konsisten menerapkan metode check-in-callee , masing-masing fungsi perantara tersebut harus melakukan pemeriksaan ulang untuk setiap parameter.
Dan sekarang bayangkan bahwa salah satu dari parameter itu seharusnya adalah daftar yang diurutkan.
Dengan tanda centang pada penelepon, hanya fungsi pertama yang harus memastikan bahwa daftar benar-benar diurutkan. Semua yang lain tahu daftar itu sudah diurutkan (karena itulah yang mereka nyatakan dalam pra-kondisi mereka) dan dapat meneruskannya tanpa pemeriksaan lebih lanjut.

Bart van Ingen Schenau
sumber
+1 Terima kasih atas jawabannya. Refleksi yang bagus: "Pemeriksaan dalam callee menghasilkan duplikasi kode dan banyak pekerjaan yang tidak dibutuhkan dilakukan". Dan dalam kalimat: "Dalam kebanyakan kasus, tidak diperlukan tes eksplisit karena logika internal dan pra-kondisi penelepon sudah memastikan" - apa yang Anda maksud dengan ekspresi "logika internal"? Fungsionalitas DBC?
srnka
@ srnka: Dengan "logika internal" yang saya maksud adalah perhitungan dan keputusan dalam suatu fungsi. Ini pada dasarnya adalah implementasi dari fungsi.
Bart van Ingen Schenau
0

Paling sering Anda tidak bisa tahu siapa, kapan dan bagaimana akan memanggil fungsi yang Anda tulis. Yang terbaik adalah menganggap yang terburuk: fungsi Anda akan dipanggil dengan parameter yang tidak valid. Jadi Anda pasti harus membahasnya.

Namun demikian, jika bahasa yang Anda gunakan mendukung pengecualian, Anda mungkin tidak memeriksa kesalahan tertentu dan memastikan bahwa pengecualian akan dilemparkan, tetapi dalam hal ini Anda harus memastikan untuk menggambarkan kasus dalam dokumentasi (Anda perlu memiliki dokumentasi). Pengecualian akan memberikan penelepon informasi yang cukup tentang apa yang terjadi, dan juga akan mengarahkan perhatian pada argumen yang tidak valid.

superM
sumber
Sebenarnya, mungkin lebih baik untuk memvalidasi parameter, dan, jika parameter tidak valid, buat sendiri pengecualian. Inilah sebabnya: badut-badut yang memanggil rutinitas Anda tanpa repot-repot memastikan bahwa mereka memberikannya data yang valid adalah orang-orang yang sama yang tidak akan repot-repot memeriksa kode pengembalian kesalahan yang menunjukkan mereka memberikan data yang tidak valid. Melempar pengecualian MEMUTUSKAN masalah yang harus diperbaiki.
John R. Strohm