Jika model memvalidasi data, bukankah seharusnya memberikan pengecualian pada input yang buruk?

9

Membaca pertanyaan SO ini sepertinya melemparkan pengecualian untuk memvalidasi input pengguna tidak disukai.

Tetapi siapa yang harus memvalidasi data ini? Dalam aplikasi saya, semua validasi dilakukan di lapisan bisnis, karena hanya kelas itu sendiri yang benar-benar tahu nilai mana yang valid untuk masing-masing propertinya. Jika saya menyalin aturan untuk memvalidasi properti ke controller, ada kemungkinan bahwa aturan validasi berubah dan sekarang ada dua tempat di mana modifikasi harus dilakukan.

Apakah premis saya bahwa validasi harus dilakukan pada lapisan bisnis salah?

Apa yang saya lakukan

Jadi kode saya biasanya berakhir seperti ini:

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      throw new ValidationException("Name cannot be empty");
    }
    $this->name = $n;
  }

  public function setAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        throw new ValidationException("Age $a is not valid");
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      throw new ValidationException("Age $a is out of bounds");
    }
    $this->age = $a;
  }

  // other getters, setters and methods
}

Dalam controller, saya hanya meneruskan data input ke model, dan menangkap pengecualian yang dilemparkan untuk menunjukkan kesalahan (s) kepada pengguna:

<?php
$person = new Person();
$errors = array();

// global try for all exceptions other than ValidationException
try {

  // validation and process (if everything ok)
  try {
    $person->setAge($_POST['age']);
  } catch (ValidationException $e) {
    $errors['age'] = $e->getMessage();
  }

  try {
    $person->setName($_POST['name']);
  } catch (ValidationException $e) {
    $errors['name'] = $e->getMessage();
  }

  ...
} catch (Exception $e) {
  // log the error, send 500 internal server error to the client
  // and finish the request
}

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

Apakah ini metodologi yang buruk?

Metode alternatif

Haruskah saya membuat metode untuk isValidAge($a)mengembalikan true / false dan kemudian memanggil mereka dari controller?

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if ($this->isValidName($n)) {
      $this->name = $n;
    } else {
      throw new Exception("Invalid name");
    }
  }

  public function setAge($a) {
    if ($this->isValidAge($a)) {
      $this->age = $a;
    } else {
      throw new Exception("Invalid age");
    }
  }

  public function isValidName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      return false;
    }
    return true;
  }

  public function isValidAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        return false;
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      return false;
    }
    return true;
  }

  // other getters, setters and methods
}

Dan pengontrol pada dasarnya akan sama, alih-alih coba / tangkap sekarang ada / jika:

<?php
$person = new Person();
$errors = array();
if ($person->isValidAge($age)) {
  $person->setAge($age);
} catch (Exception $e) {
  $errors['age'] = "Invalid age";
}

if ($person->isValidName($name)) {
  $person->setName($name);
} catch (Exception $e) {
  $errors['name'] = "Invalid name";
}

...

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

Jadi apa yang harus aku lakukan?

Saya cukup senang dengan metode asli saya, dan rekan-rekan saya yang saya tunjukkan secara umum menyukainya. Meskipun demikian, haruskah saya mengubah metode alternatif? Atau apakah saya melakukan ini sangat salah dan saya harus mencari cara lain?

Carlos Campderrós
sumber
Saya telah memodifikasi sedikit kode "asli" untuk ditangani ValidationExceptiondan pengecualian lainnya
Carlos Campderrós
2
Salah satu masalah dengan menampilkan pesan pengecualian kepada pengguna akhir adalah bahwa model tiba-tiba perlu mengetahui bahasa apa yang digunakan pengguna, tetapi itu sebagian besar merupakan masalah bagi Tampilan.
Bart van Ingen Schenau
@ BartartIngenSchenau tangkapan yang bagus. Aplikasi saya selalu menggunakan satu bahasa, tetapi ada baiknya memikirkan masalah pelokalan yang mungkin muncul dalam implementasi apa pun.
Carlos Campderrós
Pengecualian untuk validasi hanyalah cara mewah untuk menyuntikkan tipe ke dalam proses. Anda dapat mencapai hasil yang sama dengan mengembalikan objek yang mengimplementasikan antarmuka validasi IValidateResults.
Reactgular

Jawaban:

7

