Convalida dei dati: classe separata o no?


16

Quando ho molti dati che devono essere validati, dovrei creare una nuova classe al solo scopo di validazione o devo attenermi alla validazione nel metodo?

Il mio esempio particolare riguarda un torneo e una classe evento / categoria: Tournamente Event, che modella un torneo sportivo e ogni torneo ha una o più categorie.

Ci sono tutti i tipi di cose da convalidare in queste classi: i giocatori dovrebbero essere vuoti, dovrebbero essere unici, il numero di partite che ogni giocatore dovrebbe giocare, il numero di giocatori che ogni partita ha, matchup predefiniti e un eccetera davvero grande, incluso molto altro regole complesse.

Ci sono anche alcune parti che devo convalidare nel loro insieme, come il modo in cui le classi si integrano l'una con l'altra. Ad esempio, la convalida unitaria di a Playerpuò andare bene, ma se un evento ha lo stesso giocatore due volte, si tratta di un errore di convalida.

Che ne dici di questo ?: Dimentico assolutamente qualsiasi controllo preliminare quando utilizzo i setter delle classi del mio modello e metodi simili per aggiungere dati e invece lascio che le classi di validazione gestiscano ciò.

Quindi avremo qualcosa di simile EventValidatora Eventcome membro e un validate()metodo che convalida l'intero oggetto, oltre a metodi singolari per convalidare tutte le regole dei membri.

Quindi, prima di creare un'istanza di un oggetto validabile, eseguirò la convalida per impedire valori illegali.

Il mio design è corretto? Dovrei fare qualcosa di diverso?

Inoltre, dovrei usare metodi di validazione di ritorno booleani? O semplicemente lanciare un'eccezione se la validazione fallisce? Mi sembra che l'opzione migliore sarebbe un metodo di restituzione booleano e generare l'eccezione quando l'oggetto viene istanziato, ad esempio:

public Event() {
    EventValidator eventValidator = new EventValidator(this);
    if (!eventValidator.validate()) {
        // show error messages with methods defined in the validator
        throw new Exception(); // what type of exception would be best? should I create custom ones?
    }
}

Risposte:


7

È corretto delegare qualsiasi logica mediante composizione se tale logica cambierà dinamicamente durante l'esecuzione di un programma. Convalide complesse come quelle che spieghi sono un candidato buono come un altro da delegare a un'altra classe tramite composizione.

Ricorda però che le convalide possono verificarsi in momenti diversi.

Creare un'istanza di un validatore concreto come nel tuo esempio è una cattiva idea perché abbina la tua classe Event a quel particolare validatore.

Supponiamo che non si stia utilizzando alcun framework DI.

È possibile aggiungere il validatore al costruttore o iniettarlo con un metodo setter. Suggerisco che un metodo creatore in una fabbrica crea un'istanza sia dell'evento che del validatore e quindi lo passa al costruttore dell'evento o con un metodo setValidator.

Ovviamente un'interfaccia Validator e / o una classe astratta dovrebbero essere scritte in modo che le tue classi dipendano da essa e non da alcun validatore concreto.

L'esecuzione del metodo di convalida nel costruttore può essere problematica perché tutto lo stato che si desidera convalidare potrebbe non essere ancora in atto.

Ti suggerisco di creta un'interfaccia Validable e lasciare che le tue classi la implementino, quell'interfaccia potrebbe avere un metodo validate ().

In questo modo i componenti superiori dell'applicazione chiamano il metodo validate a piacimento (che a sua volta è delegato al membro validatore).

==> IValidable.java <==

import java.util.List;

public interface IValidable {
    public void setValidator(IValidator<Event> validator_);
    public void validate() throws ValidationException;
    public List<String> getMessages();
}

==> IValidator.java <==

import java.util.List;

public interface IValidator<T> {
    public boolean validate(T e);
    public List<String> getValidationMessages();
}

==> Event.java <==

import java.util.List;

public class Event implements IValidable {

    private IValidator<Event> validator;

    @Override
    public void setValidator(IValidator<Event> validator_) {
        this.validator = validator_;
    }

    @Override
    public void validate() throws ValidationException {
        if (!this.validator.validate(this)){
            throw new ValidationException("WTF!");
        }
    }

    @Override
    public List<String> getMessages() {
        return this.validator.getValidationMessages();
    }

}

==> SimpleEventValidator.java <==

import java.util.ArrayList;
import java.util.List;

public class SimpleEventValidator implements IValidator<Event> {

    private List<String> messages = new ArrayList<String>();
    @Override
    public boolean validate(Event e) {
        // do validations here, by accessing the public getters of e
        // propulate list of messages is necessary
        // this example always returns false    
        return false;
    }

    @Override
    public List<String> getValidationMessages() {
        return this.messages;
    }

}

==> ValidationException.java <==

public class ValidationException extends Exception {
    public ValidationException(String message) {
        super(message);
    }

    private static final long serialVersionUID = 1L;
}

==> Test.java <==

