Se il modello sta validando i dati, non dovrebbe generare eccezioni su input errati?

9

Leggendo questa domanda SO sembra che vengano lanciate eccezioni per la convalida dell'input dell'utente è disapprovato.

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

La mia premessa è che la convalida dovrebbe essere eseguita sul livello aziendale in modo errato?

Cosa faccio

Quindi il mio codice di solito finisce in questo modo:

<?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 appena passato i dati di input al modello e catturato le eccezioni generate per mostrare l'errore (i) all'utente:

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

Questa è una cattiva metodologia?

Metodo alternativo

Dovrei forse creare metodi per isValidAge($a) che restituiscono true / false e quindi 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à praticamente lo stesso, invece di try / catch 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 male e dovrei cercare un altro modo?

    
posta Carlos Campderrós 12.06.2013 - 11:50
fonte

6 risposte

6

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

È quindi possibile iniettare queste classi di convalida nel proprio Presentation Layer per la convalida dell'input iniziale. Inoltre, nulla impedisce alle classi Model di utilizzare le stesse classi per imporre l'integrità dei dati.

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

  • Se la convalida dell'integrità dei dati fallisce nel modello, genera un'eccezione.
  • Se la convalida dell'input dell'utente fallisce nel Presentation Layer, allora mostra un suggerimento utile e rimanda il valore al tuo Modello.
risposta data 12.06.2013 - 12:55
fonte
8

I'm pretty happy with my original method, and my colleagues to whom I have showed it in general have liked it. Despite this, should I change to the alternate method? Or am I doing this terribly wrong and I should look for another way?

Se tu ei tuoi colleghi siete soddisfatti, non vedo alcuna necessità urgente di cambiare.

L'unica cosa che è discutibile da una prospettiva pragmatica è che stai lanciando Exception piuttosto che qualcosa di più specifico. Il problema è che se si cattura Exception , si può finire per intercettare 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 X Y Z non è eccezionale". (Ad esempio, @ Risposta di dann1111 ... dove etichetta gli errori dell'utente come "perfettamente normale".)

La mia risposta è che non esiste un criterio obiettivo per decidere se qualcosa ("X Y Z") sia eccezionale o meno. È una misura soggettiva . (Il fatto che qualsiasi programma abbia bisogno di verificare errori nell'input dell'utente non rende gli errori di occorrenza "normali", anzi, "normale" è in gran parte privo di significato da un punto di vista oggettivo.)

C'è un granello di verità in quel mantra. In alcune lingue (o più esattamente, alcune implementazioni della lingua ) la creazione di eccezioni, il lancio e / o la cattura sono significativamente più costosi dei semplici condizionali. Ma se si guarda da quella prospettiva, è necessario confrontare il costo di creare / gettare / catturare rispetto al costo dei test aggiuntivi che potrebbe essere necessario eseguire se si è evitato di utilizzare le eccezioni. E l'equazione deve tenere conto della probabilità che l'eccezione deve essere lanciata.

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

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

    
risposta data 12.06.2013 - 12:02
fonte
6

A mio parere, è utile distinguere tra errori di applicazione e errori utente e utilizzare solo eccezioni per il primo.

  • Le eccezioni sono intese a coprire aspetti che impediscono l'esecuzione corretta del tuo programma .

    Si tratta di occorrenze inaspettate che impediscono di continuare, e il loro design riflette questo: interrompono la normale esecuzione e passano a un posto che consente la gestione degli errori.

  • Gli errori degli utenti come input non validi sono perfettamente normali (dal punto di vista del tuo programma) e non dovrebbero essere trattati come inattesi dalla tua applicazione .

    Se l'utente immette il valore sbagliato e viene visualizzato un messaggio di errore, il programma ha "esito negativo" o ha avuto un errore in qualche modo? No. La tua applicazione ha avuto successo - dato un certo tipo di input, ha prodotto l'output corretto in quella situazione.

    Gestire gli errori degli utenti, poiché fa parte della normale esecuzione, dovrebbe far parte del normale flusso del programma, piuttosto che essere gestito saltando con un'eccezione.