Pendekatan yang saya gunakan di masa lalu adalah untuk menempatkan semua logika validasi yang didedikasikan kelas Validasi.

Anda kemudian dapat menyuntikkan kelas Validasi ini ke dalam Lapisan Presentasi Anda untuk validasi input awal. Dan, tidak ada yang mencegah kelas Model Anda menggunakan kelas yang sama untuk menegakkan Integritas Data.

Dengan mengikuti pendekatan ini, Anda kemudian dapat memperlakukan Kesalahan Validasi secara berbeda tergantung pada lapisan mana mereka terjadi:

  • Jika Validasi Integritas Data gagal dalam Model, maka lemparkan Pengecualian.
  • Jika Validasi Input Pengguna gagal di Lapisan Presentasi, maka tampilkan tip bermanfaat dan tunda mendorong nilai ke Model Anda.
MetaFight
sumber
Jadi, Anda memiliki kelas PersonValidatordengan semua logika untuk memvalidasi atribut yang berbeda dari Person, dan Personkelas yang bergantung pada ini PersonValidator, kan? Apa keuntungan yang ditawarkan proposal Anda dibandingkan metode alternatif yang saya sarankan dalam pertanyaan? Saya hanya melihat kapasitas untuk menyuntikkan kelas Validasi yang berbeda untuk Person, tetapi saya tidak dapat memikirkan kasus apa pun di mana ini akan diperlukan.
Carlos Campderrós
Saya setuju bahwa menambahkan seluruh kelas baru untuk validasi berlebihan, setidaknya dalam kasus yang relatif sederhana ini. Ini bisa berguna untuk masalah dengan kompleksitas yang lebih banyak.
Nah, untuk aplikasi yang Anda rencanakan untuk dijual ke banyak orang / perusahaan mungkin masuk akal memilikinya, karena masing-masing perusahaan mungkin memiliki aturan yang berbeda untuk memvalidasi berapa kisaran yang valid untuk usia Orang. Jadi mungkin bermanfaat, tetapi benar-benar berlebihan untuk kebutuhan saya. Pokoknya, +1 untuk Anda juga
Carlos Campderró
1
Memisahkan validasi dari model masuk akal dari sudut pandang kopling dan kohesi. Dalam skenario sederhana ini mungkin berlebihan, tetapi hanya akan mengambil aturan validasi "bidang silang" tunggal untuk membuat kelas Validator terpisah jauh lebih menarik.
Seth M.
8

Saya cukup senang dengan metode asli saya, dan rekan-rekan saya yang saya tunjukkan secara umum menyukainya. Meskipun demikian, haruskah saya mengubah metode alternatif? Atau apakah saya melakukan ini sangat salah dan saya harus mencari cara lain?

Jika Anda dan kolega Anda senang dengan itu, saya melihat tidak ada kebutuhan mendesak untuk berubah.

Satu-satunya hal yang dipertanyakan dari perspektif pragmatis adalah bahwa Anda melempar Exceptionbukan sesuatu yang lebih spesifik. Masalahnya adalah bahwa jika Anda menangkap Exception, Anda mungkin berakhir dengan menangkap pengecualian yang tidak ada hubungannya dengan validasi input pengguna.


Sekarang ada banyak orang yang mengatakan hal-hal seperti "pengecualian hanya boleh digunakan untuk hal-hal luar biasa, dan XYZ tidak luar biasa". (Misalnya, Jawaban @ dann1111 ... tempat ia memberi label kesalahan pengguna sebagai "sangat normal".)

Tanggapan saya terhadap hal itu adalah bahwa tidak ada kriteria objektif untuk memutuskan apakah sesuatu ("XY Z") luar biasa atau tidak. Itu adalah ukuran subyektif . (Fakta bahwa setiap program perlu memeriksa kesalahan dalam input pengguna tidak membuat kesalahan terjadinya "normal". Bahkan, "normal" sebagian besar tidak berarti dari sudut pandang objektif.)

Ada sebutir kebenaran dalam mantra itu. Dalam beberapa bahasa (atau lebih tepatnya, beberapa implementasi bahasa ) pengecualian penciptaan, melempar dan / atau menangkap secara signifikan lebih mahal daripada persyaratan sederhana. Tetapi jika Anda melihatnya dari perspektif itu, Anda perlu membandingkan biaya membuat / melempar / menangkap dengan biaya tes tambahan yang mungkin perlu Anda lakukan jika Anda menghindari menggunakan pengecualian. Dan "persamaan" harus memperhitungkan probabilitas bahwa pengecualian perlu dibuang.

