Model verileri doğrulıyorsa, kötü girdiye istisnalar atmamalıdır mı?


9

Bu SO sorusunu okumak , kullanıcı girişini doğrulamak için istisnalar atmanın hoşnut olmadığı anlaşılıyor.

Ancak bu verileri kim doğrulamalı? Uygulamalarımda, tüm doğrulamalar iş katmanında yapılır, çünkü yalnızca sınıfın kendisi, özelliklerinin her biri için hangi değerlerin geçerli olduğunu gerçekten bilir. Bir özelliği doğrulamak için kuralları denetleyiciye kopyalayacak olsaydım, doğrulama kurallarının değişmesi mümkündür ve şimdi değişikliğin yapılması gereken iki yer vardır.

Doğrulamanın iş katmanı üzerinde yapılması gerektiği konusunda benim fikrim yanlış mı?

Ne yaptığım

Yani benim kod genellikle böyle biter:

<?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
}

Denetleyicide, giriş verilerini modele geçiririm ve kullanıcıya hata (lar) ı göstermek için atılan istisnaları yakalarım:

<?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);
}

Bu kötü bir metodoloji mi?

Alternatif yöntem

Belki de isValidAge($a)doğru / yanlış döndürme için yöntemler oluşturmalı ve onları denetleyiciden çağırmalı mıyım?

<?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
}

Ve kontrolör temelde aynı olacak, sadece dene / yakala yerine şimdi / else varsa:

<?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);
}

Peki ne yapmalıyım?

Orijinal yöntemimle oldukça mutluyum ve genel olarak gösterdiğim meslektaşlarım bunu beğendi. Buna rağmen, alternatif yönteme geçmeli miyim? Yoksa bunu çok mu yanlış yapıyorum ve başka bir yol aramalıyım?


Ben "orijinal" kodu biraz ele ValidationExceptionve diğer istisnalar değiştirdim
Carlos Campderrós

2
Son kullanıcıya istisna mesajları göstermeyle ilgili bir sorun, modelin aniden kullanıcının hangi dili konuştuğunu bilmesi gerektiğidir, ancak bu çoğunlukla Görünüm için bir endişe kaynağıdır.
Bart van Ingen Schenau

@BartvanIngenSchenau iyi yakalama. Uygulamalarım her zaman tek dildir, ancak herhangi bir uygulamada ortaya çıkabilecek yerelleştirme sorunlarını düşünmek iyidir.
Carlos Campderrós

Doğrulama istisnaları sadece sürece türleri enjekte etmenin süslü bir yoludur. Gibi bir doğrulama arayüzü uygulayan bir nesne döndürerek aynı sonuçları elde edebilirsiniz IValidateResults.
Reactgular

Yanıtlar:


7

Geçmişte kullandığım yaklaşım tüm geçerlilik mantığını özel geçerlilik sınıflarına koymaktır.

Daha sonra bu Giriş Doğrulama sınıflarını erken giriş doğrulaması için Sunum Katmanınıza enjekte edebilirsiniz. Ve hiçbir şey Model sınıflarınızın Veri Bütünlüğünü uygulamak için aynı sınıfları kullanmasını engellemez.

Bu yaklaşımı izleyerek Doğrulama Hatalarını hangi katmanda bulunduklarına bağlı olarak farklı şekilde tedavi edebilirsiniz:

  • Veri Bütünlüğü Doğrulaması Modelde başarısız olursa, bir İstisna atın.
  • Sunum Katmanında Kullanıcı Giriş Doğrulaması başarısız olursa, yararlı bir ipucu görüntüleyin ve değeri Modelinize aktarmayı geciktirin.

Yani PersonValidatora'nın farklı niteliklerini doğrulamak için tüm mantığa sahip bir sınıfınız var Personve buna Personbağlı olan sınıf PersonValidator, değil mi? Teklifinizin, soruda önerdiğim alternatif yönteme göre sunduğu avantaj nedir? Sadece a için farklı Validasyon sınıfları enjekte etme kapasitesini görüyorum Person, ancak bunun gerekli olacağı hiçbir durum düşünemiyorum.
Carlos Campderrós

En azından bu nispeten basit durumda, doğrulama için tamamen yeni bir sınıf eklemenin aşırıya kaçması olduğunu kabul ediyorum. Çok daha karmaşık bir sorun için yararlı olabilir.

Birden fazla kişiye / şirkete satmayı planladığınız bir uygulama için bu mantıklı olabilir, çünkü her şirketin bir Kişinin yaşı için geçerli bir aralığı doğrulamak için farklı kuralları olabilir. Bu yüzden yararlı, ama gerçekten benim ihtiyaçları için aşırı. Her neyse, senin için +1
Carlos Campderrós