Ovviamente è possibile utilizzare eccezioni per scopi diversi da quelli previsti, ma in questo modo si confonde il paradigma e si rischia un comportamento scorretto quando si verificano tali errori.

Il tuo codice originale è problematico:

  • Il chiamante del metodo setAge() 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 possono essere lanciate altre eccezioni all'interno del metodo . Questa ipotesi potrebbe essere risolta in seguito se hai aggiunto funzionalità aggiuntive in setAge() .
  • Se il chiamante non rileva eccezioni, l'eccezione di età non valida verrà in seguito gestita in un altro, molto probabilmente opaco, modo. O addirittura causare un arresto anomalo non gestito. Comportamento non valido per l'inserimento di dati non validi.

Anche il codice alternativo presenta problemi:

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

Design consigliato

  • Crea setAge() restituisce true in caso di successo e falso in caso di errore.

  • Verifica il valore restituito di setAge() e se non è riuscito, informa l'utente che l'età non è valida, non con un'eccezione, ma con una funzione normale che visualizza un errore per l'utente.

risposta data 12.06.2013 - 12:05
fonte
4

Dal mio punto di vista (sono un ragazzo Java) è totalmente valido come lo hai implementato nel primo modo.

È valido che un oggetto lanci 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 gettato in modo propizio, e il chiamante deve esplicitamente catturarle. Al contrario, le eccezioni non controllate (dette anche RuntimeExceptions), possono accadere in qualsiasi momento senza la necessità di definire una catch-clause nel codice. Mentre i primi sono usati per i casi recuperabili (per esempio input utente errato, filename non esiste), questi ultimi sono usati per i casi in cui l'utente / programmatore non può fare nulla (ad es. Out-Of-Memory).

Dovresti però, come già detto da @Stephen C, definire le tue eccezioni e catturare in modo specifico quelle per non catturare gli altri in modo non intenzionale.

Un altro modo, tuttavia, sarebbe utilizzare Oggetti di trasferimento dati che sono semplicemente contenitori di dati senza alcuna logica. Quindi si consegna un DTO a un validatore o all'oggetto Model stesso per la convalida e, solo se ha esito positivo, si effettua l'aggiornamento nell'oggetto modello. Questo approccio viene spesso utilizzato quando la logica di presentazione e la logica dell'applicazione sono livelli separati (la presentazione è una pagina Web, l'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 sarà soluzione per impostare un valore senza convalida.

    
risposta data 12.06.2013 - 13:54
fonte
4

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

Ciò che accade concettualmente è che prima hai un sacco di byte, e dopo l'analisi e la convalida, puoi quindi costruire una Persona.

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

Essere in grado di rappresentare una persona che ha solo un nome, ma nessuna età è qualcosa che si vuole evitare a tutti i costi, perché è questo che crea compleità. Per invarianti rigorosi, ad esempio, non è necessario verificare la presenza di un'età successiva.

Quindi nel mio mondo, Person viene creato atomicamente usando un singolo costruttore o funzione. Quel costruttore o funzione può controllare nuovamente la validità dei parametri, ma non dovrebbe essere costruita una mezza persona.

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

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

o il più dettagliato:

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 vengono lanciate eccezioni o dove avviene la convalida, è impossibile ricevere un'istanza Person non valida.

    
risposta data 03.06.2014 - 00:49
fonte
2

In parole semplici:

Il primo approccio è quello corretto.

Il secondo approccio presuppone che quelle classi di business saranno chiamate da quei controller e che non saranno mai chiamate da un altro contesto.

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

Il livello di controller o di presentazione deve decidere se lanciarli o fa le proprie convalide per impedire che si verifichino eccezioni.

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

    
risposta data 13.06.2013 - 18:37
fonte

Leggi altre domande sui tag