Lendo essa pergunta do SO , parece que lançar exceções para validar a entrada do usuário é desaprovado.
Mas quem deve validar esses dados? Nos meus aplicativos, todas as validações são feitas na camada de negócios, porque apenas a própria classe realmente sabe quais valores são válidos para cada uma de suas propriedades. Se eu copiasse as regras para validar uma propriedade para o controlador, é possível que as regras de validação mudem e agora existem dois locais onde a modificação deve ser feita.
Minha premissa é que a validação deve ser feita na camada de negócios errada?
O que eu faço
Então, meu código geralmente acaba assim:
<?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
}
No controlador, eu apenas passo os dados de entrada para o modelo e pego exceções lançadas para mostrar o (s) erro (s) para o usuário:
<?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);
}
Essa é uma metodologia ruim?
Método alternativo
Talvez eu deva criar métodos para isValidAge($a)
esse retorno verdadeiro / falso e depois chamá-los do controlador?
<?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
}
E o controlador será basicamente o mesmo, apenas em vez de try / catch, existem agora if / else:
<?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);
}
Então, o que eu deveria fazer?
Estou muito feliz com meu método original e meus colegas a quem eu o mostrei em geral gostaram. Apesar disso, devo mudar para o método alternativo? Ou estou fazendo isso terrivelmente errado e devo procurar outro caminho?
fonte
ValidationException
e outras exceçõesIValidateResults
.Respostas:
A abordagem que usei no passado é colocar todas as classes de validação dedicadas à lógica de validação.
Você pode injetar essas classes de validação em sua camada de apresentação para validação de entrada antecipada. E nada impede que suas classes Model usem as mesmas classes para impor a Integridade de Dados.
Seguindo essa abordagem, você pode tratar os erros de validação de maneira diferente, dependendo da camada em que eles ocorrem:
fonte
PersonValidator
com toda a lógica para validar os diferentes atributos de aPerson
, e aPerson
classe que depende dissoPersonValidator
, certo? Qual é a vantagem que sua proposta oferece sobre o método alternativo sugerido na pergunta? Só vejo a capacidade de injetar diferentes classes de validação para aPerson
, mas não consigo pensar em nenhum caso em que isso seria necessário.Se você e seus colegas estão satisfeitos com isso, não vejo necessidade urgente de mudar.
A única coisa que é questionável de uma perspectiva pragmática é que você está jogando em
Exception
vez de algo mais específico. O problema é que, se você capturarException
, poderá acabar capturando exceções que nada têm a ver com a validação da entrada do usuário.Agora, muitas pessoas dizem que coisas como "exceções devem ser usadas apenas para coisas excepcionais e XYZ não é excepcional". (Por exemplo, a resposta de @ dann1111 ... onde ele rotula os erros do usuário como "perfeitamente normais".)
Minha resposta a isso é que não há critério objetivo para decidir se algo ("XY Z") é excepcional ou não. É uma medida subjetiva . (O fato de qualquer programa precisar verificar se há erros na entrada do usuário não torna os erros de ocorrência "normais". Na verdade, "normal" não faz muito sentido do ponto de vista objetivo.)
Há um grão de verdade nesse mantra. Em alguns idiomas (ou mais precisamente, em algumas implementações de idiomas ), a criação de exceções, o lançamento e / ou a captura são significativamente mais caros do que os condicionais simples. Mas se você olhar dessa perspectiva, precisará comparar o custo de criação / lançamento / captura com o custo dos testes extras que talvez você precise executar se evitar o uso de exceções. E a "equação" deve levar em conta a probabilidade de que a exceção precise ser lançada.
O outro argumento contra as exceções é que elas podem dificultar o entendimento do código. Mas o outro lado é que, quando usados de maneira apropriada, eles podem facilitar a compreensão do código .
Em resumo - a decisão de usar ou não usar exceções deve ser tomada após ponderar os méritos ... e NÃO com base em algum dogma simplista.
fonte
Exception
ser jogado / capturado. Eu realmente jogo algumas subclasses deException
, e o código dos setters geralmente não faz nada que poderia lançar outra exceção.Na minha opinião, é útil distinguir entre erros de aplicativo e erros de usuário e usar apenas exceções para o primeiro.
Exceções destinam-se a cobrir coisas que impedem a execução correta do seu programa .
São ocorrências inesperadas que o impedem de continuar, e seu design reflete isso: eles interrompem a execução normal e pulam para um local que permite o tratamento de erros.
Erros de usuário como entrada inválida são perfeitamente normais (da perspectiva do seu programa) e não devem ser tratados como inesperados pelo seu aplicativo .
Se o usuário digitar o valor errado e você exibir uma mensagem de erro, seu programa "falhou" ou ocorreu algum erro? Não. Seu aplicativo foi bem-sucedido - dado um certo tipo de entrada, ele produziu a saída correta nessa situação.
A manipulação de erros do usuário, porque faz parte da execução normal, deve fazer parte do fluxo normal do programa, em vez de ser tratada saltando com uma exceção.
Obviamente, é possível usar exceções para outros fins que não os propósitos pretendidos, mas isso confunde o paradigma e corre o risco de comportamento incorreto quando esses erros ocorrem.
Seu código original é problemático:
setAge()
método deve saber muito sobre o tratamento interno de erros do método: o responsável pela chamada precisa saber que uma exceção é lançada quando a idade é inválida e que nenhuma outra exceção pode ser lançada no método . Essa suposição pode ser quebrada mais tarde se você adicionar funcionalidades adicionaissetAge()
.O código alternativo também tem problemas:
isValidAge()
, foi introduzido.setAge()
método deve assumir que o chamador já verificouisValidAge()
(uma suposição terrível) ou validar a idade novamente. Se validar a idade novamente,setAge()
ainda será necessário fornecer algum tipo de tratamento de erros e você voltará à estaca zero novamente.Projeto sugerido
Torne o
setAge()
retorno verdadeiro em caso de sucesso e falso em caso de falha.Verifique o valor de retorno
setAge()
e, se falhou, informe ao usuário que a idade era inválida, não com uma exceção, mas com uma função normal que exibe um erro para o usuário.fonte
setAge
validação novamente, mas como a lógica é basicamente "se for válida, defina a idade para lançar uma exceção", não me leva de volta à estaca zero."The entered age is out of bounds" == true
pessoas sempre devem usar===
, portanto, essa abordagem seria mais problemática do que o problema que ela tenta solucionar. resolversetAge()
que você cria em qualquer lugar, é necessário verificar se realmente funcionou. Lançar exceções significa que você não deve se preocupar em lembrar de verificar se tudo correu bem. A meu ver, tentar definir um valor inválido em um atributo / propriedade é algo excepcional e vale a pena jogar oException
. O modelo não deve se importar se está recebendo sua entrada do banco de dados ou do usuário. Ele nunca deve receber informações incorretas, portanto vejo que é legítimo lançar uma exceção lá.Do meu ponto de vista (eu sou do Java), é totalmente válido como você o implementou da primeira maneira.
É válido que um objeto gere uma exceção quando algumas pré-condições não forem atendidas (por exemplo, sequência vazia). Em Java, o conceito de exceções verificadas é destinado a esse objetivo - exceções que devem ser declaradas na assinatura para serem lançadas de maneira adequada e o chamador explicitamente precisa capturá-las. Por outro lado, exceções não verificadas (também conhecidas como RuntimeExceptions), podem ocorrer a qualquer momento, sem a necessidade de definir uma cláusula catch no código. Enquanto o primeiro é usado para casos recuperáveis (por exemplo, entrada incorreta do usuário, o nome do arquivo não existe), o último é usado para casos em que o usuário / programador não pode fazer nada (por exemplo, falta de memória).
Você deve, no entanto, como já mencionado por @Stephen C, definir suas próprias exceções e capturar especificamente aquelas para não capturar outras pessoas sem querer.
Outra maneira, no entanto, seria usar objetos de transferência de dados que são simplesmente recipientes de dados sem lógica. Em seguida, você entrega esse DTO a um validador ou ao próprio Objeto-modelo para validação, e somente se for bem-sucedido, faça as atualizações no Objeto-modelo. Essa abordagem geralmente é usada quando a lógica de apresentação e a lógica do aplicativo são camadas separadas (a apresentação é uma página da Web, o aplicativo é um serviço da Web). Dessa forma, eles são fisicamente separados, mas se você tiver os dois em uma camada (como no seu exemplo), verifique se não haverá nenhuma solução alternativa para definir um valor sem validação.
fonte
Com meu chapéu de Haskell, as duas abordagens estão erradas.
O que acontece conceitualmente é que você primeiro tem um monte de bytes e, depois de analisar e validar, pode construir uma Pessoa.
A Pessoa tem certos invariantes, como a precença de um nome e uma idade.
Ser capaz de representar uma Pessoa que tem apenas um nome, mas nenhuma idade é algo que você deseja evitar a todo custo, porque é isso que cria a complementaridade. Invariantes estritos significa que você não precisa verificar a presença de uma idade mais tarde, por exemplo.
Então, no meu mundo, Person é criado atomicamente usando um único construtor ou função. Esse construtor ou função pode novamente verificar a validade dos parâmetros, mas nenhuma Meia Pessoa deve ser construída.
Infelizmente, Java, PHP e outras linguagens OO tornam a opção correta bastante detalhada. Nas APIs Java apropriadas, os objetos do construtor são frequentemente usados. Em tal API, a criação de uma pessoa seria algo como isto:
ou o mais detalhado:
Nesses casos, não importa onde exceções são lançadas ou onde a validação ocorre, é impossível receber uma instância de Pessoa inválida.
fonte
Nas palavras dos leigos:
A primeira abordagem é a correta.
A segunda abordagem pressupõe que essas classes de negócios serão chamadas apenas por esses controladores e que nunca serão chamadas de outro contexto.
As classes de negócios devem lançar uma exceção sempre que uma regra de negócios for violada.
O controlador ou a camada de apresentação deve decidir se os lança ou executa suas próprias validações para impedir que as exceções aconteçam.
Lembre-se: suas aulas serão potencialmente usadas em diferentes contextos e por diferentes integradores. Portanto, eles devem ser inteligentes o suficiente para lançar exceções a insumos ruins.
fonte