Come eseguire la convalida dell'input senza eccezioni o ridondanza


11

Quando provo a creare un'interfaccia per un programma specifico, in genere cerco di evitare di generare 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 a titolo di esempio, non importa la funzione che svolge, 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 dire che in evenSizerealtà è derivato dall'input dell'utente. Quindi non sono sicuro che sia pari. Ma non voglio chiamare questo metodo con la possibilità che venga generata un'eccezione. Quindi 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 ifnell'istruzione e il check-in esplicito isEven. Codice duplicato, non carino, quindi cerchiamo di refactor:

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 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 padToEvenWithIsEvenesegue comunque il controllo dei parametri, che restituirà sempre vero, come abbiamo già chiamato questa funzione.

Ora, isEvenovviamente, non rappresenta un problema, ma se il controllo dei parametri è più complicato, ciò può comportare costi eccessivi. Oltre a ciò, eseguire una chiamata ridondante semplicemente non sembra giusto.

A volte possiamo aggirare il 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 questo richiede un po 'di pensiero intelligente e un grande fattore di riflessione.

Esiste un modo (più) generico in cui possiamo evitare le chiamate ridondanti isEvened eseguire il doppio controllo dei parametri? Vorrei che la soluzione non chiamasse effettivamente padToEvencon un parametro non valido, innescando l'eccezione.


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


Stai solo cercando di rimuovere l'eccezione? Puoi farlo prendendo un'ipotesi su quale dovrebbe essere la dimensione effettiva. Ad esempio, se passi 13, passa a 12 o 14 ed evita del tutto il controllo. Se non puoi fare una di queste ipotesi, allora sei bloccato con l'eccezione perché il parametro è inutilizzabile e la tua funzione non può continuare.
Robert Harvey,

@RobertHarvey Questo - secondo me - va dritto contro il principio della minima sorpresa e contro il principio del fallimento veloce. È molto simile a restituire null se il parametro di input è null (e quindi dimenticare di gestire correttamente il risultato, ovviamente, ciao NPE inspiegabile).
Maarten Bodewes,

Ergo, l'eccezione. Giusto?
Robert Harvey,

2
Aspetta cosa? Sei venuto qui per un consiglio, giusto? Quello che sto dicendo (nel mio modo apparentemente non così sottile), è che quelle eccezioni sono progettate, quindi se vuoi eliminarle, probabilmente dovresti avere una buona ragione. A proposito, sono d'accordo con Amon: probabilmente non dovresti avere una funzione che non possa accettare numeri dispari per un parametro.
Robert Harvey,

1
@MaartenBodewes: devi ricordare che padToEvenWithIsEven non esegue la convalida dell'input dell'utente. Esegue un controllo di validità sul suo input per proteggersi dagli errori di programmazione nel codice chiamante. L'ampiezza di questa convalida deve dipendere da un'analisi costi / rischi in cui si mette il costo del controllo rispetto al rischio che la persona che scrive il codice chiamante passi nel parametro errato.
Bart van Ingen Schenau,

Risposte:


7

Nel caso del tuo esempio, la soluzione migliore è utilizzare una funzione di imbottitura più generale; se il chiamante desidera raggiungere una dimensione uniforme, può verificarlo da solo.

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 esegui ripetutamente la stessa convalida su un valore o desideri consentire solo un sottoinsieme di valori di un tipo, i microtipi / i tipi minuscoli possono essere utili. Per utilità generiche come il riempimento, questa non è una buona idea, ma se il tuo valore gioca un ruolo particolare nel tuo modello di dominio, l'utilizzo di un tipo dedicato anziché di valori primitivi può essere un grande passo avanti. Qui, potresti 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 è necessario eseguire alcuna convalida interna. Per un test così semplice, un int all'interno di un oggetto sarà probabilmente più costoso in termini di prestazioni in fase di esecuzione, ma l'utilizzo del sistema di tipi a tuo vantaggio può ridurre i bug e chiarire il tuo design.

L'uso di tipi così piccoli può anche essere utile anche quando non eseguono alcuna convalida, ad esempio per chiarire una stringa che rappresenta a FirstNameda a LastName. Uso frequentemente questo modello in lingue tipicamente statiche.


La tua seconda funzione non costituisce ancora un'eccezione in alcuni casi?
Robert Harvey,

