Se il modello sta convalidando i dati, non dovrebbe generare eccezioni in caso di input errato?


9

Leggendo questa domanda SO sembra che non vengano considerate le eccezioni per la convalida dell'input dell'utente.

Ma chi dovrebbe validare questi dati? Nelle mie applicazioni, tutte le convalide vengono eseguite nel livello aziendale, poiché solo la classe stessa sa davvero quali valori sono validi per ciascuna delle sue proprietà. Se dovessi copiare le regole per la convalida di una proprietà sul controller, è possibile che le regole di convalida cambino e ora ci sono due punti in cui è necessario apportare la modifica.

La mia premessa è che la validazione dovrebbe essere fatta a livello aziendale sbagliata?

Quello che faccio

Quindi il mio codice di solito finisce così:

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

Nel controller, ho semplicemente passato i dati di input al modello e ho intercettato le eccezioni per mostrare all'utente gli errori:

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

È una cattiva metodologia?

Metodo alternativo

Forse dovrei creare metodi per isValidAge($a)restituire vero / falso e poi chiamarli dal 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
}

E il controller sarà sostanzialmente lo stesso, solo invece di provare / catturare ci sono ora se / 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);
}

Quindi cosa dovrei fare?

Sono abbastanza contento del mio metodo originale, e ai miei colleghi a cui l'ho mostrato in generale è piaciuto. Nonostante ciò, dovrei passare al metodo alternativo? O sto facendo questo terribilmente sbagliato e dovrei cercare un altro modo?


Ho modificato un po 'il codice "originale" da gestire ValidationExceptione altre eccezioni
Carlos Campderrós,

2
Un problema con la visualizzazione di messaggi di eccezione all'utente finale è che il modello improvvisamente ha bisogno di sapere quale lingua parla l'utente, ma questo è principalmente un problema per la vista.
Bart van Ingen Schenau,

@BartvanIngenSchenau buona cattura. Le mie applicazioni sono sempre state in una sola lingua, ma è bene pensare ai problemi di localizzazione che possono sorgere in qualsiasi implementazione.
Carlos Campderrós,

Le eccezioni per le convalide sono solo un modo sofisticato di iniettare tipi nel processo. È possibile ottenere gli stessi risultati restituendo un oggetto che implementa un'interfaccia di validazione simile IValidateResults.
Reactgular,

Risposte:


7

L'approccio che ho usato in passato è quello di mettere tutte le classi di validazione dedicate alla logica di validazione.

È quindi possibile iniettare queste classi di convalida nel proprio livello di presentazione per una convalida iniziale dell'input. Nulla impedisce alle classi Model di utilizzare le stesse classi per imporre l'integrità dei dati.

Seguendo questo approccio è quindi possibile trattare gli errori di convalida in modo diverso a seconda del livello in cui si verificano:

  • Se la convalida dell'integrità dei dati ha esito negativo nel modello, generare un'eccezione.
  • Se la convalida dell'input dell'utente ha esito negativo nel livello di presentazione, quindi visualizzare un suggerimento utile e ritardare l'invio del valore al modello.

In modo da avere classe PersonValidatorcon tutta la logica per validare i diversi attributi di una Person, e la Personclasse che dipende da questo PersonValidator, giusto? Qual è il vantaggio offerto dalla tua proposta rispetto al metodo alternativo che ho suggerito nella domanda? Vedo solo la capacità di iniettare diverse classi di convalida per a Person, ma non riesco a pensare a nessun caso in cui ciò sarebbe necessario.
Carlos Campderrós,

Concordo sul fatto che l'aggiunta di una nuova classe per la convalida sia eccessiva, almeno in questo caso relativamente semplice. Potrebbe essere utile per un problema con molta più complessità.

Bene, per un'applicazione che prevedi di vendere a più persone / aziende potrebbe avere senso, perché ogni azienda potrebbe avere regole diverse per convalidare quello che è un intervallo valido per l'età di una persona. Quindi è possibile utile, ma davvero eccessivo per le mie esigenze. Comunque, +1 anche per te
Carlos Campderrós,

