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, bar
pode 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.
fonte
foo
função estrita, rigorosa nos argumentos que aceita, e ter uma segunda função auxiliar que possa tentar "limpar" um argumento a ser usadofoo
. 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 issoOptional
e, em seguida, ter as funções que consomemfoo
lançam exceções, se necessário.Respostas:
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.
fonte
foo
efooForUncleanString
métodos em que este último faz as correções antes de passá-lo para o primeiro.)Existem alguns pontos:
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.
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 ).
fonte
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/xyz
juntado com/evil
resultará 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:
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.
fonte
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.
fonte
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 .
fonte
fonte