Como executar a validação de entrada sem exceções ou redundância

11

Quando estou tentando criar uma interface para um programa específico, geralmente estou tentando evitar exceções que dependem de entradas não validadas.

Então, o que geralmente acontece é que eu pensei em um pedaço de código como este (este é apenas um exemplo para o exemplo, não se importe com a função que ele executa, por exemplo, em 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, digamos que evenSizeseja realmente derivado da entrada do usuário. Então, eu não tenho certeza se é mesmo. Mas não quero chamar esse método com a possibilidade de uma exceção ser lançada. Então, eu faço a seguinte função:

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

mas agora eu tenho duas verificações que executam a mesma validação de entrada: a expressão na ifinstrução e a verificação explícita isEven. Código duplicado, nada legal, então vamos refatorar:

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, isso resolveu, mas agora entramos na seguinte situação:

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)

agora temos uma verificação redundante: já sabemos que o valor é par, mas padToEvenWithIsEvenainda realiza a verificação do parâmetro, que sempre retornará verdadeiro, como já chamamos essa função.

Agora isEven, é claro, não representa um problema, mas se a verificação de parâmetros for mais complicada, isso poderá resultar em um custo muito alto. Além disso, realizar uma chamada redundante simplesmente não parece certo.

Às vezes, podemos contornar isso introduzindo um "tipo validado" ou criando uma função onde esse problema não pode ocorrer:

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();
}

mas isso requer um pensamento inteligente e um refator bastante grande.

Existe uma maneira (mais) genérica pela qual podemos evitar chamadas redundantes isEvene executar a verificação de duplo parâmetro? Eu gostaria que a solução não chamasse padToEvencom um parâmetro inválido, acionando a exceção.


Sem exceções, não quero dizer programação livre de exceções, quero dizer que a entrada do usuário não aciona uma exceção por design, enquanto a própria função genérica ainda contém a verificação de parâmetros (apenas para proteger contra erros de programação).

Maarten Bodewes
fonte
Você está apenas tentando remover a exceção? Você pode fazer isso assumindo qual deve ser o tamanho real. Por exemplo, se você passar 13, use 12 ou 14 e evite a verificação por completo. Se você não pode fazer uma dessas suposições, ficará com a exceção porque o parâmetro é inutilizável e sua função não pode continuar.
Robert Harvey
@RobertHarvey Isso - na minha opinião - vai direto contra o princípio da menor surpresa e também contra o princípio do fracasso rápido. É como retornar nulo se o parâmetro de entrada for nulo (e depois esquecer de manipular o resultado corretamente, é claro, olá NPE inexplicável).
Maarten Bodewes
Ergo, a exceção. Direita?
Robert Harvey
2
Espere o que? Você veio aqui para pedir conselhos, certo? O que estou dizendo (da minha maneira aparentemente não tão sutil) é que essas exceções são intencionais, por isso , se você deseja eliminá-las, provavelmente deve ter um bom motivo. A propósito, eu concordo com amon: você provavelmente não deve ter uma função que não possa aceitar números ímpares para um parâmetro.
Robert Harvey
1
@MaartenBodewes: Você deve se lembrar que padToEvenWithIsEven não executa a validação da entrada do usuário. Ele executa uma verificação de validade em sua entrada para se proteger contra erros de programação no código de chamada. A extensão dessa validação depende de uma análise de custo / risco em que você coloca o custo da verificação no risco de que a pessoa que está escrevendo o código de chamada passe no parâmetro errado.
Bart van Ingen Schenau

Respostas:

7

No caso do seu exemplo, a melhor solução é usar uma função de preenchimento mais geral; se o chamador quiser ter um tamanho uniforme, ele poderá verificar isso sozinho.

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 você executar repetidamente a mesma validação em um valor ou desejar permitir apenas um subconjunto de valores de um tipo, os microtipos / tipos minúsculos podem ser úteis. Para utilitários de uso geral, como o preenchimento, essa não é uma boa ideia, mas se o seu valor desempenhar um papel específico no modelo de domínio, usar um tipo dedicado em vez de valores primitivos pode ser um grande passo à frente. Aqui, você pode definir:

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;
  }
}

Agora você pode declarar

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

e não precisa fazer nenhuma validação interna. Para um teste tão simples, envolver um int dentro de um objeto provavelmente será mais caro em termos de desempenho em tempo de execução, mas usar o sistema de tipos como vantagem pode reduzir bugs e esclarecer seu design.

Usar esses tipos minúsculos pode até ser útil, mesmo quando eles não realizam validação, por exemplo, para desambiguar uma string representando a FirstNamede a LastName. Eu freqüentemente uso esse padrão em linguagens estaticamente tipadas.