1
@RobertHarvey Sì, ad esempio in caso di puntatori null. Ma ora il controllo principale - che il numero sia pari - è costretto a lasciare la funzione sotto la responsabilità del chiamante. Possono quindi gestire l'eccezione nel modo che ritengono opportuno. Penso che la risposta si concentri meno sull'eliminazione di tutte le eccezioni che sull'eliminazione della duplicazione del codice nel codice di convalida.
Amon,

1
Bella risposta. Ovviamente in Java la penalità per la creazione di classi di dati è piuttosto elevata (un sacco di codice aggiuntivo) ma è più un problema del linguaggio che l'idea che hai presentato. Immagino che non useresti questa soluzione per tutti i casi d'uso in cui si verificherebbe il mio problema, ma è una buona soluzione contro l'uso eccessivo di primitivi o per casi limite in cui il costo del controllo dei parametri è eccezionalmente difficile.
Maarten Bodewes,

1
@MaartenBodewes Se vuoi fare la validazione, non puoi liberarti di qualcosa come le eccezioni. Bene, l'alternativa è usare una funzione statica che restituisce un oggetto validato o null in caso di fallimento, ma che sostanzialmente non ha vantaggi. Ma spostando la validazione fuori dalla funzione nel costruttore, ora possiamo chiamare la funzione senza la possibilità di un errore di validazione. Questo dà al chiamante tutto il controllo. Ad esempio:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon,

1
@MaartenBodewes La convalida duplicata avviene sempre. Ad esempio, prima di provare a chiudere una porta è possibile verificare se è già chiusa, ma la porta stessa non consentirà di richiuderla se lo è già. Non c'è modo di uscire da questo, tranne se ti fidi sempre che il chiamante non esegue alcuna operazione non valida. Nel tuo caso potresti avere unsafePadStringToEvenun'operazione che non esegue alcun controllo, ma sembra una cattiva idea solo per evitare la convalida.
Plalx,

9

Come estensione della risposta di @amon, possiamo combinare il suo EvenIntegercon quello che la comunità di programmazione funzionale potrebbe definire un "costruttore intelligente" - una funzione che avvolge il costruttore muto e si assicura che l'oggetto sia in uno stato valido (facciamo il muto classe costruttore o in moduli / pacchetto di lingue non basati su classe privato per assicurarsi che vengano utilizzati solo i costruttori intelligenti). Il trucco è restituire un Optional(o equivalente) per rendere la funzione più compostabile.

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 facilmente utilizzare Optionalmetodi standard 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());
    }
}

Puoi 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 EvenIntegercontare con certezza che sarà davvero pari e che il nostro controllo pari è stato scritto solo una volta, in EvenInteger::of.


Opzionale in generale rappresenta abbastanza bene dati potenzialmente non validi. Ciò è analogo a una moltitudine di metodi TryParse, che restituiscono sia i dati che un flag che indica la validità dell'input. Con controlli di tempo conformi.
Basilevs,

1

Eseguire 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 controllo isEven che è stato fatto è una convalida dell'input, il fallimento comporta la richiesta di un nuovo input. Il secondo controllo è completamente indipendente dal primo, in quanto è su un metodo pubblico padToEvenWithEven che può essere chiamato dall'esterno della classe e ha un risultato diverso (l'eccezione).

Il problema è simile al problema del codice accidentalmente identico che viene confuso per non secco. 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 stanno servendo lo stesso scopo e che possano sempre essere intercambiabili. Inoltre, probabilmente la tua classe farà troppo, ma salta questo dato che questo è probabilmente solo un esempio di giocattolo ...

Se questo è un problema di prestazioni, puoi risolvere il problema creando un metodo privato, che non esegue alcuna convalida, che il tuo padPoEvenWithEven pubblico ha chiamato dopo aver effettuato la convalida e che invece chiamerebbe l'altro tuo metodo. Se non si tratta di un problema di prestazioni, lasciare che i diversi metodi eseguano i controlli necessari per eseguire le attività assegnate.


OP ha dichiarato che la convalida dell'input viene eseguita per impedire il lancio della funzione. Quindi i controlli sono completamente dipendenti e intenzionalmente uguali.
D Drmmr,

@DDrmmr: no, non sono dipendenti. La funzione viene generata perché fa parte del suo contratto. Come ho detto, la risposta è creare un metodo privato che fa tutto tranne che per lanciare l'eccezione e quindi fare in modo che il metodo pubblico chiami il metodo privato. Il metodo pubblico mantiene il controllo, dove ha uno scopo diverso: gestire input non convalidati.
jmoreno,
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.