Um método deve perdoar os argumentos passados? [fechadas]

21

Suponha que temos um método foo(String bar)que opera apenas em strings que atendem a determinados critérios; por exemplo, ele deve estar em minúsculas, não deve estar vazio ou ter apenas espaço em branco e deve corresponder ao padrão [a-z0-9-_./@]+. A documentação para o método afirma esses critérios.

O método deve rejeitar todo e qualquer desvio deste critério ou deve ser mais tolerante com alguns critérios? Por exemplo, se o método inicial for

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
    }
    this.bar = bar;
}

E o segundo método de perdão é

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        bar = bar.toLowerCase().trim().replaceAll(" ", "_");
        if (!bar.matches(BAR_PATTERN_STRING) {
            throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
        }
    }
    this.bar = bar;
}

A documentação deve ser alterada para indicar que será transformada e configurada com o valor transformado, se possível, ou o método deve ser o mais simples possível e rejeitar todos e quaisquer desvios? Nesse caso, barpode ser definido pelo usuário de um aplicativo.

O principal caso de uso para isso seria usuários acessando objetos de um repositório por um identificador de cadeia específico. Cada objeto no repositório deve ter uma sequência única para identificá-lo. Esses repositórios podiam armazenar os objetos de várias maneiras (servidor sql, json, xml, binário etc.) e, portanto, tentei identificar o menor denominador comum que corresponderia à maioria das convenções de nomenclatura.

Zymus
fonte
1
Provavelmente isso depende muito do seu caso de uso. Qualquer um deles pode ser razoável, e eu já vi classes que fornecem os dois métodos e fazem o usuário decidir. Você poderia elaborar o que esse método / classe / campo deve fazer, para que possamos oferecer alguns conselhos reais?
Ixrec
1
Você conhece todos que chamam o método? Por exemplo, se você mudar, poderá identificar todos os clientes de maneira confiável? Se assim for, eu seria tão permissivo e perdoador quanto as preocupações de desempenho permitirem. Eu também posso excluir a documentação. Caso contrário, e faça parte de uma API da biblioteca, eu garantiria que o código implementasse exatamente a API anunciada, caso contrário, alterar o código para corresponder à documentação no futuro poderá gerar relatórios de erros.
9139 Jon Chesterfield
7
Você pode argumentar que a Separação de Preocupações diz que, se necessário, você deve ter uma foofunção estrita, rigorosa nos argumentos que aceita, e ter uma segunda função auxiliar que possa tentar "limpar" um argumento a ser usado foo. Dessa forma, cada método tem menos a fazer por conta própria e eles podem ser gerenciados e integrados de maneira mais limpa. Se seguir esse caminho, provavelmente também seria útil afastar-se de um design pesado de exceção; você pode usar algo como isso Optionale, em seguida, ter as funções que consomem foolançam exceções, se necessário.
Gntskn
1
É como perguntar "alguém me prejudicou, devo perdoá-los?" Obviamente, existem circunstâncias em que um ou outro é apropriado. A programação pode não ser tão complicada quanto as relações humanas, mas é definitivamente suficientemente complexo para que uma receita geral como essa não funcione.
Kilian Foth
2
@ Boggin Gostaria também de referir o Princípio da Robustez Reconsiderado . A dificuldade surge quando você precisa expandir a implementação e a implementação que perdoa leva a um caso ambíguo com a implementação expandida.

Respostas:

47

Seu método deve fazer o que diz que faz.

Isso evita que erros, tanto do uso quanto dos mantenedores, alterem o comportamento posteriormente. Isso economiza tempo, porque os mantenedores não precisam gastar tanto tempo para descobrir o que está acontecendo.

Dito isto, se a lógica definida não for amigável, talvez deva ser melhorada.

Telastyn
fonte
8
Essa é a chave. Se o seu método fizer exatamente o que diz, o codificador que usará o método compensará o caso de uso específico. Nunca faça algo não documentado com o método apenas porque você acha que é útil. Se você precisar alterá-lo, escreva um contêiner ou altere a documentação.
Nelson
Eu acrescentaria ao comentário de @ Nelson que o método não deve ser projetado no vácuo. Se os codificadores disserem que o usarão, mas compensarão e suas compensações tiverem valor de propósito geral, considere fazer parte da classe. (Por exemplo, tem fooe fooForUncleanStringmétodos em que este último faz as correções antes de passá-lo para o primeiro.)
Blrfl
20

Existem alguns pontos:

  1. Sua implementação deve fazer o que o contrato documentado indica e não deve fazer mais nada.
  2. A simplicidade é importante, tanto para o contrato quanto para a implementação, embora mais para o primeiro.
  3. Tentar corrigir entradas incorretas aumenta a complexidade, contra-intuitivamente não apenas o contrato e a implementação, mas também o uso.
  4. Os erros só devem ser detectados com antecedência se isso melhorar a depuração e não comprometer muito a eficiência.
    Lembre-se de que existem asserções de depuração para diagnosticar erros lógicos no modo de depuração, o que alivia principalmente os problemas de desempenho.
  5. A eficiência, na medida em que o tempo e o dinheiro disponíveis permitem sem comprometer demais a simplicidade, é sempre uma meta.

Se você implementar uma interface do usuário, mensagens de erro amigáveis ​​(incluindo sugestões e outra ajuda) fazem parte de um bom design.
Mas lembre-se de que as APIs são para programadores, não para usuários finais.