Argumen lain yang menentang pengecualian adalah mereka dapat membuat kode lebih sulit untuk dipahami. Tetapi sisi sebaliknya adalah bahwa ketika mereka digunakan dengan tepat, mereka dapat membuat kode lebih mudah dimengerti.


Singkatnya - keputusan untuk menggunakan atau tidak menggunakan pengecualian harus dibuat setelah mempertimbangkan manfaatnya ... dan BUKAN berdasarkan beberapa dogma sederhana.

Stephen C
sumber
Poin bagus tentang obat generik Exceptionyang dilempar / ditangkap. Saya benar-benar melempar beberapa subclass sendiri Exception, dan kode setter biasanya tidak melakukan apa pun yang bisa melempar pengecualian lain.
Carlos Campderrós
Saya telah memodifikasi sedikit kode "asli" untuk menangani ValidationException dan pengecualian lain / cc @ dan1111
Carlos Campderrós
1
+1, saya lebih suka memiliki ValidationException deskriptif daripada kembali ke zaman kegelapan karena harus memeriksa nilai kembali dari setiap panggilan metode. Kode sederhana = kemungkinan lebih sedikit kesalahan.
Heinzi
2
@ dan1111 - Sementara saya menghormati hak Anda untuk memiliki pendapat, tidak ada dalam komentar Anda adalah sesuatu yang lain dari pendapat. Tidak ada koneksi logis antara "normalitas" validasi dan mekanisme untuk menangani kesalahan validasi. Yang Anda lakukan hanyalah membaca dogma.
Stephen C
@StephenC, setelah refleksi saya merasa saya menyatakan kasus saya terlalu kuat. Saya setuju bahwa ini lebih merupakan preferensi pribadi.
6

Menurut pendapat saya, berguna untuk membedakan antara kesalahan aplikasi dan kesalahan pengguna , dan hanya menggunakan pengecualian untuk yang pertama.

  • Pengecualian dimaksudkan untuk mencakup hal-hal yang mencegah program Anda dieksekusi dengan benar .

    Mereka adalah kejadian tak terduga yang mencegah Anda melanjutkan, dan desain mereka mencerminkan ini: mereka merusak eksekusi normal dan melompat ke tempat yang memungkinkan penanganan kesalahan.

  • Kesalahan pengguna seperti input yang tidak valid adalah hal yang normal (dari sudut pandang program Anda) dan tidak boleh diperlakukan sebagai hal yang tidak terduga oleh aplikasi Anda .

    Jika pengguna memasukkan nilai yang salah dan Anda menampilkan pesan kesalahan, apakah program Anda "gagal" atau ada kesalahan dengan cara apa pun? Tidak. Aplikasi Anda berhasil - diberi input jenis tertentu, itu menghasilkan output yang benar dalam situasi itu.

    Menangani kesalahan pengguna, karena itu adalah bagian dari eksekusi normal, harus menjadi bagian dari aliran program normal Anda, daripada ditangani dengan melompat dengan pengecualian.

Tentu saja dimungkinkan untuk menggunakan pengecualian untuk tujuan selain yang dimaksudkan, tetapi melakukan hal tersebut membingungkan paradigma dan risiko perilaku yang salah ketika kesalahan tersebut terjadi.

Kode asli Anda bermasalah:

  • Penelepon setAge()metode harus tahu terlalu banyak tentang penanganan kesalahan internal metode: penelepon perlu mengetahui bahwa pengecualian dilemparkan ketika usia tidak valid, dan bahwa tidak ada pengecualian lain yang dapat dilemparkan dalam metode ini . Asumsi ini dapat rusak nanti jika Anda menambahkan fungsionalitas tambahan di dalamnya setAge().
  • Jika penelepon tidak menangkap pengecualian, pengecualian usia yang tidak valid nantinya akan ditangani dengan cara lain yang kemungkinan besar tidak jelas. Atau bahkan menyebabkan crash pengecualian yang tidak tertangani. Perilaku yang tidak baik untuk memasukkan data yang tidak valid.

