Come eseguire la convalida dell'input senza eccezioni o ridondanza

10

Quando cerco di creare un'interfaccia per un programma specifico, in genere cerco di evitare di lanciare eccezioni che dipendono da input non convalidati.

Quindi quello che succede spesso è che ho pensato a un pezzo di codice come questo (questo è solo un esempio per il gusto di un esempio, non importa la funzione che esegue, esempio in Java):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, quindi supponiamo che evenSize sia effettivamente derivato dall'input dell'utente. Quindi non sono sicuro che sia pari. Ma non voglio chiamare questo metodo con la possibilità che venga lanciata un'eccezione. Così faccio la seguente funzione:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

ma ora ho due controlli che eseguono la stessa convalida dell'input: l'espressione nell'istruzione if e il controllo esplicito in isEven . Codice duplicato, non bello, quindi facciamo il refactoring:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, questo l'ha risolto, ma ora entriamo nella seguente situazione:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

ora abbiamo un controllo ridondante: sappiamo già che il valore è pari, ma padToEvenWithIsEven esegue ancora il controllo dei parametri, che restituirà sempre true, come abbiamo già chiamato questa funzione.

Ora per isEven ovviamente non pone un problema, ma se il controllo dei parametri è più ingombrante, questo potrebbe comportare costi eccessivi. Oltre a ciò, eseguire una chiamata ridondante semplicemente non sembra giusto.

A volte possiamo aggirare questo problema introducendo un "tipo convalidato" o creando una funzione in cui questo problema non può verificarsi:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

ma ciò richiede un pensiero intelligente e un refactoring piuttosto grande.

Esiste un modo (più) generico in cui possiamo evitare le chiamate ridondanti a isEven ed eseguire il controllo dei doppi parametri? Vorrei che la soluzione non chiamasse effettivamente padToEven con un parametro non valido, che attiva l'eccezione.

Senza eccezioni, non intendo programmazione senza eccezioni, intendo che l'input dell'utente non innesca un'eccezione in base alla progettazione, mentre la stessa funzione generica contiene ancora il controllo dei parametri (se solo per proteggersi dagli errori di programmazione).

    
posta Maarten Bodewes 14.01.2017 - 16:07
fonte

3 risposte

7

Nel caso del tuo esempio, la soluzione migliore è usare una funzione di riempimento più generale; se il chiamante desidera eseguire il pad a una dimensione uniforme, può controllarlo da sé.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Se si esegue ripetutamente la stessa convalida su un valore o si desidera consentire solo un sottoinsieme di valori di un tipo, quindi microtypes / tiny types può essere utile. Per utilità generiche come il padding, questa non è una buona idea, ma se il tuo valore gioca un ruolo particolare nel tuo modello di dominio, usare un tipo dedicato invece di valori primitivi può essere un grande passo avanti. Qui puoi definire:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Ora puoi dichiarare

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

e non devi fare alcuna convalida interna. Per un test così semplice avvolgere un int all'interno di un oggetto sarà probabilmente più costoso in termini di prestazioni del tempo di esecuzione, ma usare il sistema dei tipi a proprio vantaggio può ridurre i bug e chiarire il tuo design.

L'utilizzo di tipi così piccoli può persino essere utile anche quando non eseguono alcuna convalida, ad es. per disambiguare una stringa che rappresenta un FirstName da LastName . Uso frequentemente questo modello in lingue tipizzate staticamente.

    
risposta data 14.01.2017 - 16:56
fonte
8

Come estensione della risposta di @ amon, possiamo combinare il suo EvenInteger con ciò che la comunità di programmazione funzionale potrebbe chiamare "Smart Constructor" - una funzione che avvolge il costruttore muto e si assicura che l'oggetto sia in una valida stato (rendiamo la classe del costruttore muto o in un modulo / pacchetto di lingue non classificate private per assicurarsi che vengano utilizzati solo i costruttori intelligenti). Il trucco è di restituire un Optional (o equivalente) per rendere la funzione più componibile.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

Possiamo quindi utilizzare facilmente i metodi standard Optional per scrivere la logica di input:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

Potresti anche scrivere keepAsking() in uno stile Java più idiomatico con un ciclo do-while.

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Quindi nel resto del codice puoi contare su EvenInteger con certezza che sarà davvero pari e il nostro controllo pari è stato scritto solo una volta, in EvenInteger::of .

    
risposta data 17.01.2017 - 03:36
fonte
0

Effettuare la convalida due volte è un problema se il risultato della convalida è lo stesso E la convalida viene eseguita nella stessa classe. Questo non è il tuo esempio. Nel tuo codice refactored, il primo è che anche il controllo di input è una convalida dell'input, in caso di errore viene richiesto un nuovo input. Il secondo controllo è completamente indipendente dal primo, poiché è su un metodo pubblico padToEvenWithEven che può essere chiamato dall'esterno della classe e ha un risultato diverso (l'eccezione).

Il tuo problema è simile al problema di confusione tra codice accidentalmente identico per non asciutto. Stai confondendo l'implementazione, con il design. Non sono la stessa cosa, e solo perché hai una o una dozzina di linee uguali, non significa che stiano servendo allo stesso scopo e possano essere per sempre intercambiabili. Inoltre, probabilmente la tua classe fa troppo, ma salta questo dato che probabilmente è solo un esempio di giocattolo ...

Se si tratta di un problema di prestazioni, è possibile risolvere il problema creando un metodo privato, che non esegue alcuna convalida, che il tuo public padToEvenWithEven ha chiamato dopo aver fatto la sua convalida e che invece l'altro metodo chiamerebbe. Se non si tratta di un problema di prestazioni, lascia che i diversi metodi eseguano i controlli necessari per eseguire le attività assegnate.

    
risposta data 15.01.2017 - 19:03
fonte