Cara melakukan validasi input tanpa pengecualian atau redundansi

11

Ketika saya mencoba membuat antarmuka untuk program tertentu, saya biasanya berusaha menghindari pengecualian yang bergantung pada input yang tidak divalidasi.

Jadi yang sering terjadi adalah saya sudah memikirkan sepotong kode seperti ini (ini hanya contoh demi contoh, jangan pedulikan fungsi yang dijalankannya, contoh di Jawa):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, jadi katakan itu evenSizesebenarnya berasal dari input pengguna. Jadi saya tidak yakin itu adil. Tapi saya tidak ingin memanggil metode ini dengan kemungkinan pengecualian dilemparkan. Jadi saya membuat fungsi berikut:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

tapi sekarang saya punya dua pemeriksaan yang melakukan validasi input yang sama: ekspresi dalam ifpernyataan dan cek eksplisit dalam isEven. Kode duplikat, tidak baik, jadi mari kita refactor:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, itu menyelesaikannya, tapi sekarang kita masuk ke situasi berikut:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

sekarang kita sudah mendapatkan redundant check: kita sudah tahu bahwa nilainya genap, tetapi padToEvenWithIsEvenmasih melakukan pemeriksaan parameter, yang akan selalu mengembalikan true, seperti yang kita sudah memanggil fungsi ini.

Sekarang isEvententu saja tidak menimbulkan masalah, tetapi jika pemeriksaan parameter lebih rumit maka ini mungkin menimbulkan biaya terlalu banyak. Selain itu, melakukan panggilan redundan sama sekali tidak terasa benar.

Terkadang kita dapat mengatasi ini dengan memperkenalkan "tipe yang divalidasi" atau dengan membuat fungsi di mana masalah ini tidak dapat terjadi:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

tetapi ini membutuhkan pemikiran yang cerdas dan refactor yang cukup besar.

Apakah ada (lebih) cara umum di mana kita dapat menghindari panggilan yang berlebihan isEvendan melakukan pengecekan parameter ganda? Saya ingin solusi untuk tidak benar-benar memanggil padToEvendengan parameter yang tidak valid, memicu pengecualian.


Tanpa pengecualian saya tidak bermaksud pemrograman bebas pengecualian, maksud saya bahwa input pengguna tidak memicu pengecualian oleh desain, sedangkan fungsi generik itu sendiri masih berisi pemeriksaan parameter (jika hanya untuk melindungi terhadap kesalahan pemrograman).

Maarten Bodewes
sumber
Apakah Anda hanya mencoba menghapus pengecualian? Anda dapat melakukannya dengan membuat asumsi tentang ukuran sebenarnya. Misalnya, jika Anda memasukkan 13, pad ke 12 atau 14 dan hindari cek sama sekali. Jika Anda tidak dapat membuat salah satu dari asumsi tersebut, maka Anda terjebak dengan pengecualian karena parameter tidak dapat digunakan dan fungsi Anda tidak dapat melanjutkan.
Robert Harvey
@RobertHarvey Bahwa - menurut pendapat saya - langsung bertentangan dengan prinsip kejutan paling tidak serta terhadap prinsip gagal cepat. Ini seperti mengembalikan nol jika parameter input nol (dan kemudian lupa untuk menangani hasilnya dengan benar, tentu saja, halo yang tidak dijelaskan NPE).
Maarten Bodewes
Ergo, pengecualian. Baik?
Robert Harvey
2
Tunggu apa? Anda memang datang ke sini untuk meminta nasihat, bukan? Apa yang saya katakan (dengan cara saya yang tampaknya tidak begitu halus), adalah bahwa pengecualian itu dirancang, jadi jika Anda ingin menghilangkannya, Anda mungkin harus memiliki alasan yang bagus. Ngomong-ngomong, saya setuju dengan amon: Anda mungkin seharusnya tidak memiliki fungsi yang tidak dapat menerima angka ganjil untuk parameter.
Robert Harvey
1
@ MaartenBodewes: Anda harus ingat bahwa padToEvenWithIsEven tidak melakukan validasi input pengguna. Ia melakukan pemeriksaan validitas pada inputnya untuk melindungi dirinya dari kesalahan pemrograman dalam kode panggilan. Seberapa luas validasi ini perlu tergantung pada analisis biaya / risiko di mana Anda meletakkan biaya cek terhadap risiko yang dilewati orang yang menulis kode panggilan dalam parameter yang salah.
Bart van Ingen Schenau