public class Test {
    public static void main (String args[]){
        Event e = new Event();
        IValidator<Event> v = new SimpleEventValidator();
        e.setValidator(v);
        // set other thins to e like
        // e.setPlayers(player1,player2,player3)
        // e.setNumberOfMatches(3);
        // etc
        try {
            e.validate();
        } catch (ValidationException e1) {
            System.out.println("Your event doesn't comply with the federation regulations for the following reasons: ");
            for (String s: e.getMessages()){
                System.out.println(s);
            }
        }
    }
}

Quindi in termini di gerarchia di classi questo non differisce molto da quello che ho suggerito, vero? Invece di creare un'istanza e chiamare il validatore all'interno del costruttore, sarebbe stato creato e chiamato quando necessario nel flusso di esecuzione, questa è la differenza principale che posso vedere.
Dabadaba,

La mia risposta è fondamentalmente che è OK creare un'altra classe per fare una validazione complessa. Ho appena aggiunto alcuni consigli per evitare un accoppiamento duro e ottenere una maggiore flessibilità.
Tulains Córdova,

Se dovessi aggiungere messaggi di errore in un elenco o in un dictioary, dovrebbe essere in Validatoro in Validable? E come posso lavorare questi messaggi insieme a ValidationException?
Dabadaba,

1
@dabadaba L'elenco dovrebbe essere un membro degli implementatori IValidator, ma IValidable dovrebbe aggiungere l'accesso a quel metodo in modo che gli implementatori lo deleghino. Presumo che sia possibile restituire più di un messaggio di convalida. Fai clic sul link modificato n minuti fa in basso in modo da poter vedere le differenze. È meglio se usi la vista side-by-side (nessun markdown).
Tulains Córdova,

3
Probabilmente essere pedante, ma presumo che Validatablesia un nome molto migliore di Validablecome è il vero aggettivo
user919426

4

Dimentico qualsiasi controllo preliminare quando utilizzo i setter delle classi del mio modello e metodi simili per aggiungere dati

Questo è il problema. Idealmente, dovresti impedire ai tuoi oggetti di avere uno stato non valido: non consentire l'istanza con stato non valido e se devi avere setter e altri metodi per cambiare stato, lancia un'eccezione proprio lì.

invece lascio che le classi di validazione gestiscano la validazione.

Questo non è contradditorio. Se la logica di convalida è abbastanza complessa da giustificare la propria classe, è comunque possibile delegare la convalida. E se ho capito bene, questo è esattamente quello che stai facendo ora:

Quindi, prima di creare un'istanza di un oggetto validabile, eseguirò la convalida per impedire valori illegali.

Se hai ancora setter che potrebbero causare uno stato non valido, assicurati di convalidare prima di cambiare effettivamente lo stato. Altrimenti avrai un messaggio di errore ma lascerai comunque l'oggetto con uno stato non valido. Ancora una volta: impedisce agli oggetti di avere uno stato non valido

Inoltre, dovrei usare metodi di validazione di ritorno booleani? O semplicemente lanciare un'eccezione se la validazione fallisce? Mi sembra che l'opzione migliore sarebbe un metodo di restituzione booleana e generare l'eccezione quando l'oggetto viene istanziato

Mi sembra buono, poiché il validatore prevede input validi o non validi, quindi dal suo punto di vista input non validi non fanno eccezione.

Aggiornamento: se "il sistema nel suo insieme" non è valido, la ragione è che alcuni oggetti devono essere cambiati (ad esempio un giocatore) e alcuni oggetti di livello forse superiore (ad esempio un evento) che fanno riferimento a questo oggetto hanno una condizione che non è più soddisfatta . Ora una soluzione sarebbe quella di non consentire modifiche dirette al giocatore che potrebbero rendere qualcosa di non valido a un livello superiore. Qui è dove gli oggetti immutabili aiutano. Quindi un giocatore cambiato è un oggetto diverso. Una volta che un giocatore è associato a un evento, puoi cambiarlo solo dall'evento stesso (perché dovrai cambiare il riferimento a un nuovo oggetto) e validarlo nuovamente immediatamente.


Che dire di non avere alcun controllo nell'istanza e nei setter e di avere la validazione come un'operazione da parte? Qualcosa come Tulains Córdova ha suggerito. Perché la convalida e il lancio di un'eccezione in un setter o costruttore potrebbe funzionare per quel membro o oggetto, ma non aiuta a verificare se il sistema nel suo complesso è ok.
Dabadaba,

@dabadaba la mia risposta è stata quella di desiderare un commento, vedere l'aggiornamento sopra
Fabian Schmengler,

Non voglio usare oggetti immutabili però, voglio essere in grado di modificare le mie classi. E non so come creare oggetti immutabili, a meno che non ti riferisca alla parola chiave finale. In tal caso dovrei cambiare molto il mio design perché sto parzialmente costruendo eventi e tornei con setter aggiuntivi (perché questi dati sono adizionali e configurabili).
Dabadaba,

3
Per creare oggetti immutabili, rimuovere tutti i setter e utilizzare solo i costruttori per impostare i valori. Se disponi di dati opzionali o non disponibili contemporaneamente,
prendi in
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.