Kode alternatif juga memiliki masalah:

  • Metode tambahan, mungkin tidak perlu isValidAge()telah diperkenalkan.
  • Sekarang setAge()metode tersebut harus mengasumsikan bahwa penelepon sudah memeriksa isValidAge()(asumsi yang mengerikan) atau memvalidasi usia lagi. Jika itu memvalidasi usia lagi, setAge() masih harus menyediakan semacam penanganan kesalahan, dan Anda kembali ke titik awal lagi.

Desain yang disarankan

  • Jadikan setAge()kembali benar pada kesuksesan dan salah pada kegagalan.

  • Periksa nilai pengembalian setAge()dan jika gagal, beri tahu pengguna bahwa usia tidak valid, tidak terkecuali, tetapi dengan fungsi normal yang menampilkan kesalahan pada pengguna.


sumber
Lalu bagaimana saya harus melakukannya? Dengan metode alternatif yang saya usulkan atau dengan sesuatu yang sama sekali berbeda yang belum saya pikirkan? Juga, apakah premis saya bahwa "validasi harus dilakukan pada lapisan bisnis" salah?
Carlos Campderrós
@ CarlosCampderrós, lihat pembaruan; Saya menambahkan informasi itu ketika Anda berkomentar. Desain asli Anda memiliki validasi di tempat yang benar, tetapi kesalahan menggunakan pengecualian untuk melakukan validasi itu.
Metode alternatif tidak memaksa setAgeuntuk memvalidasi lagi, tetapi karena logikanya pada dasarnya "jika itu valid maka atur umur selain melemparkan pengecualian" tidak membuat saya kembali ke titik awal.
Carlos Campderrós
2
Satu masalah yang saya lihat baik dengan metode alternatif dan desain yang disarankan adalah bahwa mereka kehilangan kemampuan untuk membedakan MENGAPA usia tidak valid. Ini dapat dibuat untuk mengembalikan true atau string kesalahan (yeah, php sangat kotor), tetapi ini dapat menyebabkan banyak masalah, karena "The entered age is out of bounds" == truedan orang harus selalu menggunakan ===, jadi pendekatan ini akan lebih bermasalah daripada masalah yang dicoba untuk memecahkan
Carlos Campderrós
2
Tetapi kemudian mengkodekan aplikasi ini benar-benar melelahkan karena untuk setiap setAge()yang Anda buat di mana saja, Anda harus memeriksa apakah itu benar-benar berfungsi. Melontarkan pengecualian berarti Anda tidak perlu khawatir mengingat untuk memeriksa semuanya berjalan dengan baik. Seperti yang saya lihat, mencoba untuk menetapkan nilai yang tidak valid ke dalam atribut / properti adalah sesuatu yang luar biasa dan kemudian, layak untuk dibuang Exception. Model seharusnya tidak peduli jika mendapatkan input dari database atau dari pengguna. Seharusnya tidak pernah menerima input yang buruk, jadi saya melihat itu sah untuk membuang pengecualian di sana.
Carlos Campderrós
4

Dari sudut pandang saya (saya orang Jawa) benar-benar valid bagaimana Anda menerapkannya dengan cara pertama.

Adalah valid bahwa suatu objek melempar Exception ketika beberapa prasyarat tidak terpenuhi (mis. String kosong). Di Jawa konsep pengecualian yang diperiksa ditujukan untuk tujuan semacam itu - pengecualian yang harus dinyatakan dalam tanda tangan agar dapat dilemparkan dengan baik, dan penelepon secara eksplisit perlu menangkapnya. Sebaliknya, pengecualian yang tidak dicentang (alias RuntimeExceptions), dapat terjadi kapan saja tanpa perlu mendefinisikan klausa catch-in dalam kode. Sementara yang pertama digunakan untuk kasus-kasus yang dapat dipulihkan (misalnya input pengguna yang salah, nama file tidak ada), yang terakhir digunakan untuk kasus-kasus yang tidak dapat dilakukan oleh pengguna / pemrogram (mis. Kehabisan Memori).

Anda harus, seperti yang telah disebutkan oleh @Stephen C, mendefinisikan pengecualian Anda sendiri dan menangkap secara khusus mereka yang tidak menangkap orang lain secara tidak sengaja.

