Sviluppo di un'API per delegare / disaccoppiare lunghe catene di convalida

1

Recentemente, mi è stato affidato il compito di riscrivere un vecchio software. L'intero software stesso è ben scritto, tranne che per l'unica cosa che mi preoccupa, le classi contenenti un'enorme quantità di codice. Molte di queste non sono altro che catene di convalida davvero grandi come:

if (!isFooValid(dataObject.getFoo()))
    throw new FooException(...);

if (!isBarValid(dataObject.getBar()))
    throw new BarException(...);

E questo mi ha spinto a scrivere un'API che può aiutare a disaccoppiare le catene di convalida.

Per quanto riguarda la mia comprensione, la validazione è generalmente un Pattern composito , e se scompariamo e separiamo che vogliamo dal come vogliamo farlo, otteniamo:

Se foo è valido quindi fai qualcosa.

E abbiamo un'astrazione: è valida.

Quindi sono andato su disaccoppiando come otteniamo l'astrazione è valida , ho trovato un'idea di design.

Posso avere un oggetto Result , che contiene il messaggio sulla convalida con un semplice vero / falso per verificare se ha avuto successo o meno.

public interface Result {
    // StandardResult is the default implementation of Result.
    // Using the default constructor gives an instance that indicates
    // "SUCCESS" state of validation.
    public static final Result OK = new StandardResult();       
    public Throwable getError();    
    public boolean isOk();    
    public String getMessage();    
}

Posso avere un oggetto Validator<T> , che ha la logica di convalida, e quindi restituisce un oggetto Result che contiene informazioni su cosa è successo.

public interface Validator<T> {     
    public Result validate(T target);    
}

Questo mi consente di fare cose del tipo:

Result r = new SomeStringValidator().validate("This String");

E allo stesso modo, posso fare le catene di validazione usando un pattern Chain of Responsibility .

public class ChainValidator<T> implements Validator<T> {

    // This list contains all the validators that are in the chain.
    private final List<Validator<T>> validators = new ArrayList<Validator<T>>();

    public CompositeResult<T> validate(T target) {
        CompositeResult<T> result = new CompositeResult<T>();
        for (Validator<T> v : validators) {
            Result validationResult = null;
            try {
                validationResult = v.validate(target);
            } catch (Exception ex) {
                // Creating it with StandardResult(Throwable) would give
                // an instance that indicates "FAILED" state.
                validationResult = new StandardResult(ex);
            }
            result.put(v, validationResult);
        }
        return result;
    }

    private final class CompositeResult<T> implements Result {

        private final Map<Validator<T>, Result> delegate = new HashMap<Validator<T>, Result>(); 
        private Throwable failCause;

        public boolean isOk() {
            for (Result r : delegate.values()) {
                if (!r.isOk()) {
                    failCause = r.getError();
                    return false;
                }
            }
            return true;
        }


        public String getMessage() {
            return delegate.toString();
        }

        public void put(Validator<T> validator, Result resut) {
            delegate.put(validator, resut);
        }

        public Throwable getError() {
            return failCause;
        }

    }

}

Funziona ma mi lascia alcune domande:

  1. La primissima linea guida alla progettazione dell'API dice Non restituire null per indicare l'assenza di un valore , ma qui sto restituendo un oggetto null nel metodo Result#getError() .

  2. L'uso di generici sul mio esempio suona come codice odore?

Questo sarà basato sull'opinione

  1. Vale la pena di trasformarlo in un'API anziché solo in 4 classi?

Il codice sorgente potrebbe essere trovato qui .

    
posta Jay 08.04.2017 - 21:39
fonte

1 risposta

1

Un rapido primo punto che proverò è che se hai una funzione come questa:

public float divide(int numerator, int denominator) throws DivideByZeroException {
    if(!isValidDenominator(denominator)) {
        throw new DivideByZeroException();
    }
    return (float)numerator/denominator;
}

private boolean isValidDenominator(int denominator) {
    return denominator != 0;
}

Potresti semplificare le cose usando invece questa struttura:

public float divide(int numerator, int denominator) throws DivideByZeroException {
    checkValidDenominator(denominator);
    return (float)numerator/denominator;
}

private void checkValidDenominator(int denominator) throws DivideByZeroException {
    if(denominator == 0){
        throw new DivideByZeroException();
    }
}

Si noti che è anche possibile combinare facilmente più controlli in un unico metodo di controllo:

public int calculateMyThing(DataObject obj) throws InvalidInputException, NoGFException{
    checkValidDataObject();

    // calculate my thing here knowing the DataObject is valid

    return myThing;
}

private void checkValidDataObject(DataObject obj) 
            throws InvalidInputException, NoGFException {
    if(obj == null) {
        throw new InvalidInputException(obj);
    }
    checkValidFoo(obj);
    checkValidBar(obj);
    checkValidGirlfriend(obj);
    ...
}

Se sei fortunato e più metodi usano la stessa sequenza di controlli, puoi utilizzare il controllo combinato in tutti loro, riducendo notevolmente la quantità di codice nella tua classe.

Come per la tua API di convalida. Questo in realtà non funziona così bene.

Che cosa vuoi fare con un Exception ? Vuoi buttarlo?

public int foo(DataObject) throws FooException, NullPointerException {
    Validator<DataObject> fooValidator = ???
    if(!fooValidator.isOk()){
         throw fooValidator.getError() // <- type is 'Throwable'
    }
    ...
}

Come può il compilatore sapere che fooValidator.getError() è di FooException o NullPointerException ? Siamo sicuri che non si tratti di una BarException?

Un altro problema che ho con questo esempio è che non ho idea di dove debba venire il fooValidator. Fai un'istanza di una classe specifica? Com'è meglio di un semplice metodo (statico)?

Ora hai bisogno di un codice più ora rispetto a prima.

Sei, comunque, corretto nella tua preoccupazione per il grosso pezzo di codice di convalida mescolato con la logica di business. Dal momento che hai deciso che queste sono 2 responsabilità separate, è consigliabile mettere la convalida in una classe separata. Ma questo implica di per sé la necessità di progettare un'intera API attorno ad esso? Direi di no.

Invece dovresti concentrarti su quali parti del tuo codice appartengono insieme e quali parti possono essere riutilizzate.

Inizia in piccolo. Estrarre completamente la logica di convalida dalle funzioni. Innanzitutto inserendoli nella propria funzione nella stessa classe. Scopri quali funzioni possono essere riutilizzate o che possono essere leggermente modificate per essere riutilizzate in altri luoghi.

Successivamente, guarda quali funzioni logicamente appartengono insieme. Estrai quelli nella loro stessa classe.

A questo punto puoi iniziare a guardare in che modo queste classi di validazione si relazionano tra loro. Se eseguono tutti gli stessi controlli, ma su un tipo di input diverso, puoi guardare i generici per combinarli in una singola classe.

Quello che puoi combinare e come / quando farlo dipende molto dal codice esatto che stai cercando di ridefinire. Senza quel codice è piuttosto difficile dare consigli concreti.

    
risposta data 14.04.2017 - 13:08
fonte

Leggi altre domande sui tag