Apakah kita perlu memvalidasi seluruh penggunaan modul atau hanya argumen metode publik?

9

Saya pernah mendengar bahwa disarankan untuk memvalidasi argumen metode publik:

Motivasi bisa dimengerti. Jika modul akan digunakan dengan cara yang salah, kami ingin membuang pengecualian segera alih-alih perilaku yang tidak terduga.

Yang menggangguku, adalah bahwa argumen yang salah bukanlah satu-satunya kesalahan yang dapat dibuat saat menggunakan modul. Inilah beberapa skenario kesalahan di mana kita perlu menambahkan logika pemeriksaan jika kita mengikuti rekomendasi dan tidak ingin eskalasi kesalahan:

  • Panggilan masuk - argumen yang tidak terduga
  • Panggilan masuk - modul dalam kondisi yang salah
  • Panggilan eksternal - hasil yang tidak terduga dikembalikan
  • Panggilan eksternal - efek samping yang tidak terduga (masuk dua kali ke modul panggilan, melanggar keadaan dependensi lainnya)

Saya sudah mencoba memperhitungkan semua kondisi ini dan menulis modul sederhana dengan satu metode (maaf, bukan-C # guys):

public sealed class Room
{
    private readonly IDoorFactory _doorFactory;
    private bool _entered;
    private IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        if (doorFactory == null)
            throw new ArgumentNullException("doorFactory");
        _doorFactory = doorFactory;
    }
    public void Open()
    {
        if (_door != null)
            throw new InvalidOperationException("Room is already opened");
        if (_entered)
            throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;
        _door = _doorFactory.Create();
        if (_door == null)
            throw new IncompatibleDependencyException("doorFactory");
        _door.Open();
        _entered = false;
    }
}

Sekarang Aman =)

