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?
sumber
ValidationException
dan pengecualian lainnyaIValidateResults
.Jawaban:
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:
sumber
PersonValidator
dengan semua logika untuk memvalidasi atribut yang berbeda dariPerson
, danPerson
kelas yang bergantung pada iniPersonValidator
, 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 untukPerson
, tetapi saya tidak dapat memikirkan kasus apa pun di mana ini akan diperlukan.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
Exception
bukan sesuatu yang lebih spesifik. Masalahnya adalah bahwa jika Anda menangkapException
, 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.
sumber
Exception
yang dilempar / ditangkap. Saya benar-benar melempar beberapa subclass sendiriException
, dan kode setter biasanya tidak melakukan apa pun yang bisa melempar pengecualian lain.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:
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 dalamnyasetAge()
.Kode alternatif juga memiliki masalah:
isValidAge()
telah diperkenalkan.setAge()
metode tersebut harus mengasumsikan bahwa penelepon sudah memeriksaisValidAge()
(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
setAge
untuk memvalidasi lagi, tetapi karena logikanya pada dasarnya "jika itu valid maka atur umur selain melemparkan pengecualian" tidak membuat saya kembali ke titik awal."The entered age is out of bounds" == true
dan orang harus selalu menggunakan===
, jadi pendekatan ini akan lebih bermasalah daripada masalah yang dicoba untuk memecahkansetAge()
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 dibuangException
. 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.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.
sumber
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:
atau lebih verbose:
Dalam kasus ini, di mana pun pengecualian dilemparkan atau di mana validasi terjadi, tidak mungkin untuk menerima contoh Orang yang tidak valid.
sumber
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.
sumber