Jawaban:

7

Dalam contoh Anda, solusi terbaik adalah dengan menggunakan fungsi padding yang lebih umum; jika penelepon ingin pad ke ukuran genap maka mereka dapat memeriksanya sendiri.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Jika Anda berulang kali melakukan validasi yang sama pada suatu nilai, atau hanya ingin membolehkan subset nilai dari suatu jenis, maka jenis mikro / kecil dapat membantu. Untuk keperluan umum utilitas seperti padding, ini bukan ide yang baik, tetapi jika nilai Anda memainkan peran tertentu dalam model domain Anda, menggunakan tipe khusus alih-alih nilai primitif dapat menjadi langkah maju yang baik. Di sini, Anda mungkin mendefinisikan:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Sekarang kamu bisa mendeklarasikan

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

dan tidak perlu melakukan validasi dari dalam. Untuk pengujian sederhana yang membungkus int di dalam suatu objek kemungkinan akan lebih mahal dalam hal kinerja run time, tetapi menggunakan sistem tipe untuk keuntungan Anda dapat mengurangi bug dan memperjelas desain Anda.

Menggunakan tipe kecil seperti itu bahkan dapat berguna bahkan ketika mereka tidak melakukan validasi, misalnya untuk mendisambiguasikan sebuah string yang mewakili a FirstNamedari LastName. Saya sering menggunakan pola ini dalam bahasa yang diketik secara statis.

amon
sumber
Bukankah fungsi kedua Anda masih melemparkan pengecualian dalam beberapa kasus?
Robert Harvey
1
@ RobertTarvey Ya, misalnya dalam kasus null pointer. Tapi sekarang cek utama - bahwa nomor itu genap - dipaksa keluar dari fungsi menjadi tanggung jawab penelepon. Mereka kemudian dapat menangani pengecualian dengan cara yang mereka inginkan. Saya pikir jawabannya kurang berfokus pada menyingkirkan semua pengecualian daripada menyingkirkan duplikasi kode dalam kode validasi.
amon
1
Jawaban yang bagus Tentu saja di Jawa hukuman untuk membuat kelas data cukup tinggi (banyak kode tambahan) tapi itu lebih merupakan masalah bahasa daripada ide yang Anda sajikan. Saya kira Anda tidak akan menggunakan solusi ini untuk semua kasus penggunaan di mana masalah saya akan terjadi, tetapi itu adalah solusi yang baik terhadap penggunaan primitif yang berlebihan atau untuk kasus tepi di mana biaya pemeriksaan parameter sangat sulit.
Maarten Bodewes
1
@MaartenBodewes Jika Anda ingin melakukan validasi, Anda tidak dapat menghilangkan sesuatu seperti pengecualian. Nah, alternatifnya adalah menggunakan fungsi statis yang mengembalikan objek yang divalidasi atau null pada kegagalan, tetapi pada dasarnya tidak memiliki kelebihan. Tetapi dengan memindahkan validasi dari fungsi ke konstruktor, kita sekarang dapat memanggil fungsi tanpa kemungkinan kesalahan validasi. Itu memberi si penelepon semua kendali. Misalnya:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon
1
@MaartenBodewes Validasi duplikat terjadi setiap saat. Misalnya, sebelum mencoba menutup pintu, Anda dapat memeriksa apakah sudah ditutup, tetapi pintu itu sendiri juga tidak akan memungkinkan untuk ditutup kembali jika sudah ada. Tidak ada jalan keluar dari ini, kecuali jika Anda selalu percaya bahwa penelepon tidak melakukan operasi yang tidak valid. Dalam kasus Anda, Anda mungkin memiliki unsafePadStringToEvenoperasi yang tidak melakukan pemeriksaan, tetapi sepertinya ide yang buruk hanya untuk menghindari validasi.
plalx
9

