Validação de dados: classe separada ou não?

15

Quando tenho muitos dados que precisam ser validados, devo criar uma nova classe com o único objetivo de validação ou devo continuar com a validação no método?

Meu exemplo particular contempla um torneio e uma classe de evento / categoria: Tournamente Event, que modela um torneio esportivo e cada torneio possui uma ou várias categorias.

Há todo tipo de coisa para validar nessas classes: os jogadores devem estar vazios, devem ser únicos, o número de partidas que cada jogador deve jogar, o número de jogadores que cada partida tem, partidas predefinidas e um grande número de itens, incluindo muito mais regras complexas.

Também há algumas partes que preciso validar como um todo, como a forma como as classes se integram. Por exemplo, a validação unitária de a Playerpode ser ótima, mas se um evento tiver o mesmo jogador duas vezes, isso é um erro de validação.

Então, e quanto a isso ?: Eu esqueço absolutamente qualquer pré-verificação ao usar os setters e métodos semelhantes das minhas classes de modelo para adicionar dados e, em vez disso, deixo as classes de validação lidar com isso.

Portanto, teremos algo como EventValidatorcom Eventcomo membro e um validate()método que valida todo o objeto, além de métodos singulares para validar as regras de todos os membros.

Então, antes de instanciar um objeto validável, executarei a validação para evitar valores ilegais.

Meu design está correto? Devo fazer algo diferente?

Além disso, devo usar métodos de validação de retorno booleano? Ou apenas lançar uma exceção se a validação falhar? Parece-me que a melhor opção seria retornar métodos booleanos e lançar a exceção quando o objeto é instanciado instantaneamente, por exemplo:

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

Respostas:

7

Não há problema em delegar qualquer lógica por meio da composição se essa lógica mudar dinamicamente durante a execução de um programa. Validações complexas como as que você explica são um candidato tão bom quanto qualquer outro para ser delegado a outra classe por meio de composição.

Lembre-se, porém, que as validações podem ocorrer em diferentes momentos.

Instanciar um validador concreto, como no seu exemplo, é uma péssima idéia, porque associa sua classe Event a esse validador específico.

Vamos supor que você não esteja usando nenhuma estrutura DI.

Você pode adicionar o validador ao construtor ou injetá-lo com um método setter. Sugiro que um método criador em uma fábrica instancia o Event e o validador e depois o transmita ao construtor de eventos ou com um método setValidator.

Obviamente, uma interface Validator e / ou classe abstrata deve ser gravada para que suas classes dependam dela e não de qualquer validador concreto.

A execução do método validate no construtor pode ser problemática porque todo o estado que você deseja validar ainda não está em vigor.

Eu sugiro que você crie uma interface Validable e permita que suas classes a implementem, essa interface pode ter um método validate ().

Dessa forma, os componentes superiores do seu aplicativo chamam o método validate à vontade (que por sua vez é delegado ao membro validador).

==> 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);
            }
        }
    }
}
Tulains Córdova
fonte
Então, em termos de hierarquia de classes, isso não difere muito do que eu sugeri, não é? Em vez de instanciar e chamar o validador dentro do construtor, ele seria criado e chamado quando necessário no fluxo de execução, essa é a principal diferença que eu posso ver.
dabadaba
Minha resposta é basicamente que não há problema em criar outra classe para validação complexa. Acabei de adicionar alguns conselhos para você evitar acoplamentos rígidos e obter mais flexibilidade.
Tulains Córdova
Se eu fosse adicionar mensagens de erro em uma lista ou dictioary, deveria estar dentro Validatorou dentro Validable? E como posso trabalhar essas mensagens em conjunto com o ValidationException?
precisa saber é o seguinte
1
@dabadaba A lista deve ser um membro dos implementadores IValidator, mas IValidable deve adicionar acesso a esse método para que os implementadores o delegem. Presumi que mais de uma mensagem de validação possa ser retornada. Clique no link editado n minutos atrás, na parte inferior, para que você possa ver as diferenças. É melhor se você usar a visualização lado a lado (sem descontos).
Tulains Córdova
3
Provavelmente sendo pedante, mas suponho que Validatableseja um nome muito melhor do Validableque o verdadeiro adjetivo #
user919426 17/16/16
4

Eu esqueço absolutamente qualquer pré-verificação ao usar os setters e métodos semelhantes das minhas classes de modelo para adicionar dados

Esse é o problema. Idealmente, você deve impedir que seus objetos tenham um estado inválido: não permita instanciação com estado inválido e se você precisar de setters e outros métodos de alteração de estado, lance uma exceção ali.

em vez disso, deixo as classes de validação manipularem a validação.

Isto não é contraditório. Se a lógica de validação for suficientemente complexa para garantir sua própria classe, você ainda poderá delegar a validação. E se eu entendi que você está correto, é exatamente isso que você está fazendo agora:

Então, antes de instanciar um objeto validável, executarei a validação para evitar valores ilegais.

Se você ainda possui setters que podem resultar em um estado inválido, valide antes de alterar o estado. Caso contrário, você receberá uma mensagem de erro, mas ainda assim deixará seu objeto com um estado inválido. Novamente: impeça que seus objetos tenham estado inválido

Além disso, devo usar métodos de validação de retorno booleano? Ou apenas lançar uma exceção se a validação falhar? Parece-me que a melhor opção seria retornar métodos booleanos e lançar a exceção quando o objeto é instanciado

Parece bom para mim, como o validador espera entrada válida ou inválida, portanto, de sua perspectiva, a entrada inválida não é uma exceção.

Atualização: Se "o sistema como um todo" se tornar inválido, o motivo é que algum objeto deve ter sido alterado (por exemplo, um jogador) e algum objeto de nível mais alto (por exemplo, um evento) que faça referência a esse objeto tem uma condição que não é mais cumprida . Agora, uma solução seria não permitir mudanças diretas no player que poderiam invalidar algo em um nível superior. É aqui que os objetos imutáveis ajudam. Então um jogador alterado é um objeto diferente. Depois que um jogador estiver associado a um evento, você poderá alterá-lo apenas do próprio evento (porque precisará alterar a referência para um novo objeto) e validar novamente imediatamente.

Fabian Schmengler
fonte
Que tal não ter nenhuma verificação na instanciação e setters e ter a validação como uma operação de lado? Algo como Tulains Córdova sugeriu. Como validar e lançar uma exceção em um setter ou construtor pode funcionar para esse membro ou objeto, mas não ajuda a verificar se o sistema como um todo está correto.
precisa saber é o seguinte
@dabadaba minha resposta foi muito tempo para um comentário, por favor, veja update acima
Fabian Schmengler
Porém, não quero usar objetos imutáveis, quero poder modificar minhas classes. E não sei como criar objetos imutáveis, a menos que você esteja se referindo à palavra-chave final. Nesse caso, eu teria que mudar muito o meu design, porque estou construindo parcialmente eventos e torneios com levantadores adicionais (porque esses dados são adicionais e configuráveis).
precisa saber é o seguinte
3
Para criar objetos imutáveis, remova todos os setters e use apenas construtores para definir valores. Se você tiver dados ou dados que não estão disponíveis de imediato opcionais, considere usar o padrão do construtor: developer.amd.com/community/blog/2009/02/06/...
Fabian Schmengler