Cukup menyeramkan. Tapi bayangkan betapa menyeramkannya dalam modul nyata dengan lusinan metode, keadaan kompleks dan banyak panggilan eksternal (hai, pecinta injeksi ketergantungan!). Perhatikan bahwa jika Anda memanggil modul yang perilakunya dapat diganti (kelas tidak disegel dalam C #) maka Anda membuat panggilan eksternal dan konsekuensinya tidak dapat diprediksi pada ruang lingkup pemanggil.

Kesimpulannya, apa jalan yang benar dan mengapa? Jika Anda dapat memilih dari opsi di bawah ini, tolong jawab pertanyaan tambahan.

Periksa seluruh penggunaan modul. Apakah kita perlu tes unit? Apakah ada contoh kode seperti itu? Haruskah ketergantungan injeksi dibatasi dalam penggunaan (karena akan menyebabkan lebih banyak memeriksa logika)? Apakah tidak praktis untuk memindahkan cek ke waktu debug (tidak termasuk dalam rilis)?

Periksa hanya argumen. Dari pengalaman saya, pemeriksaan argumen - terutama pemeriksaan nol - adalah pemeriksaan yang paling tidak efektif, karena kesalahan argumen jarang mengarah pada kesalahan kompleks dan eskalasi kesalahan. Sebagian besar waktu Anda akan mendapatkan NullReferenceExceptiondi baris berikutnya. Jadi mengapa cek argumen begitu istimewa?

Jangan periksa penggunaan modul. Pendapat ini sangat tidak populer, dapatkah Anda menjelaskan mengapa?

astef
sumber
Pemeriksaan harus dilakukan selama penugasan lapangan untuk memastikan invarian disimpan.
Basilev
@ Basilevs Menarik ... Apakah itu dari ideologi Kontrak Kode atau sesuatu yang lebih tua? Bisakah Anda merekomendasikan sesuatu untuk dibaca (terkait dengan komentar Anda)?
astef
Ini adalah pemisahan dasar dari kekhawatiran. Semua kasus Anda dibahas, sementara duplikasi kode minimal dan tanggung jawab didefinisikan dengan baik.
Basilevs
@ Basilevs Jadi, jangan periksa perilaku modul lain sama sekali, tetapi periksa invarian negara bagian Anda. Kedengarannya masuk akal. Tetapi mengapa saya tidak melihat tanda terima sederhana ini dalam pertanyaan terkait tentang pemeriksaan argumen?
astef
Yah, beberapa pemeriksaan behevorial masih diperlukan, tetapi mereka hanya harus dilakukan pada nilai yang benar-benar digunakan, bukan yang diteruskan di tempat lain. Misalnya, Anda mengandalkan implementasi daftar untuk memeriksa kesalahan OOB, bukan memeriksa indeks dalam kode klien. Biasanya itu adalah kegagalan kerangka kerja tingkat rendah dan tidak perlu dipancarkan secara manual.
Basilev

Jawaban:

2

TL; DR: Validasi perubahan negara, andalkan [validitas dari] kondisi saat ini.

Di bawah ini saya hanya mempertimbangkan verifikasi yang diaktifkan rilis. Penegasan aktif hanya Debug adalah bentuk dokumentasi, yang berguna dengan caranya sendiri dan di luar ruang lingkup untuk pertanyaan ini.

Pertimbangkan prinsip-prinsip berikut:

  • Akal sehat
  • Gagal cepat
  • KERING
  • SRP

Definisi

  • Komponen - unit yang menyediakan API
  • Klien - pengguna API komponen

Keadaan yang bisa berubah

Masalah

Dalam bahasa imperatif, gejala kesalahan dan penyebabnya dapat dipisahkan oleh jam-jam angkat berat. Korupsi negara dapat menyembunyikan dirinya dan bermutasi untuk menghasilkan kegagalan yang tidak dapat dijelaskan, karena inspeksi negara saat ini tidak dapat mengungkapkan proses korupsi penuh dan, karenanya, asal mula kesalahan.

Larutan

Setiap perubahan negara harus dibuat dan diverifikasi dengan cermat. Salah satu cara untuk berurusan dengan keadaan yang bisa berubah adalah menjaganya agar tetap minimum. Ini dicapai dengan:

  • jenis sistem (deklarasi const dan anggota akhir)
  • memperkenalkan invarian
  • memverifikasi setiap perubahan status komponen melalui API publik

Saat memperluas status komponen, pertimbangkan untuk melakukannya dengan membiarkan kompiler memaksakan imutabilitas data baru. Juga, tegakkan setiap kendala runtime yang masuk akal, membatasi status potensial yang dihasilkan ke set terkecil yang terdefinisi dengan baik.

Contoh

// Wrong
class Natural {
    private int number;
    public Natural(int number) {
        this.number = number;
    }
    public int getInt() {
      if (number < 1)
          throw new InvalidOperationException();
      return number;
    }
}

// Right
class Natural {
    private readonly int number;
    /**
     * @param number - positive number
     */
    public Natural(int number) {
      // Going to modify state, verification is required
      if (number < 1)
        throw new ArgumentException("Natural number should be  positive: " + number);
      this.number = number;
    }
    public int getInt() {
      // State is guaranteed by construction and compiler
      return number;
    }
}

Kohesi pengulangan dan tanggung jawab

Masalah

Memeriksa prasyarat dan kondisi operasi pasca-mengarah pada duplikasi kode verifikasi di kedua klien dan komponen. Memvalidasi permintaan komponen sering kali memaksa klien untuk mengambil sebagian tanggung jawab komponen.

Larutan

Andalkan komponen untuk melakukan verifikasi keadaan bila memungkinkan. Komponen harus menyediakan API yang tidak memerlukan verifikasi penggunaan khusus (verifikasi argumen atau penegakan urutan operasi, misalnya) untuk menjaga status komponen agar terdefinisi dengan baik. Mereka berkewajiban untuk memverifikasi argumen permohonan API sebagaimana diperlukan, melaporkan kegagalan dengan cara yang diperlukan, dan berusaha untuk mencegah korupsi negara mereka.

Klien harus mengandalkan komponen untuk memverifikasi penggunaan API mereka. Tidak hanya pengulangan yang dihindari, klien tidak lagi bergantung pada detail implementasi komponen tambahan. Pertimbangkan kerangka kerja sebagai komponen. Hanya tulis kode verifikasi khusus ketika invarian komponen tidak cukup ketat atau untuk merangkum pengecualian komponen sebagai detail implementasi.

Jika suatu operasi tidak mengubah keadaan dan tidak dicakup oleh verifikasi perubahan keadaan, verifikasi setiap argumen pada tingkat sedalam mungkin.

Contoh

class Store {
  private readonly List<int> slots = new List<int>();
  public void putToSlot(int slot, int data) {
    if (slot < 0 || slot >= slots.Count) // Unnecessary, validated by List, only needed for custom error message
      throw new ArgumentException("data");
    slots[slot] = data;
  }
}

class Natural {
   int _number;
   public Natural(int number) {
       if (number < 1)
          number = 1;  //Wrong: client can't rely on argument verification, additional state uncertainity is introduced.  Right: throw new ArgumentException(number);
       _number = number;
   }
}

Menjawab

Ketika prinsip yang dijelaskan diterapkan pada contoh yang dipermasalahkan, kita mendapatkan:

public sealed class Room
{
    private bool _entered = false;
    // Do not use lazy instantiation if not absolutely necessary, this introduces additional mutable state
    private readonly IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        // Rely on system null check
        IDoor door = _doorFactory.Create();
        // Modifying own state, verification is required
        if (door == null)
           throw new ArgumentNullException("Door");
        _door = door;
    }
    public void Enter()
    {
        // Room invariants do not guarantee _entered value. Door state is indirectly a part of our state. Verification is required to prevent second door state change below.
        if (_entered)
           throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;     
        // rely on immutability for _door field to be non-null
        // rely on door implementation to control resulting door state       
        _door.Open();            
    }
}