Cara lain, bagaimanapun, akan menggunakan Objek Transfer Data yang hanya data-wadah tanpa logika apa pun. Anda kemudian menyerahkan DTO tersebut ke validator atau Model-Object itu sendiri untuk validasi, dan hanya jika berhasil, buat pembaruan dalam Object-Model. Pendekatan ini sering digunakan ketika logika presentasi dan logika aplikasi dipisahkan tingkatan (presentasi adalah halaman web, aplikasi aplikasi web). Dengan cara ini mereka dipisahkan secara fisik, tetapi jika Anda memiliki keduanya di satu tingkat (seperti dalam contoh Anda), Anda harus memastikan bahwa tidak akan ada solusi untuk menetapkan nilai tanpa validasi.

Andy
sumber
4

Dengan topi Haskell saya aktif, kedua pendekatan itu salah.

Apa yang terjadi secara konseptual adalah bahwa Anda pertama kali memiliki banyak byte, dan setelah parsing dan memvalidasi, Anda kemudian dapat membangun Orang.

Orang tersebut memiliki invarian tertentu, seperti precense nama dan usia.

Mampu mewakili Seseorang yang hanya memiliki nama, tetapi tidak ada usia adalah sesuatu yang ingin Anda hindari dengan cara apa pun, karena itulah yang menciptakan kepatuhan. Misalnya, invarian yang ketat berarti Anda tidak perlu memeriksa keberadaan usia di kemudian hari.

Jadi di dunia saya, Orang dibuat secara atom dengan menggunakan satu konstruktor atau fungsi. Konstruktor atau fungsi itu lagi dapat memeriksa validitas parameter, tetapi tidak ada setengah-orang yang harus dibangun.

Sayangnya, Java, PHP dan bahasa OO lainnya membuat opsi yang benar cukup bertele-tele. Dalam Java API yang tepat, objek pembangun sering digunakan. Dalam API seperti itu, membuat seseorang akan terlihat seperti ini:

Person p = new Person.Builder().setName(name).setAge(age).build();

atau lebih verbose:

Person.Builder builder = new Person.Builder();
builder.setName(name);
builder.setAge(age);
Person p = builder.build();
// Person object must have name and age here

Dalam kasus ini, di mana pun pengecualian dilemparkan atau di mana validasi terjadi, tidak mungkin untuk menerima contoh Orang yang tidak valid.

pengguna239558
sumber
Yang Anda lakukan di sini adalah memindahkan masalah ke kelas Builder, yang belum Anda jawab.
Cypher
2
Saya telah melokalkan masalah ke fungsi builder.build () yang dijalankan secara atom. Fungsi itu adalah daftar semua langkah verifikasi. Ada perbedaan besar antara pendekatan ini dan pendekatan ad-hoc. Kelas Builder tidak memiliki invarian di luar tipe sederhana, sedangkan kelas Person memiliki invarian yang kuat. Membangun program yang benar adalah tentang menegakkan invarian kuat dalam data Anda.
user239558
Masih tidak menjawab pertanyaan (setidaknya tidak sepenuhnya). Bisakah Anda menguraikan bagaimana masing-masing pesan kesalahan dikirimkan dari kelas Builder ke tumpukan panggilan ke tampilan?
Cypher
Tiga kemungkinan: build () dapat melempar pengecualian khusus, seperti pada contoh pertama OP. Mungkin ada Set publik <String> validate () yang mengembalikan satu set kesalahan yang dapat dibaca manusia. Akan ada Set publik <Error> memvalidasi () untuk kesalahan siap-i18n. Intinya adalah bahwa ini terjadi selama konversi ke objek Orang.
user239558
2

Dalam kata-kata awam:

Pendekatan pertama adalah yang benar.

Pendekatan kedua mengasumsikan bahwa kelas-kelas bisnis hanya akan dipanggil oleh pengendali tersebut, dan bahwa mereka tidak akan pernah dipanggil dari konteks lain.

Kelas bisnis harus mengeluarkan Pengecualian setiap kali aturan bisnis dilanggar.

Kontroler atau lapisan presentasi harus memutuskan apakah itu melempar mereka atau apakah itu validasinya sendiri untuk mencegah pengecualian terjadi.

Ingat: kelas Anda berpotensi digunakan dalam konteks yang berbeda dan oleh integrator yang berbeda. Jadi mereka harus cukup pintar untuk melempar pengecualian ke input yang buruk.

Tulains Córdova
sumber