Parâmetro para controlar se deve lançar uma exceção ou retornar nula - boa prática?

24

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_failureparâ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.

Don Quixote
fonte
5
Enquanto eu tenho que trabalhar em PHP e sou bastante proficiente, é simplesmente uma linguagem ruim e sua biblioteca padrão está em todo lugar. Leia eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design para referência. Portanto, não é realmente surpreendente que alguns desenvolvedores de PHP adotem maus hábitos. Mas ainda não vi esse design em particular.
Polygnome
Estou faltando um ponto essencial nas respostas dadas: quando você usaria uma ou outra. Você usaria o método de exceção caso um item não encontrado fosse inesperado e considerado um erro (hora de entrar em pânico). Você usaria o método Try ... caso um item ausente não seja improvável e certamente não seja um erro, mas apenas uma possibilidade que você inclua em seu controle de fluxo normal.
Martin Maat
Veja também: stackoverflow.com/questions/34439782/…
Martin Maat
11
@ Polygnome Você está certo, as funções padrão da biblioteca estão por toda parte, existe muito código ruim e existe o problema de um processo por solicitação. Mas está melhorando a cada versão. Os tipos e modelo de objeto fazem todas ou quase todas as coisas que se pode esperar dele. Quero ver genéricos, co- e contravariância, tipos de retorno de chamada que especificam uma assinatura, etc. Muito disso está sendo discutido ou a caminho de ser implementado. Eu acho que a direção geral é boa, mesmo que as peculiaridades da biblioteca padrão não desapareçam facilmente.
Donquixote
4
Uma sugestão: em vez de loadItemOrNull(id), considere se loadItemOr(id, defaultItem)faz sentido para você. Se o item é uma String ou número, geralmente é o caso.
user949300

Respostas:

35

Você está correto: dois métodos são muito melhores para isso, por várias razões:

  1. 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.

  2. 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.

  3. 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 nullresposta 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.

  4. 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): intlança uma exceção, enquanto int.TryParse(string, out int): boolnã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.

Arseni Mourzenko
fonte
11
Obrigado, esta é a resposta que eu estava procurando! O objetivo foi iniciar uma discussão sobre isso no Drupal CMS, drupal.org/project/drupal/issues/2932772 , e não quero que isso seja sobre minha preferência pessoal, mas apoie-o com comentários de outras pessoas.
Donquixote
4
Na verdade, a tradição de longa data para pelo menos .NET é NÃO usar dois métodos, mas escolher a escolha certa para a tarefa certa. As exceções são para circunstâncias excepcionais, para não manipular o fluxo de controle esperado. Falha ao analisar uma string em um número inteiro? Não é uma circunstância muito inesperada ou excepcional. int.Parse()é considerada uma péssima decisão de design, e é exatamente por isso que a equipe do .NET foi adiante e implementou TryParse.
Voo 23/12
11
Fornecer dois métodos, em vez de pensar em que tipo de caso de uso estamos lidando, é a saída mais fácil: não pense no que está fazendo, em vez disso, coloque a responsabilidade em todos os usuários da sua API. Claro que funciona, mas essa não é a marca registrada do bom design de API (ou qualquer outro design para o assunto. Leia, por exemplo, a opinião de Joel sobre isso .
Voo
@Voo Ponto válido. De fato, o fornecimento de dois métodos coloca a escolha no chamador. Talvez para alguns valores de parâmetro um item deva existir, enquanto para outros valores de parâmetro um item inexistente é considerado um caso regular. Talvez o componente seja usado em um aplicativo em que os itens sempre devem existir e em outro aplicativo em que itens não existentes são considerados normais. Talvez o chamador ->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.
Donquixote
11
Razão 1b: Alguns idiomas (Haskell, Swift) exigem que a nulidade esteja presente na assinatura. Especificamente, Haskell tem um tipo totalmente diferente para valores anuláveis ​​( data Maybe a = Just a | Nothing).
John Dvorak
9

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 retornar optional<Void>; portanto, (d) pode ser usada para lançar funções retornando Void.

gnasher729
fonte
2

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:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

exemplo de uso (significa saída por referência não inicializada)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}
Ewan
fonte
Desculpe, você me perdeu aqui. Como seria sua " bool TryMethodName(out returnValue)abordagem" no exemplo original?
Donquixote
editado para adicionar um exemplo
Ewan
11
Então você usa um parâmetro por referência em vez de um valor de retorno, para ter o valor de retorno livre para o sucesso do bool? Existe uma linguagem de programação que suporte essa palavra-chave "out"?
Donquixote
2
sim, desculpe não percebi 'out' é específico para c #
Ewan
11
nem sempre não. Mas e se o item 3 for nulo? você não saberia se não tivesse havido um erro ou não
Ewan
2

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.