amon
fonte
Sua segunda função ainda não gera uma exceção em alguns casos?
Robert Harvey
1
@RobertHarvey Sim, por exemplo, no caso de ponteiros nulos. Mas agora a verificação principal - de que o número é par - é forçada a sair da função, sob responsabilidade do chamador. Eles podem então lidar com a exceção da maneira que acharem melhor. Eu acho que a resposta se concentra menos em se livrar de todas as exceções do que em se livrar da duplicação de código no código de validação.
14487 amon
1
Ótima resposta. Obviamente, em Java, a penalidade para criar classes de dados é bastante alta (muitos códigos adicionais), mas isso é mais um problema da linguagem do que a idéia que você apresentou. Eu acho que você não usaria essa solução para todos os casos de uso em que meu problema ocorreria, mas é uma boa solução contra o uso excessivo de primitivos ou para casos extremos onde o custo da verificação de parâmetros é excepcionalmente difícil.
Maarten Bodewes
1
@MaartenBodewes Se você deseja fazer a validação, não pode se livrar de algo como exceções. Bem, a alternativa é usar uma função estática que retorna um objeto validado ou nulo na falha, mas que basicamente não tem vantagens. Mas, movendo a validação da função para o construtor, agora podemos chamar a função sem a possibilidade de um erro de validação. Isso dá ao chamador todo o controle. Por exemplo:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon
1
@MaartenBodewes A validação duplicada acontece o tempo todo. Por exemplo, antes de tentar fechar uma porta, você pode verificar se ela já está fechada, mas a porta também não permitirá que ela seja novamente fechada, se já estiver. Não há como escapar disso, exceto se você sempre confiar que o chamador não realiza nenhuma operação inválida. No seu caso, você pode ter uma unsafePadStringToEvenoperação que não realiza nenhuma verificação, mas parece uma má idéia apenas para evitar a validação.
plalx
9

Como uma extensão da resposta do @ amon, podemos combiná-lo EvenIntegercom o que a comunidade de programação funcional pode chamar de "Construtor Inteligente" - uma função que envolve o construtor burro e garante que o objeto esteja em um estado válido (fazemos o burro classe de construtor ou em módulo / pacote de linguagens não baseadas em classe privado (para garantir que apenas os construtores inteligentes sejam usados). O truque é retornar um Optional(ou equivalente) para tornar a função mais compostável.

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;
    }
}

Podemos então facilmente usar Optionalmétodos padrão para escrever a lógica de entrada:

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

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

Você também pode escrever keepAsking()em um estilo Java mais idiomático com um loop do-while.

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

return result.get();

No restante do seu código, você pode confiar, EvenIntegercom certeza, que ele realmente será equilibrado e que nosso cheque par foi escrito apenas uma vez EvenInteger::of.

Walpen
fonte
Opcional em geral representa dados potencialmente inválidos muito bem. Isso é análogo a uma infinidade de métodos TryParse, que retornam dados e um sinalizador indicando a validade da entrada. Com verificações de conformidade.
Basilevs
1

Fazer a validação duas vezes é um problema se o resultado da validação for o mesmo E a validação estiver sendo feita na mesma classe. Esse não é o seu exemplo. No seu código refatorado, a primeira verificação isEven feita é uma validação de entrada; a falha resulta em uma nova entrada sendo solicitada. A segunda verificação é completamente independente da primeira, como em um método público padToEvenWithEven que pode ser chamado de fora da classe e tem um resultado diferente (a exceção).

Seu problema é semelhante ao de código acidentalmente idêntico sendo confundido por não-seco. Você está confundindo a implementação com o design. Eles não são os mesmos, e só porque você tem uma ou uma dúzia de linhas iguais, não significa que elas estejam servindo ao mesmo propósito e possam ser consideradas para sempre intercambiáveis. Além disso, provavelmente a sua turma faz muito, mas pule isso, pois esse provavelmente é apenas um exemplo de brinquedo ...

Se esse é um problema de desempenho, você pode resolvê-lo criando um método privado, que não faz nenhuma validação, que seu padToEvenWithEven público chamou depois de fazer sua validação e que outro método chamaria. Se não for um problema de desempenho, deixe seus métodos diferentes fazerem as verificações necessárias para executar as tarefas atribuídas.

jmoreno
fonte
O OP afirmou que a validação de entrada é feita para impedir o lançamento da função. Portanto, as verificações são completamente dependentes e intencionalmente iguais.
D Drmmr
@ DDrmmr: não, eles não são dependentes. A função é lançada porque isso faz parte do contrato. Como eu disse, a resposta é criar um método privado que faz tudo, exceto lançar a exceção e fazer com que o método público chame o método privado. O método público retém a verificação, onde serve a um propósito diferente - manipular entrada não validada.
jmoreno