1
Doğrulamanın modelden ayrılması, bir birleşme ve birleşme açısından da mantıklıdır. Bu basit senaryoda aşırıya kaçabilir, ancak ayrı Validator sınıfını çok daha çekici kılmak için sadece tek bir "çapraz alan" doğrulama kuralı gerekir.
Seth M.12

8

Orijinal yöntemimle oldukça mutluyum ve genel olarak gösterdiğim meslektaşlarım bunu beğendi. Buna rağmen, alternatif yönteme geçmeli miyim? Yoksa bunu çok mu yanlış yapıyorum ve başka bir yol aramalıyım?

Siz ve meslektaşlarınız bundan memnunsanız, değişmenin acil bir ihtiyacını görmüyorum.

Pragmatik bir perspektiften sorgulanabilir olan tek şey, Exceptiondaha spesifik bir şeyden ziyade fırlatmanızdır . Sorun şu ki, yakalarsanız Exception, kullanıcı girişinin doğrulanmasıyla ilgisi olmayan istisnaları yakalayabilirsiniz.


Şimdi "istisnalar sadece istisnai şeyler için kullanılmalı ve XYZ istisnai değil" gibi şeyler söyleyen birçok insan var. (Örneğin, @ dann1111'in Cevabı ... burada kullanıcı hatalarını "tamamen normal" olarak etiketler.)

Buna cevabım, bir şeyin ("XY Z") istisnai olup olmadığına karar vermek için objektif bir kriter olmamasıdır. Bu öznel bir ölçüdür. (Herhangi bir programın kullanıcı girişindeki hataları kontrol etmesi gerektiği gerçeği , oluşma hatalarını "normal" yapmaz. Aslında "normal", objektif açıdan büyük ölçüde anlamsızdır.)

Bu mantrada bir gerçek doğrusu vardır. Bazı dillerde (veya daha doğru bir şekilde, bazı dil uygulamaları ) istisna oluşturma, atma ve / veya yakalama basit koşullardan çok daha pahalıdır. Ancak bu perspektiften bakarsanız, istisnalar kullanmaktan kaçınırsanız yapmanız gereken ekstra testlerin maliyetiyle oluşturma / atma / yakalama maliyetini karşılaştırmanız gerekir. Ve "denklem", istisnanın atılma olasılığını hesaba katmalıdır .

İstisnalar karşı diğer argüman onlar olmasıdır edebilirsiniz kod zor anlamak olun. Ancak kapak tarafı, uygun şekilde kullanıldıklarında, kodun anlaşılmasını kolaylaştırabilmeleridir .


Kısacası - istisnaları kullanma veya kullanma kararı, esasları tarttıktan sonra verilmelidir ... ve bazı basit dogmalar temelinde DEĞİL.


ExceptionAtılan / yakalanan jenerik hakkında iyi bir nokta . Gerçekten kendi alt sınıfını atıyorum Exceptionve ayarlayıcıların kodu genellikle başka bir istisna atabilecek hiçbir şey yapmıyor.
Carlos Campderrós

ValidationException ve diğer istisnalar / cc @ dan1111
Carlos Campderrós

1
+1, her yöntem çağrısının dönüş değerini kontrol etmek zorunda kalmadan karanlık çağlara dönmekten ziyade açıklayıcı bir ValidationException'ım var. Daha basit kod = potansiyel olarak daha az hata.
Heinzi

2
@ dan1111 - Fikir alma hakkınıza saygı duysam da, yorumunuzdaki hiçbir şey görüşten başka bir şey değildir. Doğrulamanın "normalliği" ile doğrulama hatalarını ele alma mekanizması arasında mantıksal bir bağlantı yoktur. Yaptığın tek şey dogmayı okumak.
Stephen C

@StephenC, yansıma üzerine davamı çok güçlü ifade ettiğimi hissediyorum. Daha kişisel bir tercih olduğuna katılıyorum.

6

Kanımca, uygulama hataları ile kullanıcı hataları arasında ayrım yapmak ve sadece birincisi için istisnalar kullanmak yararlıdır .

  • İstisnalar, programınızın düzgün bir şekilde yürütülmesini engelleyen şeyleri kapsamaktadır .

    Bunlar, devam etmenizi engelleyen beklenmedik olaylardır ve tasarımları bunu yansıtır: normal yürütmeyi bozar ve hata işlemeye izin veren bir yere atlarlar.

  • Geçersiz giriş gibi kullanıcı hataları son derece normaldir (programınızın bakış açısından) ve uygulamanız tarafından beklenmedik olarak değerlendirilmemelidir .

    Kullanıcı yanlış değeri girerse ve bir hata mesajı görüntülerseniz, programınız "başarısız" mı veya herhangi bir şekilde bir hata mı aldı? Hayır. Başvurunuz başarılı oldu - belirli bir girdi türü göz önüne alındığında, bu durumda doğru çıktıyı üretti.

    Kullanıcı hatalarını işleme, normal yürütmenin bir parçası olduğu için, bir istisna dışında atlamak yerine normal program akışınızın bir parçası olmalıdır.

Elbette istisnaları amaçlanan amaçların dışında kullanmak mümkündür, ancak bunu yapmak paradigmayı karıştırır ve bu hatalar meydana geldiğinde yanlış davranış riskini taşır.

Orijinal kodunuz sorunlu:

  • setAge()Yöntemin çağıran, yöntemin dahili hata işleme hakkında çok fazla şey bilmelidir: çağıran, yaş geçersiz olduğunda bir kural dışı durumun atıldığını ve yöntem içinde başka bir kural dışı durumun atılamayacağını bilmesi gerekir . Bu varsayım, içine ek işlevler eklerseniz daha sonra bozulabilir setAge().
  • Arayan istisnaları yakalamazsa, geçersiz yaş istisnası daha sonra başka bir şekilde, büyük olasılıkla opak bir şekilde ele alınacaktır. Veya işlenmeyen bir özel durum çökmesine neden olabilir. Girilen geçersiz veriler için iyi bir davranış değil.

Alternatif kodda da sorunlar var:

  • Ekstra, muhtemelen gereksiz bir yöntem isValidAge()getirildi.
  • Şimdi setAge()yöntem, arayan kişinin önceden kontrol edildiğini (korkunç bir varsayım) veya yaşı tekrar doğruladığını varsaymalıdırisValidAge() . Yaşı tekrar doğrularsa, setAge() yine de bir tür hata işleme sağlamanız gerekir ve tekrar kareye geri dönersiniz.

Önerilen tasarım

  • Yap setAge()başarısına doğru ve başarısızlık üzerine yanlış dönüş.

  • Dönüş değerini kontrol edin setAge()ve başarısız olursa, kullanıcıyı istisna dışında değil, kullanıcıya bir hata görüntüleyen normal bir işlevle yaşın geçersiz olduğunu bildirin.


O zaman nasıl yapmalıyım? Önerdiğim alternatif yöntemle ya da hiç düşünmediğim tamamen farklı bir şeyle mi? Ayrıca, "iş katmanında doğrulama yapılmalıdır" öncülüm yanlış mı?
Carlos Campderrós

@ CarlosCampderrós, güncellemeye bakın; Bu bilgiyi siz yorum yaparken ekliyordum. Orijinal tasarımınızın doğru yerde doğrulaması vardı, ancak bu doğrulamayı gerçekleştirmek için istisnalar kullanmak bir hataydı.

Alternatif yöntem, setAgetekrar doğrulamaya zorlar , ancak mantık temelde "geçerliyse, başka yaş atma istisnası ayarlayın" olduğundan beni kare kareye geri götürmez.
Carlos Campderrós

2
Hem alternatif yöntemde hem de önerilen tasarımda gördüğüm bir sorun, yaşın neden geçersiz olduğunu ayırt etme yeteneğini kaybetmeleridir. Doğru veya bir hata dizesi döndürmek için yapılabilir (evet, php soooo kirli), ancak bu birçok soruna yol açabilir, çünkü "The entered age is out of bounds" == trueinsanlar her zaman kullanmalıdır ===, bu yüzden bu yaklaşım denediği problemden daha sorunlu olacaktır. çözmek
Carlos Campderrós

2
Ama sonra uygulamayı kodlamak gerçekten yorucu çünkü her setAge()yerde yaptığınız her şey için, gerçekten işe yaradığını kontrol etmeniz gerekiyor. İstisnalar atmak, her şeyin yolunda gittiğini kontrol etmeyi hatırlamamanız gerektiği anlamına gelir. Gördüğüm gibi, bir niteliğe / özelliğe geçersiz bir değer ayarlamaya çalışmak olağanüstü bir şeydir ve sonra atmaya değer Exception. Model, girdisini veritabanından mı yoksa kullanıcıdan mı aldığını umursamalıdır. Asla kötü girdi almamalı, bu yüzden oraya bir istisna atmanın meşru olduğunu görüyorum.
Carlos Campderrós

4

Benim bakış açımdan (ben bir Java adamıyım) ilk şekilde nasıl uyguladığınız tamamen geçerlidir.

Bazı önkoşullar karşılanmadığında (örn. Boş dize) bir nesnenin İstisna atması geçerlidir. Java'da, kontrol edilen istisnalar kavramı, böyle bir amaca itilir - imzada açıkça atılabilmesi için bildirilmesi gereken istisnalar ve arayanın bunları açıkça yakalaması gerekir. Buna karşılık, işaretlenmeyen istisnalar (diğer adıyla RuntimeExceptions), kodda bir catch-cümlesi tanımlamaya gerek kalmadan herhangi bir zamanda olabilir. İlki kurtarılabilir durumlar için kullanılırken (örn. Yanlış kullanıcı girişi, dosya adı mevcut değildir), ikincisi kullanıcı / programcının hakkında hiçbir şey yapamayacağı durumlar için kullanılır (örn. Bellek Yetersiz).

Yine de, @Stephen C tarafından belirtildiği gibi, kendi istisnalarınızı tanımlamalı ve özellikle istemeden başkalarını yakalamamaları yakalamalısınız.

Bununla birlikte başka bir yol, herhangi bir mantık olmadan sadece veri kapları olan Veri Aktarım Nesneleri kullanmak olacaktır . Daha sonra böyle bir DTO'yu bir doğrulayıcıya veya Model Nesnesi'ne doğrulama için devredersiniz ve yalnızca başarılı olursa Model Nesnesinde güncellemeler yapın. Bu yaklaşım genellikle sunum mantığı ve uygulama mantığı ayrı katmanlar olduğunda kullanılır (sunum bir web sayfasıdır, bir web servisidir). Bu şekilde fiziksel olarak ayrılırlar, ancak her ikisinde de (örneğin örneğiniz gibi) varsa, doğrulama olmadan bir değer ayarlamak için geçici bir çözüm olmayacağından emin olmalısınız.


4

Haskell şapkamla her iki yaklaşım da yanlış.

Kavramsal olarak olan şey, önce bir demet baytınız olması ve ayrıştırıp onayladıktan sonra bir Kişi oluşturabilirsiniz.

Kişinin bir isim ve yaşın önceliği gibi belirli değişmezleri vardır.

Sadece bir adı olan, ancak yaşı olmayan bir Kişiyi temsil edebilmek her ne pahasına olursa olsun kaçınmak istediğiniz bir şeydir, çünkü bu bütünlüğü yaratan şeydir. Sıkı değişmezler, örneğin daha sonra bir yaşın varlığını kontrol etmeniz gerekmediği anlamına gelir.

Yani benim dünyamda, Kişi tek bir kurucu ya da işlev kullanılarak atomik olarak yaratılmıştır. Bu kurucu veya işlev, parametrelerin geçerliliğini tekrar kontrol edebilir, ancak yarım Kişi oluşturulmamalıdır.

Ne yazık ki, Java, PHP ve diğer OO dilleri doğru seçeneği oldukça ayrıntılı hale getirir. Uygun Java API'lerinde, oluşturucu nesneleri sıklıkla kullanılır. Böyle bir API'da, bir kişi oluşturmak şu şekilde görünür:

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

veya daha ayrıntılı:

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

Bu durumlarda, istisnalar nereye atılacak olursa veya doğrulama nerede gerçekleşirse gerçekleşsin, geçersiz bir Person örneği almak imkansızdır.


Burada yaptığınız tek şey sorunu gerçekten cevaplamadığınız Builder sınıfına taşımak.
Cypher

2
Sorunu atomik olarak yürütülen builder.build () işlevine yerelleştirdim. Bu işlev, tüm doğrulama adımlarının bir listesidir. Bu yaklaşım ile geçici yaklaşımlar arasında büyük bir fark vardır. Builder sınıfının basit türlerin ötesinde hiç değişmezi varken, Person sınıfının güçlü değişmezleri vardır. Doğru programlar oluşturmak, verilerinizde güçlü değişmezleri zorlamakla ilgilidir.
user239558

Hala soruya cevap vermiyor (en azından tamamen değil). Tek tek hata mesajlarının Builder sınıfından çağrı yığınından Görünüm'e nasıl aktarıldığını açıklayabilir misiniz?
Cypher

Üç olasılık: build (), OP'nin ilk örneğinde olduğu gibi belirli istisnalar atabilir. Bir grup insan tarafından okunabilir hata döndüren genel bir Set <String> validate () olabilir. İ18n-ready hataları için genel bir Set <Error> validate () olabilir. Önemli olan, bunun bir Person nesnesine dönüştürme işlemi sırasında gerçekleşmesidir.
user239558

2

Layman'ın sözleriyle:

İlk yaklaşım doğru yaklaşımdır.

İkinci yaklaşım, bu işletme sınıflarının yalnızca bu denetleyiciler tarafından çağrılacağını ve asla başka bir bağlamdan çağrılmayacağını varsayar.

İş dersleri, bir iş kuralı her ihlal edildiğinde bir İstisna atmalıdır.

Denetleyici veya sunum katmanı, istisnaların gerçekleşmesini önlemek için bunları atar mı yoksa kendi doğrulamalarını mı yapıp yapmadığına karar vermelidir.

Unutmayın: Sınıflarınız potansiyel olarak farklı bağlamlarda ve farklı entegratörler tarafından kullanılacaktır. Bu nedenle, kötü girdilere istisnalar atacak kadar akıllı olmalılar.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.