Máx.
fonte
11
Eu não aceitaria isso como regra geral sem adicionar alguma qualificação ou nuance. A remoção de um argumento booleano como pode forçá-lo a escrever um if / else idiota em algum outro lugar e, assim, você codifica os dados no seu código escrito e não nas variáveis.
Gerard
@ Gerard, você pode me dar um exemplo, por favor?
Max
@ Max Quando você tem uma variável booleana b: Em vez de chamar, foo(…, b);você deve escrever if (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.
precisa
@BlackJack bem, sim, eu concordo com você. Pensei no exemplo, if (myGrandmaMadeCookies) eatThem() else makeFood()que sim, eu poderia envolver em outra função como esta checkIfShouldCook(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.
Max
11
A nuance mencionada no meu comentário refere-se a "você deve sempre". Talvez reformule-o como "se a, bec são aplicáveis ​​então [...]". A "regra" mencionada é um ótimo paradigma, mas muito variável no caso de uso.
Gerard
1

O Código Limpo recomenda evitar argumentos de sinalização booleana, porque seu significado é opaco no site da chamada:

loadItem(id, true) // does this throw or not? We need to check the docs ...

Considerando que métodos separados podem usar nomes expressivos:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs
meriton - em greve
fonte
1

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:

Optional<Item> loadItem(int id);

No seu método, você retorna Optional.of(item)se encontrou o item ou retorna Optional.empty()caso não o encontre . Você nunca joga 2 e nunca volta null.

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 da Optionalação explícita, é necessário:

  • loadItem(1).get()jogará um NoSuchElementExceptionou retornará o item encontrado.
  • Também loadItem(1).orElseThrow(() -> new MyException("No item 1"))deve usar uma exceção personalizada se o retornado Optionalestiver vazio.
  • loadItem(1).orElse(defaultItem)retorna o item encontrado ou o passado defaultItem(que também pode ser null) sem gerar uma exceção.
  • loadItem(1).map(Item::getName)retornaria novamente um Optionalcom o nome dos itens, se presente.
  • Existem mais métodos, consulte o Javadoc paraOptional .

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.

siegi
fonte
0

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.

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}
Krillgar
fonte
0

Eu prefiro dois métodos, dessa forma, você não apenas me dirá que está retornando um valor, mas qual valor exatamente.

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

O TryDoSomething () também não é um padrão ruim.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

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.

A Bravo Dev
fonte
-1

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:

Thing getThing(string x);
Thing tryGetThing (string x);

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:

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

Se houver um argumento sobre como lidar com erros, apenas um wrapper seria necessário:

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

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:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

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.

supercat
fonte
Você está certo: decoradores e wrappers, e mesmo implementação regular, terão alguma duplicação de código ao usar dois métodos.
Donquixote
Certamente coloque as versões com e sem exceção em pacotes separados e torne os wrappers genéricos?
Will Crawford
-1

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 Vectorclasse 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çãoif (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:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

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!

Cort Ammon - Restabelecer Monica
fonte