Validación de datos: ¿clase separada o no?

16

Cuando tengo muchos datos que necesitan ser validados, ¿debo crear una nueva clase con el único propósito de validación o debo seguir con la validación dentro del método?

Mi ejemplo particular contempla un torneo y una clase de evento / categoría: Tournamenty Event, que modela un torneo deportivo y cada torneo tiene una o varias categorías.

Hay todo tipo de cosas para validar en estas clases: los jugadores deben estar vacíos, deben ser únicos, la cantidad de partidos que cada jugador debe jugar, la cantidad de jugadores que tiene cada partido, enfrentamientos predefinidos y un gran etcétera que incluye mucho más. reglas complejas

También hay algunas partes que necesito validar en su conjunto, como la forma en que las clases se integran entre sí. Por ejemplo, la validación unitaria de a Playerpuede estar bien, pero si un evento tiene el mismo jugador dos veces, eso es un error de validación.

Entonces, ¿qué tal esto ?: Me olvido de absolutamente cualquier verificación previa cuando uso los setters de mis clases de modelo y métodos similares para agregar datos y en su lugar dejo que las clases de validación se encarguen de eso.

Entonces tendremos algo así como EventValidatorun Eventcomo miembro, y un validate()método que valida todo el objeto, además de métodos singulares para validar las reglas de todos los miembros.

Luego, antes de crear una instancia de un objeto validable, ejecutaré la validación para evitar valores ilegales.

¿Es correcto mi diseño? ¿Debo hacer algo diferente?

Además, ¿debo usar métodos de validación de retorno booleanos? ¿O simplemente lanzar una excepción si la validación falla? Me parece que la mejor opción serían los métodos de retorno booleanos y lanzar la excepción cuando se crea una instancia del objeto, por ejemplo:

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
fuente

Respuestas:

7

Está bien delegar cualquier lógica por medio de la composición si esa lógica va a cambiar dinámicamente durante la ejecución de un programa. Validaciones complejas como las que explicas son tan buenas candidatas como cualquiera para ser delegadas a otra clase a través de la composición.

Sin embargo, recuerde que las validaciones pueden ocurrir en diferentes momentos.

Instanciar un validador concreto como en su ejemplo es una mala idea porque combina su clase de evento con ese validador en particular.

Supongamos que no está utilizando ningún marco DI.

Puede agregar el validador al constructor o inyectarlo con un método de establecimiento. Sugiero que un método creador en una fábrica instancia tanto el evento como el validador y luego lo pasa al constructor del evento o con un método setValidator.

Obviamente, se debe escribir una interfaz Validator y / o una clase abstracta para que sus clases dependan de ella y no de ningún validador concreto.

Ejecutar el método de validación en el constructor puede ser problemático porque es posible que todavía no se haya implementado todo el estado que desea validar.

Le sugiero que cree una interfaz Validable y deje que sus clases la implementen, esa interfaz podría tener un método validate ().

De esa manera, los componentes superiores de su aplicación llaman al método de validación a voluntad (que a su vez se delega al miembro 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
fuente
Entonces, en términos de jerarquía de clases, esto no difiere mucho de lo que sugerí, ¿verdad? En lugar de crear instancias y llamar al validador dentro del constructor, se crearía y se llamaría cuando fuera necesario en el flujo de ejecución, esta es la principal diferencia que puedo ver.
dabadaba
Mi respuesta es básicamente que está bien crear otra clase para realizar validaciones complejas. Acabo de agregar algunos consejos para evitar el acoplamiento duro y obtener más flexibilidad.
Tulains Córdova
Si tuviera que agregar mensajes de error en una lista o diccionario, ¿debería estar dentro Validatoro dentro Validable? ¿Y cómo puedo trabajar estos mensajes junto con el ValidationException?
dabadaba
1
@dabadaba La lista debería ser miembro de los implementadores de IValidator, pero IValidable debería agregar acceso a ese método para que los implementadores lo deleguen. Supuse que se puede devolver más de un mensaje de validación. Haga clic en el enlace editado hace unos minutos en la parte inferior para que pueda ver las diferencias. Es mejor si usa la vista de lado a lado (sin rebajas).
Tulains Córdova
3
Probablemente sea pedante, pero supongo que Validatablees un nombre mucho mejor que Validablecomo es el adjetivo real
usuario919426
4

Me olvido de cualquier verificación previa cuando uso los setters de mis clases de modelos y métodos similares para agregar datos

Ese es el problema. Idealmente, debe evitar que sus objetos tengan un estado no válido: no permita la creación de instancias con un estado no válido y si debe tener setters y otros métodos de cambio de estado, inicie una excepción allí mismo.

en su lugar, dejo que las clases de validación manejen la validación.

Esto no es contradictorio. Si la lógica de validación es lo suficientemente compleja como para garantizar su propia clase, aún puede delegar la validación. Y si entiendo que estás en lo correcto, esto es exactamente lo que estás haciendo ahora:

Luego, antes de crear una instancia de un objeto validable, ejecutaré la validación para evitar valores ilegales.

Si todavía tiene configuradores que pueden generar un estado no válido, asegúrese de validar antes de cambiar el estado. De lo contrario, tendrá un mensaje de error, pero aún dejará su objeto con un estado no válido. Nuevamente: evite que sus objetos tengan un estado no válido

Además, ¿debo usar métodos de validación de retorno booleanos? ¿O simplemente lanzar una excepción si la validación falla? Me parece que la mejor opción serían los métodos de retorno booleanos y lanzar la excepción cuando se crea una instancia del objeto

Me parece bien, ya que el validador espera una entrada válida o no válida, por lo que desde su perspectiva, la entrada no válida no es una excepción.

Actualización: si "el sistema en su conjunto" no es válido, la razón es que algún objeto debe haber cambiado (por ejemplo, un jugador) y algún objeto posiblemente de nivel superior (por ejemplo, un evento) que hace referencia a este objeto tiene una condición que ya no se cumple . Ahora, una solución sería no permitir cambios directos en el jugador que pudieran invalidar algo en un nivel superior. Aquí es donde ayudan los objetos inmutables . Entonces un jugador cambiado es un objeto diferente. Una vez que un jugador está asociado a un evento, solo puede cambiarlo desde el evento en sí (porque tendrá que cambiar la referencia a un nuevo objeto) y volver a validarlo inmediatamente.

Fabian Schmengler
fuente
¿Qué pasa con no tener cheques de ningún tipo en la creación de instancias y colocadores y tener la validación como una operación aparte? Algo como lo sugirió Tulains Córdova. Porque validar y lanzar una excepción en un setter o constructor podría funcionar para ese miembro u objeto, pero no ayuda a verificar si el sistema en su conjunto está bien.
dabadaba
@dabadaba mi respuesta fue anhelar un comentario, por favor vea la actualización anterior
Fabian Schmengler
Sin embargo, no quiero usar objetos inmutables, quiero poder modificar mis clases. Y no sé cómo hacer objetos inmutables, a menos que se refiera a la palabra clave final. En ese caso, tendría que cambiar mucho mi diseño porque estoy construyendo parcialmente eventos y torneos con setters adicionales (porque estos datos son adicionales y configurables).
dabadaba
3
Para hacer objetos inmutables, elimine todos los establecedores y solo use constructores para establecer valores. Si tiene datos opcionales o datos que no están disponibles de inmediato, considere usar el patrón de construcción: developer.amd.com/community/blog/2009/02/06/…
Fabian Schmengler