Esta é uma situação comum e há muitas maneiras comuns de lidar com isso. Aqui está minha tentativa de resposta canônica. Por favor, comente se eu perdi alguma coisa e vou manter esta postagem atualizada.
Esta é uma flecha
O que você está discutindo é conhecido como anti-padrão de seta . É chamada de seta porque a cadeia de ifs aninhados forma blocos de código que se expandem cada vez mais para a direita e depois para a esquerda, formando uma seta visual que "aponta" para o lado direito do painel do editor de código.
Achate a flecha com a guarda
Algumas maneiras comuns de evitar o Arrow são discutidas aqui . O método mais comum é usar um padrão de guarda , no qual o código lida primeiro com os fluxos de exceção e depois lida com o fluxo básico, por exemplo, em vez de
if (ok)
{
DoSomething();
}
else
{
_log.Error("oops");
return;
}
... você usaria ....
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
Quando houver uma longa série de guardas, isso achatará o código consideravelmente, pois todos os guardas aparecem totalmente à esquerda e seus ifs não estão aninhados. Além disso, você está visualizando o emparelhamento visual da condição lógica com o erro associado, o que torna muito mais fácil saber o que está acontecendo:
Seta:
ok = DoSomething1();
if (ok)
{
ok = DoSomething2();
if (ok)
{
ok = DoSomething3();
if (!ok)
{
_log.Error("oops"); //Tip of the Arrow
return;
}
}
else
{
_log.Error("oops");
return;
}
}
else
{
_log.Error("oops");
return;
}
Guarda:
ok = DoSomething1();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething2();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething3();
if (!ok)
{
_log.Error("oops");
return;
}
ok = DoSomething4();
if (!ok)
{
_log.Error("oops");
return;
}
É objetiva e quantificável mais fácil de ler, porque
- Os caracteres {e} para um determinado bloco lógico estão mais próximos
- A quantidade de contexto mental necessária para entender uma linha específica é menor
- A totalidade da lógica associada a uma condição if é mais provável que esteja em uma página
- A necessidade de o codificador rolar a página / trilha ocular diminui bastante
Como adicionar código comum no final
O problema com o padrão de guarda é que ele se baseia no que é chamado de "retorno oportunista" ou "saída oportunista". Em outras palavras, ele quebra o padrão de que toda e qualquer função deve ter exatamente um ponto de saída. Este é um problema por dois motivos:
- Isso atrapalha algumas pessoas da maneira errada, por exemplo, pessoas que aprenderam a codificar em Pascal aprenderam que uma função = um ponto de saída.
- Ele não fornece uma seção de código que é executada na saída, independentemente do assunto , que é o assunto em questão.
Abaixo, forneci algumas opções para contornar essa limitação, usando os recursos de idioma ou evitando o problema completamente.
Opção 1. Você não pode fazer isso: use finally
Infelizmente, como desenvolvedor de c ++, você não pode fazer isso. Mas esta é a resposta número um para idiomas que contêm finalmente uma palavra-chave, pois é exatamente para isso que serve.
try
{
if (!ok)
{
_log.Error("oops");
return;
}
DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
DoSomethingNoMatterWhat();
}
Opção 2. Evite o problema: reestruture suas funções
Você pode evitar o problema dividindo o código em duas funções. Esta solução tem o benefício de trabalhar para qualquer idioma e, além disso, pode reduzir a complexidade ciclomática , que é uma maneira comprovada de reduzir a taxa de defeitos e melhora a especificidade de qualquer teste de unidade automatizado.
Aqui está um exemplo:
void OuterFunction()
{
DoSomethingIfPossible();
DoSomethingNoMatterWhat();
}
void DoSomethingIfPossible()
{
if (!ok)
{
_log.Error("Oops");
return;
}
DoSomething();
}
Opção 3. Truque de idiomas: use um loop falso
Outro truque comum que vejo é usar while (true) e break, como mostrado nas outras respostas.
while(true)
{
if (!ok) break;
DoSomething();
break; //important
}
DoSomethingNoMatterWhat();
Embora isso seja menos "honesto" do que usado goto
, é menos propenso a ser confuso ao refatorar, pois marca claramente os limites do escopo da lógica. Um codificador ingênuo que corta e cola seus rótulos ou suas goto
declarações pode causar grandes problemas! (E, francamente, o padrão é tão comum agora, acho que comunica claramente a intenção e, portanto, não é "desonesto").
Existem outras variantes dessas opções. Por exemplo, um poderia usar em switch
vez de while
. Qualquer construção de idioma com uma break
palavra - chave provavelmente funcionaria.
Opção 4. Aproveite o ciclo de vida do objeto
Uma outra abordagem aproveita o ciclo de vida do objeto. Use um objeto de contexto para carregar seus parâmetros (algo que nosso exemplo ingênuo desconfia) e descartá-lo quando terminar.
class MyContext
{
~MyContext()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
MyContext myContext;
ok = DoSomething(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingElse(myContext);
if (!ok)
{
_log.Error("Oops");
return;
}
ok = DoSomethingMore(myContext);
if (!ok)
{
_log.Error("Oops");
}
//DoSomethingNoMatterWhat will be called when myContext goes out of scope
}
Nota: Certifique-se de entender o ciclo de vida do objeto de seu idioma preferido. Você precisa de algum tipo de coleta de lixo determinística para que isso funcione, ou seja, você precisa saber quando o destruidor será chamado. Em alguns idiomas, você precisará usar em Dispose
vez de um destruidor.
Opção 4.1. Aproveite o ciclo de vida do objeto (padrão de wrapper)
Se você usar uma abordagem orientada a objetos, pode fazê-lo corretamente. Esta opção usa uma classe para "agrupar" os recursos que requerem limpeza, bem como suas outras operações.
class MyWrapper
{
bool DoSomething() {...};
bool DoSomethingElse() {...}
void ~MyWapper()
{
DoSomethingNoMatterWhat();
}
}
void MainMethod()
{
bool ok = myWrapper.DoSomething();
if (!ok)
_log.Error("Oops");
return;
}
ok = myWrapper.DoSomethingElse();
if (!ok)
_log.Error("Oops");
return;
}
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed
Novamente, certifique-se de entender o seu ciclo de vida do objeto.
Opção 5. Truque de idiomas: use avaliação de curto-circuito
Outra técnica é tirar proveito da avaliação de curto-circuito .
if (DoSomething1() && DoSomething2() && DoSomething3())
{
DoSomething4();
}
DoSomethingNoMatterWhat();
Esta solução tira proveito da maneira como o operador && funciona. Quando o lado esquerdo de && é avaliado como falso, o lado direito nunca é avaliado.
Esse truque é mais útil quando o código compacto é necessário e quando o código provavelmente não recebe muita manutenção, por exemplo, você está implementando um algoritmo conhecido. Para uma codificação mais geral, a estrutura desse código é muito frágil; mesmo uma pequena alteração na lógica pode desencadear uma reescrita total.