Geralmente encontro métodos / funções que possuem um parâmetro booleano adicional que controla se uma exceção é lançada em caso de falha ou se é retornado nulo.
Já existem discussões sobre qual delas é a melhor escolha, em qual caso, então não vamos nos concentrar nisso aqui. Veja, por exemplo, Retornar valor mágico, lançar exceção ou retornar falso em caso de falha?
Vamos supor que há uma boa razão pela qual queremos apoiar os dois lados.
Pessoalmente, acho que esse método deveria ser dividido em dois: um que lança uma exceção na falha e o outro que retorna nulo na falha.
Então, qual é o melhor?
A: Um método com $exception_on_failure
parâmetro.
/**
* @param int $id
* @param bool $exception_on_failure
*
* @return Item|null
* The item, or null if not found and $exception_on_failure is false.
* @throws NoSuchItemException
* Thrown if item not found, and $exception_on_failure is true.
*/
function loadItem(int $id, bool $exception_on_failure): ?Item;
B: Dois métodos distintos.
/**
* @param int $id
*
* @return Item|null
* The item, or null if not found.
*/
function loadItemOrNull(int $id): ?Item;
/**
* @param int $id
*
* @return Item
* The item, if found (exception otherwise).
*
* @throws NoSuchItemException
* Thrown if item not found.
*/
function loadItem(int $id): Item;
EDIT: C: Algo mais?
Muitas pessoas sugeriram outras opções ou afirmam que A e B são defeituosos. Tais sugestões ou opiniões são bem-vindas, relevantes e úteis. Uma resposta completa pode conter essas informações extras, mas também abordará a questão principal de saber se um parâmetro para alterar a assinatura / comportamento é uma boa idéia.
Notas
Caso alguém esteja se perguntando: Os exemplos estão em PHP. Mas acho que a pergunta se aplica a todas as linguagens, desde que sejam algo semelhantes ao PHP ou Java.
fonte
loadItemOrNull(id)
, considere seloadItemOr(id, defaultItem)
faz sentido para você. Se o item é uma String ou número, geralmente é o caso.Respostas:
Você está correto: dois métodos são muito melhores para isso, por várias razões:
Em Java, a assinatura do método que gera uma exceção potencialmente incluirá essa exceção; o outro método não. Isso torna particularmente claro o que esperar de um e de outro.
Em idiomas como C #, onde a assinatura do método não diz nada sobre as exceções, os métodos públicos ainda devem ser documentados e essa documentação inclui as exceções. Documentar um único método não seria fácil.
Seu exemplo é perfeito: os comentários no segundo trecho de código parecem muito mais claros, e eu diria até “O item, se encontrado (exceção em contrário).” Até “O item”. - a presença de uma possível exceção e a A descrição que você forneceu é auto-explicativa.
No caso de um único método, existem poucos casos em que você deseja alternar o valor do parâmetro booleano no tempo de execução e, se o fizer, isso significa que o chamador terá que lidar com os dois casos (uma
null
resposta e a exceção ), tornando o código muito mais difícil do que precisa. Como a escolha é feita não em tempo de execução, mas ao escrever o código, dois métodos fazem todo o sentido.Algumas estruturas, como o .NET Framework, estabeleceram convenções para essa situação e a resolvem com dois métodos, como você sugeriu. A única diferença é que eles usam um padrão explicado por Ewan em sua resposta , então
int.Parse(string): int
lança uma exceção, enquantoint.TryParse(string, out int): bool
não usa. Essa convenção de nomenclatura é muito forte na comunidade do .NET e deve ser seguida sempre que o código corresponder à situação que você descreve.fonte
int.Parse()
é considerada uma péssima decisão de design, e é exatamente por isso que a equipe do .NET foi adiante e implementouTryParse
.->itemExists($id)
ligue antes de ligar->loadItem($id)
. Ou talvez seja apenas uma escolha feita por outras pessoas no passado, e temos que tomar isso como garantido.data Maybe a = Just a | Nothing
).Uma variação interessante é a linguagem Swift. No Swift, você declara uma função como "lançamento", ou seja, é permitido gerar erros (semelhante, mas não exatamente o mesmo que as exceções Java). O chamador pode chamar esta função de quatro maneiras:
uma. Em uma instrução try / catch, captura e manipulação da exceção.
b. Se o chamador for declarado jogando, basta chamá-lo e qualquer erro lançado se transformará em um erro lançado pelo chamador.
c. Marcar a chamada de função com o
try!
que significa "Tenho certeza de que esta chamada não será lançada". Se a função chamada não lançada, é garantido que o aplicativo falhe.d. Marcar a chamada de função com o
try?
que significa "Eu sei que a função pode ser lançada, mas não me importo com qual erro exatamente é lançado". Isso altera o valor de retorno da função do tipo "T
" para o tipo "optional<T>
" e uma exceção lançada é transformada em retorno nulo.(a) e (d) são os casos que você está perguntando. E se a função puder retornar nulo e lançar exceções? Nesse caso, o tipo do valor de retorno seria "
optional<R>
" para algum tipo R e (d) mudaria isso para "optional<optional<R>>
", o que é um pouco enigmático e difícil de entender, mas completamente correto. E uma função Swift pode retornaroptional<Void>
; portanto, (d) pode ser usada para lançar funções retornando Void.fonte
Eu gosto da
bool TryMethodName(out returnValue)
abordagem.Ele fornece uma indicação conclusiva sobre se o método foi bem-sucedido ou não e se a nomeação corresponde à quebra do método de exceção em um bloco try catch.
Se você retornar nulo, não saberá se ele falhou ou se o valor de retorno foi legitimamente nulo.
por exemplo:
exemplo de uso (significa saída por referência não inicializada)
fonte
bool TryMethodName(out returnValue)
abordagem" no exemplo original?Normalmente, um parâmetro booleano indica que uma função pode ser dividida em duas e, como regra, você deve sempre dividi-la em vez de passar em um booleano.
fonte
b
: Em vez de chamar,foo(…, b);
você deve escreverif (b) then foo_x(…) else foo_y(…);
e repetir os outros argumentos, ou algo como((b) ? foo_x : foo_y)(…)
se a linguagem tiver um operador ternário e funções / métodos de primeira classe. E isso talvez até se repita em vários lugares diferentes do programa.if (myGrandmaMadeCookies) eatThem() else makeFood()
que sim, eu poderia envolver em outra função como estacheckIfShouldCook(myGrandmaMadeCookies)
. Eu me pergunto se a nuance que você mencionou tem a ver com o fato de estarmos passando um booleano sobre como queremos que a função se comporte, como na pergunta original, vs. estamos passando algumas informações sobre o nosso modelo, como no exemplo da avó que acabei de dar.O Código Limpo recomenda evitar argumentos de sinalização booleana, porque seu significado é opaco no site da chamada:
Considerando que métodos separados podem usar nomes expressivos:
fonte
Se eu tivesse que usar A ou B, usaria B, dois métodos separados, pelas razões expostas na resposta de Arseni Mourzenkos .
Porém, existe outro método 1 : em Java, ele é chamado
Optional
, mas você pode aplicar o mesmo conceito a outras linguagens em que pode definir seus próprios tipos, se a linguagem ainda não fornecer uma classe semelhante.Você define seu método assim:
No seu método, você retorna
Optional.of(item)
se encontrou o item ou retornaOptional.empty()
caso não o encontre . Você nunca joga 2 e nunca voltanull
.Para o cliente do seu método, é algo parecido
null
, mas com a grande diferença que ele força a pensar no caso de itens ausentes. O usuário não pode simplesmente ignorar o fato de que pode não haver resultado. Para obter o item daOptional
ação explícita, é necessário:loadItem(1).get()
jogará umNoSuchElementException
ou retornará o item encontrado.loadItem(1).orElseThrow(() -> new MyException("No item 1"))
deve usar uma exceção personalizada se o retornadoOptional
estiver vazio.loadItem(1).orElse(defaultItem)
retorna o item encontrado ou o passadodefaultItem
(que também pode sernull
) sem gerar uma exceção.loadItem(1).map(Item::getName)
retornaria novamente umOptional
com o nome dos itens, se presente.Optional
.A vantagem é que agora cabe ao código do cliente o que deve acontecer se não houver nenhum item e você ainda precisa fornecer um único método .
1 Acho que é algo parecido com a resposta do
try?
in gnasher729, mas não está totalmente claro para mim, pois não conheço o Swift e parece ser um recurso de linguagem, por isso é específico do Swift.2 Você pode lançar exceções por outros motivos, por exemplo, se você não souber se existe um item ou não, porque teve problemas de comunicação com o banco de dados.
fonte
Como outro ponto de concordar com o uso de dois métodos, isso pode ser muito fácil de implementar. Escreverei meu exemplo em C #, pois não conheço a sintaxe do PHP.
fonte
Eu prefiro dois métodos, dessa forma, você não apenas me dirá que está retornando um valor, mas qual valor exatamente.
O TryDoSomething () também não é um padrão ruim.
Estou mais acostumado a ver o primeiro em interações com o banco de dados, enquanto o último é mais usado em coisas de análise.
Como outros já disseram, parâmetros de sinalização geralmente são cheiros de código (cheiros de código não significam que você esteja fazendo algo errado, apenas que há uma probabilidade de que você esteja fazendo errado). Embora você o nomeie com precisão, às vezes existem nuances que tornam difícil saber como usar essa bandeira e você acaba procurando dentro do corpo do método.
fonte
Enquanto outras pessoas sugerem o contrário, eu sugeriria que ter um método com um parâmetro para indicar como os erros devem ser tratados é melhor do que exigir o uso de métodos separados para os dois cenários de uso. Embora possa ser desejável também ter pontos de entrada distintos para os usos "erros lançados" versus "retornos de erro de indicação de erro", ter um ponto de entrada que possa atender aos dois padrões de uso evitará a duplicação de código nos métodos de wrapper. Como um exemplo simples, se houver funções:
e se deseja funções que se comportam da mesma forma, mas com x entre colchetes, seria necessário escrever dois conjuntos de funções de invólucro:
Se houver um argumento sobre como lidar com erros, apenas um wrapper seria necessário:
Se o wrapper for simples o suficiente, a duplicação de código pode não ser muito ruim, mas se precisar mudar, a duplicação do código exigirá duplicar as alterações. Mesmo se optar por adicionar pontos de entrada que não usem o argumento extra:
pode-se manter toda a "lógica de negócios" em um método - aquele que aceita um argumento indicando o que fazer em caso de falha.
fonte
Penso que todas as respostas que explicam que você não deve ter uma bandeira que controla se uma exceção é lançada ou não são boas. O conselho deles seria o meu conselho para quem sente a necessidade de fazer essa pergunta.
No entanto, gostaria de salientar que há uma exceção significativa a essa regra. Quando você tiver avançado o suficiente para começar a desenvolver APIs que centenas de outras pessoas usarão, há casos em que você pode querer fornecer esse sinalizador. Quando você está escrevendo uma API, não está apenas escrevendo para os puristas. Você está escrevendo para clientes reais que têm desejos reais. Parte do seu trabalho é fazê-los felizes com sua API. Isso se torna um problema social, e não um problema de programação, e às vezes os problemas sociais ditam soluções que seriam menos do que ideais como soluções de programação por conta própria.
Você pode achar que possui dois usuários distintos da sua API, um dos quais deseja exceções e outro que não. Um exemplo da vida real onde isso pode ocorrer é com uma biblioteca usada tanto no desenvolvimento quanto em um sistema incorporado. Em ambientes de desenvolvimento, os usuários provavelmente vão querer ter exceções em todos os lugares. Exceções são muito populares para lidar com situações inesperadas. No entanto, eles são proibidos em muitas situações incorporadas porque são muito difíceis de analisar restrições em tempo real. Quando você realmente se preocupa não apenas com o tempo médio necessário para executar sua função, mas com o tempo que leva para uma diversão individual, a idéia de relaxar a pilha em qualquer local arbitrário é muito indesejável.
Um exemplo da vida real para mim: uma biblioteca de matemática. Se sua biblioteca de matemática possui uma
Vector
classe que suporta a obtenção de um vetor de unidade na mesma direção, você deve ser capaz de dividir pela magnitude. Se a magnitude for 0, você terá uma situação de divisão por zero. No desenvolvimento, você quer pegar esses . Você realmente não quer uma divisão surpreendente por zeros por aí. Lançar uma exceção é uma solução muito popular para isso. Quase nunca acontece (é um comportamento verdadeiramente excepcional), mas quando acontece, você quer saber.Na plataforma incorporada, você não deseja essas exceções. Faria mais sentido fazer uma verificação
if (this->mag() == 0) return Vector(0, 0, 0);
. Na verdade, eu vejo isso em código real.Agora pense da perspectiva de um negócio. Você pode tentar ensinar duas maneiras diferentes de usar uma API:
Isso satisfaz as opiniões da maioria das respostas aqui, mas da perspectiva da empresa isso é indesejável. Os desenvolvedores incorporados terão que aprender um estilo de codificação, e os desenvolvedores de desenvolvimento terão que aprender outro. Isso pode ser bastante desagradável e força as pessoas a uma maneira de pensar. Potencialmente pior: como você prova que a versão incorporada não inclui nenhum código de exceção? A versão lançada pode se misturar facilmente, ocultando-se em algum lugar no seu código até que um caro Comitê de Revisão de Falhas o encontre.
Se, por outro lado, você tiver um sinalizador que ativa ou desativa o tratamento de exceções, os dois grupos podem aprender exatamente o mesmo estilo de codificação. De fato, em muitos casos, você pode até usar o código de um grupo nos projetos do outro grupo. No caso deste código usar
unit()
, se você realmente se importa com o que acontece em uma divisão por 0, deve ter escrito o teste você mesmo. Portanto, se você o mover para um sistema incorporado, onde você obtém maus resultados, esse comportamento é "sadio". Agora, da perspectiva dos negócios, treino meus codificadores uma vez e eles usam as mesmas APIs e os mesmos estilos em todas as partes da minha empresa.Na grande maioria dos casos, você deseja seguir o conselho de outras pessoas: use expressões idiomáticas como
tryGetUnit()
ou semelhantes para funções que não geram exceções e, em vez disso, retorne um valor sentinela como nulo. Se você acha que os usuários podem querer usar o código de manipulação de exceções e não exceções lado a lado em um único programa, atenha-se àtryGetUnit()
notação. No entanto, há um caso em que a realidade dos negócios pode melhorar o uso de uma bandeira. Se você se encontrar nessa esquina, não tenha medo de lançar as "regras" pelo caminho. É para isso que servem as regras!fonte