Um experimento da vida real em ser confuso e permissivo com a entrada é o HTML.
O que resultou em todo mundo fazendo isso de maneira um pouco diferente, e a especificação, agora está documentada, é um volume gigantesco cheio de casos especiais.
Veja a lei de Postel (" Seja conservador no que faz, seja liberal no que aceita dos outros " . ) E um crítico tocando nisso ( ou um muito melhor que MichaelT me fez conhecer ).

Desduplicador
fonte
Outra peça fundamental pelo autor do sendmail: A Robustez Princípio Reconsidered
15

O comportamento de um método deve ser claro, intuitivo, previsível e simples. Em geral, devemos ser muito hesitante em fazer processamento extra na entrada de um chamador. Tais suposições sobre o que o chamador pretendia invariavelmente têm muitos casos extremos que produzem comportamentos indesejados. Considere uma operação tão simples como ingressar no caminho do arquivo. Muitas (ou talvez a maioria) das funções de junção de caminhos de arquivos descartam silenciosamente quaisquer caminhos anteriores, se um dos caminhos que estiver sendo associado parecer estar enraizado! Por exemplo, /abc/xyzjuntado com /evilresultará em apenas /evil. Isso quase nunca é o que pretendo quando ingresso nos caminhos de arquivos, mas como não há uma interface que não se comporte dessa maneira, sou forçado a ter bugs ou a escrever códigos extras que cobrem esses casos.

Dito isto, há raras ocasiões em que faz sentido que um método seja "perdoador", mas deve sempre estar ao alcance do chamador decidir quando e se essas etapas de processamento se aplicam à sua situação. Portanto, quando você identificar uma etapa comum de pré-processamento que deseja aplicar aos argumentos em várias situações, exponha as interfaces para:

  • A funcionalidade bruta, sem qualquer pré-processamento.
  • A etapa de pré-processamento por si só .
  • A combinação da funcionalidade bruta e o pré-processamento.

O último é opcional; você deve fornecê-lo apenas se um grande número de chamadas o usar.

A exposição da funcionalidade bruta oferece ao chamador a capacidade de usá-la sem a etapa de pré-processamento, quando necessário. Expor a etapa do pré-processador por si só permite que o chamador a use em situações em que nem sequer está chamando a função ou quando deseja pré-processar alguma entrada antes de chamar a função (como quando deseja passar primeiro para outra função). O fornecimento da combinação permite que os chamadores invoquem ambos sem problemas, o que é útil principalmente se a maioria dos chamadores a usar dessa maneira.

jpmc26
fonte
2
+1 para previsível. E outro +1 (desejo) para simples. Prefiro que você me ajude a identificar e corrigir meus erros do que tentar escondê-los.
John M Gant
4

Como outros já disseram, fazer a correspondência de "perdoar" significa introduzir complexidade adicional. Isso significa mais trabalho na implementação da correspondência. Agora você tem muitos outros casos de teste, por exemplo. Você precisa fazer um trabalho adicional para garantir que não haja nomes semanticamente iguais no espaço para nome. Mais complexidade também significa que há mais coisas a dar errado no futuro. Um mecanismo mais simples, como uma bicicleta, requer menos manutenção do que um mais complexo, como um carro.

Então, a correspondência de cadeia branda vale todo esse custo extra? Depende do caso de uso, como outros observaram. Se as seqüências de caracteres são algum tipo de entrada externa sobre a qual você não tem controle e há uma vantagem definida na correspondência branda, pode valer a pena. Talvez a entrada seja proveniente de usuários finais que podem não ter muita consciência sobre caracteres espaciais e letras maiúsculas, e você tem um forte incentivo para tornar seu produto mais fácil de usar.

Por outro lado, se a entrada vier de, digamos, arquivos de propriedades montados por pessoas técnicas, que deveriam entender isso "Fred Mertz" != "FredMertz", eu estaria mais inclinado a tornar a correspondência mais rígida e economizar o custo de desenvolvimento.

Eu acho que, de qualquer forma, há valor em aparar e desconsiderar os espaços iniciais e finais - já vi muitas horas desperdiçadas na depuração desses tipos de problemas.

mat_noshi
fonte
3

Você menciona parte do contexto em que essa pergunta vem.

Dado que, eu gostaria que o método fizesse apenas uma coisa, ele afirma os requisitos da string, deixe-o executar com base nisso - eu não tentaria transformá-lo aqui. Mantenha-o simples e claro; documente e tente manter a documentação e o código sincronizados.

Se você deseja transformar os dados que vêm do banco de dados do usuário de uma maneira mais indulgente, coloque essa funcionalidade em um método de transformação separado e documente a funcionalidade associada .

Em algum momento, os requisitos da função precisam ser medidos, claramente documentados e a execução deve continuar. O "perdão", no momento, é um pouco mudo, é uma decisão de design e eu argumentaria que a função não muda seu argumento. Tendo a função modificada, a entrada oculta algumas das validações que seriam necessárias ao cliente. Ter uma função que faz a mutação ajuda o cliente a acertar.

A grande ênfase aqui é a clareza e a documentação do que o código faz .

Niall
fonte
-1
  1. Você pode nomear um método de acordo com a ação como doSomething (), takeBackUp ().
  2. Para facilitar a manutenção, você pode manter os contratos e a validação comuns em diferentes procedimentos. Chame-os conforme os casos de uso.
  3. Programação defensiva: seu procedimento lida com uma ampla gama de entradas, incluindo (o mínimo de casos de uso deve ser coberto de qualquer maneira)
user435491
fonte