Sebagai ekstensi untuk jawaban @ amon, kita dapat menggabungkannya EvenIntegerdengan apa yang oleh komunitas pemrograman fungsional dapat disebut "Smart Constructor" - sebuah fungsi yang membungkus konstruktor bodoh dan memastikan bahwa objek berada dalam keadaan valid (kita membuat bodoh kelas konstruktor atau dalam bahasa non-kelas berbasis modul / paket pribadi untuk memastikan hanya konstruktor pintar yang digunakan). Caranya adalah dengan mengembalikan Optional(atau setara) untuk membuat fungsi lebih komposable.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

Kita kemudian dapat dengan mudah menggunakan Optionalmetode standar untuk menulis logika input:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

Anda juga bisa menulis keepAsking()dalam gaya Java yang lebih idiomatis dengan loop do-while.

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Kemudian di sisa kode Anda, Anda dapat mengandalkan EvenIntegerdengan pasti bahwa itu benar-benar akan genap dan cek genap kami hanya pernah ditulis sekali, dalam EvenInteger::of.

walpen
sumber
Opsional secara umum menunjukkan data yang berpotensi tidak valid dengan cukup baik. Ini analog dengan kebanyakan metode TryParse, yang mengembalikan data dan tanda yang menunjukkan validitas input. Dengan cek waktu complie.
Basilevs
1

Melakukan validasi dua kali adalah masalah jika hasil validasi sama DAN validasi dilakukan di kelas yang sama. Itu bukan contoh Anda. Dalam kode refactored Anda, pemeriksaan isEven pertama yang dilakukan adalah validasi input, kegagalan menghasilkan input baru yang diminta. Pemeriksaan kedua sepenuhnya independen dari yang pertama, karena pada metode publik padToEvenWithEven yang dapat dipanggil dari luar kelas dan memiliki hasil yang berbeda (pengecualian).

Masalah Anda mirip dengan masalah kode identik yang tidak sengaja dikacaukan karena tidak kering. Anda membingungkan implementasinya, dengan desainnya. Mereka tidak sama, dan hanya karena Anda memiliki satu atau selusin baris yang sama, tidak berarti bahwa mereka melayani tujuan yang sama dan selamanya dapat dianggap dapat dipertukarkan. Juga, mungkin membuat kelas Anda melakukan terlalu banyak, tetapi lewatkan saja karena ini mungkin hanya contoh mainan ...

Jika ini adalah masalah kinerja, Anda dapat menyelesaikan masalah dengan membuat metode pribadi, yang tidak melakukan validasi apa pun, yang disebut padToEvenWithEven publik Anda setelah melakukan validasi itu dan yang akan dipanggil oleh metode lain Anda. Jika ini bukan masalah kinerja, biarkan metode Anda yang berbeda melakukan pengecekan yang mereka perlukan untuk melakukan tugas yang ditugaskan.

jmoreno
sumber
OP menyatakan bahwa validasi input dilakukan untuk mencegah fungsi melempar. Jadi cek sepenuhnya tergantung dan sengaja sama.
D Drmmr
@Drmmr: tidak, mereka tidak tergantung. Fungsi melempar karena itu adalah bagian dari kontraknya. Seperti yang saya katakan, jawabannya adalah membuat metode pribadi melakukan segalanya kecuali membuang pengecualian dan kemudian meminta metode publik memanggil metode pribadi. Metode publik mempertahankan cek, di mana ia melayani tujuan yang berbeda - untuk menangani input yang tidak divalidasi.
jmoreno