Ringkasan

Status Klien terdiri dari nilai bidangnya sendiri dan bagian dari status komponen yang tidak dicakup oleh invariannya sendiri. Verifikasi hanya boleh dilakukan sebelum keadaan aktual klien berubah.

Basilevs
sumber
1

Kelas bertanggung jawab atas negaranya sendiri. Jadi validasi sejauh itu membuat atau menempatkan hal-hal dalam keadaan yang dapat diterima.

Jika modul akan digunakan dengan cara yang salah, kami ingin membuang pengecualian segera alih-alih perilaku yang tidak terduga.

Tidak, jangan melemparkan pengecualian, alih-alih berikan perilaku yang dapat diprediksi. Akibat wajar dari tanggung jawab negara adalah menjadikan kelas / aplikasi sebagai toleran dan praktis. Misalnya, meneruskan nullke aCollection.Add()? Hanya saja, jangan tambahkan dan teruskan. Anda mendapatkan nullinput untuk membuat objek? Buat objek nol atau objek default. Di atas, doorsudah open? Jadi apa, teruskan. DoorFactoryargumen adalah nol? Buat yang baru. Ketika saya membuat, enumsaya selalu memiliki Undefinedanggota. Saya menggunakan liberal Dictionarydan enumsmendefinisikan hal-hal secara eksplisit; dan ini sangat membantu dalam memberikan perilaku yang dapat diprediksi.

(Hai, pecinta injeksi ketergantungan!)

Ya meskipun saya berjalan melalui bayangan lembah parameter saya tidak akan takut argumen. Untuk sebelumnya saya juga menggunakan parameter default dan opsional sebanyak mungkin.

Semua hal di atas memungkinkan pemrosesan internal untuk terus berjalan. Dalam aplikasi tertentu saya memiliki banyak metode di beberapa kelas dengan hanya satu tempat di mana pengecualian dilemparkan. Bahkan kemudian, itu bukan karena argumen nol atau bahwa saya tidak dapat melanjutkan memprosesnya karena kode akhirnya menciptakan objek "non-fungsional" / "null".

sunting

mengutip komentar saya secara keseluruhan. Saya pikir desain tidak hanya "menyerah" ketika bertemu 'nol'. Terutama menggunakan objek komposit.

Kami lupa konsep / asumsi utama di sini - encapsulation& single responsibility. Hampir tidak ada pemeriksaan nol setelah lapisan pertama yang berinteraksi dengan klien. Kode ini toleran kuat. Kelas dirancang dengan status default dan berfungsi tanpa ditulis seolah-olah kode yang berinteraksi adalah bug-ridden, runk junk. Orang tua komposit tidak harus menjangkau lapisan anak-anak untuk mengevaluasi validitas (dan implikasinya, periksa nol di semua sudut dan celah). Orang tua tahu apa arti status default anak

akhiri edit

radarbob
sumber
1
Tidak menambahkan elemen koleksi yang tidak valid adalah perilaku yang sangat tidak terduga.
Basilevs
1
Jika semua antarmuka akan dirancang sedemikian toleran, suatu hari, karena kesalahan dangkal, program akan secara tidak sengaja membangunkan dan menghancurkan umat manusia.
astef
Kami lupa konsep / asumsi utama di sini - encapsulation& single responsibility. Hampir tidak ada nullpengecekan setelah lapisan pertama yang berinteraksi dengan klien. Kode <strike> toleran </strike> kuat. Kelas dirancang dengan status default dan berfungsi tanpa ditulis seolah-olah kode yang berinteraksi adalah bug-ridden, runk junk. Orang tua komposit tidak harus menjangkau lapisan anak-anak untuk mengevaluasi validitas (dan implikasinya, periksa nullsemua sudut dan celah). Orangtua tahu apa arti status default anak
radarbob