1
Separare la validazione dal modello ha senso anche dal punto di vista dell'accoppiamento e della coesione. In questo semplice scenario potrebbe essere eccessivo, ma ci vorrà solo una singola regola di validazione "cross field" per rendere la classe Validator separata molto più allettante.
Seth M.,

8

Sono abbastanza contento del mio metodo originale, e ai miei colleghi a cui l'ho mostrato in generale è piaciuto. Nonostante ciò, dovrei passare al metodo alternativo? O sto facendo questo terribilmente sbagliato e dovrei cercare un altro modo?

Se tu e i tuoi colleghi siete contenti, non vedo alcuna urgente necessità di cambiare.

L'unica cosa che è discutibile da una prospettiva pragmatica è che stai lanciando Exceptionpiuttosto che qualcosa di più specifico. Il problema è che, se si rileva Exception, è possibile che vengano rilevate eccezioni che non hanno nulla a che fare con la convalida dell'input dell'utente.


Ora ci sono molte persone che dicono cose come "le eccezioni dovrebbero essere usate solo per cose eccezionali, e XYZ non è eccezionale". (Ad esempio, la risposta di @ dann1111 ... dove identifica gli errori dell'utente come "perfettamente normali".)

La mia risposta è che non esiste alcun criterio oggettivo per decidere se qualcosa ("XY Z") sia eccezionale o meno. È una misura soggettiva . (Il fatto che ogni programma necessità di verificare la presenza di errori di input dell'utente non rende gli errori di occorrenza "normale". Infatti, "normale" è molto senso dal punto di vista oggettivo.)

C'è un granello di verità in quel mantra. In alcune lingue (o più precisamente, in alcune implementazioni linguistiche ) la creazione di eccezioni, il lancio e / o la cattura sono significativamente più costosi dei semplici condizionali. Ma se lo guardi da quella prospettiva, devi confrontare il costo di creare / lanciare / catturare con il costo dei test extra che potresti dover eseguire se evitassi di usare le eccezioni. E l '"equazione" deve tenere conto della probabilità che l'eccezione debba essere lanciata.

L'altro argomento contro le eccezioni è che possono rendere il codice più difficile da capire. Ma il rovescio della medaglia è che quando vengono utilizzati in modo appropriato, possono rendere il codice più facile da capire.


In breve: la decisione di utilizzare o meno le eccezioni dovrebbe essere presa dopo aver valutato i meriti ... e NON sulla base di un dogma semplicistico.


Un buon punto per il generico che Exceptionviene lanciato / catturato. Lancio davvero una sottoclasse di Exception, e il codice dei setter di solito non fa nulla che potrebbe generare un'altra eccezione.
Carlos Campderrós,

Ho modificato un po 'il codice "originale" per gestire ValidationException e altre eccezioni / cc @ dan1111
Carlos Campderrós,

1
+1, preferirei avere una ValidationException descrittiva piuttosto che tornare ai tempi bui di dover controllare il valore di ritorno di ogni chiamata di metodo. Codice più semplice = potenzialmente meno errori.
Heinzi,

2
@ dan1111 - Mentre rispetto il tuo diritto di avere un'opinione, nulla nel tuo commento è altro che opinione. Non esiste alcuna connessione logica tra la "normalità" della convalida e il meccanismo di gestione degli errori di convalida. Tutto quello che stai facendo è recitare il dogma.
Stephen C,

@StephenC, dopo aver riflettuto sento di aver affermato il mio caso troppo fortemente. Sono d'accordo che si tratti più di una preferenza personale.

6

A mio avviso, è utile distinguere tra errori dell'applicazione e errori dell'utente e utilizzare solo le eccezioni per la prima.

  • Le eccezioni hanno lo scopo di coprire le cose che impediscono il corretto funzionamento del programma .

    Sono eventi imprevisti che ti impediscono di continuare e il loro design riflette questo: interrompono la normale esecuzione e saltano in un luogo che consente la gestione degli errori.

  • Errori dell'utente come input non validi sono perfettamente normali (dal punto di vista del programma) e non devono essere considerati imprevisti dall'applicazione .

    Se l'utente inserisce un valore errato e viene visualizzato un messaggio di errore, il programma "ha avuto esito negativo" o ha riscontrato un errore? No. L'applicazione ha avuto esito positivo: dato un determinato tipo di input, ha prodotto l'output corretto in quella situazione.

    La gestione degli errori degli utenti, poiché fa parte della normale esecuzione, dovrebbe far parte del normale flusso del programma, anziché essere gestita saltando fuori con un'eccezione.

Naturalmente è possibile utilizzare eccezioni per scopi diversi da quelli previsti, ma ciò confonde il paradigma e rischia comportamenti scorretti quando si verificano tali errori.

Il tuo codice originale è problematico:

  • Il chiamante del setAge()metodo deve sapere troppo sulla gestione degli errori interni del metodo: il chiamante deve sapere che viene generata un'eccezione quando l'età non è valida e che non è possibile generare altre eccezioni all'interno del metodo . Questa ipotesi potrebbe essere interrotta in seguito se si aggiungessero funzionalità aggiuntive all'interno setAge().
  • Se il chiamante non rileva eccezioni, l'eccezione di età non valida verrà successivamente gestita in un altro modo, molto probabilmente opaco. O addirittura causare un arresto anomalo non gestito. Comportamento non corretto per l'immissione di dati non validi.

Il codice alternativo ha anche problemi:

  • È isValidAge()stato introdotto un metodo extra, forse non necessario .
  • Ora il setAge()metodo deve presumere che il chiamante abbia già verificato isValidAge()( un'ipotesi terribile) o convalidato nuovamente l'età. Se convalida di nuovo l'età, deve setAge() ancora fornire una sorta di gestione degli errori e si torna di nuovo al punto di partenza.

Design suggerito

  • Fai setAge()return true in caso di successo e falso in caso di fallimento.

  • Controllare il valore restituito di setAge()e, in caso contrario, informare l'utente che l'età non era valida, non con un'eccezione, ma con una normale funzione che visualizza un errore per l'utente.


Allora come dovrei farlo? Con il metodo alternativo che ho proposto o con qualcosa di totalmente diverso a cui non ho pensato? Inoltre, la mia premessa è che "la validazione dovrebbe essere fatta a livello aziendale" falsa?
Carlos Campderrós,

@ CarlosCampderrós, vedi l'aggiornamento; Stavo aggiungendo queste informazioni come hai commentato. Il tuo progetto originale ha avuto la convalida al posto giusto, ma è stato un errore usare le eccezioni per eseguire quella convalida.

Il metodo alternativo impone setAgenuovamente la convalida, ma poiché la logica è fondamentalmente "se è valida, imposta l'età altrimenti genera un'eccezione" non mi riporta al punto di partenza.
Carlos Campderrós,

2
Un problema che vedo sia con il metodo alternativo sia con il progetto suggerito è che perdono la capacità di distinguere PERCHÉ l'età non era valida. Potrebbe essere fatto per restituire true o una stringa di errore (sì, php è davvero sporco), ma questo potrebbe portare a molti problemi, perché "The entered age is out of bounds" == truee le persone dovrebbero sempre usare ===, quindi questo approccio sarebbe più problematico del problema che tenta di risolvere
Carlos Campderrós,

2
Ma poi codificare l'applicazione è davvero noioso perché per ogni cosa setAge()che fai ovunque, devi controllare che abbia davvero funzionato. Generare eccezioni significa che non dovresti preoccuparti di ricordare di controllare che tutto sia andato bene. A mio avviso, provare a impostare un valore non valido in un attributo / proprietà è qualcosa di eccezionale e quindi vale la pena lanciare il Exception. Al modello non dovrebbe importare se sta ricevendo il suo input dal database o dall'utente. Non dovrebbe mai ricevere input errati, quindi vedo che è legittimo lanciare un'eccezione lì.
Carlos Campderrós,

4

Dal mio punto di vista (sono un ragazzo Java) è assolutamente valido il modo in cui lo hai implementato nel primo modo.

È valido che un oggetto generi un'eccezione quando non sono soddisfatte alcune condizioni preliminari (ad es. Stringa vuota). In Java il concetto di eccezioni controllate è inteso a tale scopo - eccezioni che devono essere dichiarate nella firma per essere lanciate in modo appropriato, e il chiamante deve esplicitamente catturarle. Al contrario, eccezioni non selezionate (ovvero RuntimeExceptions) possono verificarsi in qualsiasi momento senza la necessità di definire una clausola di cattura nel codice. Mentre i primi vengono utilizzati per casi recuperabili (ad es. Input utente errato, il nome file non esiste), i secondi vengono utilizzati per i casi in cui l'utente / programmatore non può fare nulla (ad esempio memoria insufficiente).

Dovresti però, come già accennato da @Stephen C, definire le tue eccezioni e catturare specificamente quelle per non catturare involontariamente gli altri.

Un altro modo, tuttavia, sarebbe quello di utilizzare gli oggetti di trasferimento dati che sono semplicemente contenitori di dati senza alcuna logica. Quindi consegnare tale DTO a un validatore o all'oggetto modello stesso per la convalida e solo in caso di successo, effettuare gli aggiornamenti nell'oggetto modello. Questo approccio viene spesso utilizzato quando la logica di presentazione e la logica dell'applicazione sono livelli separati (presentazione è una pagina Web, applicazione un servizio Web). In questo modo sono fisicamente separati, ma se hai entrambi su un livello (come nel tuo esempio), devi assicurarti che non ci siano soluzioni alternative per impostare un valore senza convalida.


4

Con il mio cappello Haskell, entrambi gli approcci sono sbagliati.

Ciò che accade concettualmente è che prima hai un sacco di byte e dopo aver analizzato e convalidato, puoi quindi costruire una persona.

La persona ha determinati invarianti, come la precenza di un nome e di un'età.

Essere in grado di rappresentare una persona che ha solo un nome, ma nessuna età è qualcosa che vuoi evitare a tutti i costi, perché questo è ciò che crea la completezza. Invarianti rigorosi indicano che non è necessario verificare la presenza di un'età in seguito, ad esempio.

Quindi nel mio mondo, la Persona viene creata atomicamente usando un solo costruttore o funzione. Quel costruttore o funzione può nuovamente verificare la validità dei parametri, ma nessuna mezza persona dovrebbe essere costruita.

Sfortunatamente, Java, PHP e altri linguaggi OO rendono l'opzione corretta piuttosto dettagliata. Nelle API Java appropriate, vengono spesso utilizzati oggetti builder. In una tale API, la creazione di una persona sarebbe simile a questa:

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

o più prolisso:

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

In questi casi, indipendentemente da dove vengano generate eccezioni o dove avvenga la convalida, è impossibile ricevere un'istanza Person non valida.


Tutto quello che hai fatto qui è spostare il problema nella classe Builder, a cui non hai veramente risposto.
Cypher

2
Ho localizzato il problema nella funzione builder.build () che viene eseguita atomicamente. Tale funzione è un elenco di tutti i passaggi di verifica. C'è un'enorme differenza tra questo approccio e gli approcci ad hoc. La classe Builder non ha invarianti oltre i tipi semplici, mentre la classe Person ha invarianti forti. Costruire programmi corretti significa far rispettare forti invarianti nei tuoi dati.
user239558,

Non risponde ancora alla domanda (almeno non completamente). Potresti approfondire come vengono trasmessi i singoli messaggi di errore dalla classe Builder dallo stack di chiamate alla vista?
Cypher,

Tre possibilità: build () può generare eccezioni specifiche, come nel primo esempio del PO. Può esistere un Set pubblico <String> validate () che restituisce un set di errori leggibili dall'uomo. Esiste un Set <Error> validate () pubblico per errori pronti per i18n. Il punto è che ciò accade durante la conversione in un oggetto Person.
user239558,

2

Nelle parole di laici:

Il primo approccio è quello corretto.

Il secondo approccio presuppone che tali business class saranno chiamate solo da quei controller e che non verranno mai chiamate da altri contesti.

Le classi aziendali devono generare un'eccezione ogni volta che viene violata una regola aziendale.

Il controller o il livello di presentazione devono decidere se li lancia o esegue le proprie convalide per evitare che si verifichino eccezioni.

Ricorda: le tue classi saranno potenzialmente utilizzate in contesti diversi e da diversi integratori. Quindi devono essere abbastanza intelligenti da generare eccezioni a input errati.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.