"Quão ruim" é o código não relacionado no bloco try-catch-finalmente?

11

Esta é uma pergunta relacionada Q: O uso da cláusula Finalmente para fazer o trabalho após o retorno do estilo ruim / perigoso?

No Q referenciado, o código finalmente está relacionado à estrutura usada e à necessidade de pré-busca. Minha pergunta é um pouco diferente e acredito que seja pertinente ao público em geral. Meu exemplo particular é um aplicativo winform C #, mas isso também se aplicaria ao uso em C ++ / Java.

Estou observando alguns blocos try-catch-finalmente onde há muito código não relacionado a exceções e manipulação / limpeza de exceções ocultas dentro do bloco. E admitirei minha tendência a ter blocos muito estreitos de try-catch-finalmente com o código intimamente relacionado à exceção e ao manuseio. Aqui estão alguns exemplos do que estou vendo.

Os blocos de teste terão muitas chamadas preliminares e variáveis ​​sendo configuradas, levando ao código que poderia ser lançado. As informações de log também serão configuradas e executadas no bloco try.

Finalmente, os blocos terão chamadas de formatação de formulário / módulo / controle (apesar de o aplicativo estar prestes a terminar, conforme expresso no bloco catch), além de criar novos objetos, como painéis.

Aproximadamente:

    methodName (...)
    {
        experimentar
        {
            // Muito código para o método ...
            // código que poderia lançar ...
            // Muito mais código para o método e um retorno ...
        }
        pegar (alguma coisa)
        {// manipular exceção}
        finalmente
        {
            // alguma limpeza devido a exceção, fechando as coisas
            // mais código para o material que foi criado (ignorando que qualquer exceção poderia ter sido lançada) ...
            // talvez crie mais alguns objetos
        }
    }

O código funciona, então há algum valor nele. Não está bem encapsulado e a lógica é um pouco complicada. Estou (dolorosamente) familiarizado com os riscos de mudar o código e refatorar, então minha pergunta se resume a querer conhecer a experiência dos outros com código estruturado de maneira semelhante.

O estilo ruim justifica as mudanças? Alguém foi gravemente queimado por uma situação semelhante? Gostaria de compartilhar os detalhes dessa experiência ruim? Deixe ser porque estou reagindo demais e não é tão ruim de estilo? Ganhar os benefícios de manutenção de arrumar as coisas?

Comunidade
fonte
A tag "C ++" não pertence aqui, pois o C ++ não possui (e não precisa) finally. Todos os bons usos são cobertos por RAII / RRID / SBRM (qualquer sigla que você quiser).
David Thornley
2
@ DavidThornley: Além do exemplo em que a palavra-chave 'finalmente' é usada, o restante da pergunta se aplica perfeitamente ao C ++. Sou desenvolvedor de C ++ e essa palavra-chave realmente não me confundiu. E considerando que eu tenho conversas semelhantes com minha própria equipe em base semi-regular, esta questão é muito relevante para o que fazemos com C ++
DXM
@ DXM: Estou tendo problemas para visualizar isso (e sei o que finallyfaz em C #). Qual é o equivalente em C ++? O que estou pensando é em código após o catch, e isso se aplica ao código após o c # finally.
David Thornley
@ DavidThornley: o código finalmente é o código que é executado independentemente do que seja .
Orlp 4/04/12
1
@ nightcracker, isso não é realmente correto. Não será executado se você fizer Environment.FailFast(); pode não ser executado se você tiver uma exceção não capturada. E fica ainda mais complicado se você tiver um bloco iterador com finallyo qual itera manualmente.
Svick

Respostas:

10

Passei por uma situação muito parecida quando tive que lidar com um terrível código legado do Windows Forms escrito por desenvolvedores que claramente não sabiam o que estavam fazendo.

Primeiro de tudo, você não está exagerando. Este é um código incorreto. Como você disse, o bloqueio de captura deve abortar e se preparar para parar. Não é hora de criar objetos (especialmente painéis). Eu não posso nem começar a explicar por que isso é ruim.

Dito isto ...

Meu primeiro conselho é: se não estiver quebrado, não toque nele!

Se seu trabalho é manter o código, você deve fazer o possível para não quebrá-lo. Eu sei que é doloroso (eu já estive lá), mas você deve fazer o possível para não quebrar o que já está funcionando.

Meu segundo conselho é: se você precisar adicionar mais recursos, tente manter a estrutura de código existente o máximo possível, para não quebrar o código.

Exemplo: se houver uma declaração hedionda de caso de troca que você acha que poderia ser substituída por uma herança adequada, você deve ter cuidado e pensar duas vezes antes de decidir começar a mudar as coisas.

Você definitivamente encontrará situações em que uma refatoração é a abordagem correta, mas cuidado: é mais provável que o código de refatoração introduza bugs . Você precisa tomar essa decisão da perspectiva dos proprietários do aplicativo, não da perspectiva do desenvolvedor. Portanto, você deve pensar se o esforço (dinheiro) necessário para solucionar o problema vale uma refatoração ou não. Eu já vi muitas vezes um desenvolvedor gastando vários dias consertando algo que não está realmente quebrado apenas porque ele acha que "o código é feio".

Meu terceiro conselho é: você será queimado se quebrar o código, não importa se a culpa é sua ou não.

Se você foi contratado para fazer manutenção, realmente não importa se o aplicativo está desmoronando porque alguém tomou decisões erradas. Do ponto de vista do usuário, ele estava funcionando antes e agora você o quebrou. Você quebrou!

Joel coloca muito bem em seu artigo explicando várias razões pelas quais você não deve reescrever o código legado.

http://www.joelonsoftware.com/articles/fog0000000069.html

Portanto, você deve se sentir muito mal com esse tipo de código (e nunca deve escrever algo assim), mas mantê-lo é um monstro totalmente diferente.

Sobre minha experiência: tive que manter o código por cerca de 1 ano e, eventualmente, consegui reescrevê-lo do zero, mas não de uma só vez.

O que aconteceu é que o código era tão ruim que era impossível implementar novos recursos. O aplicativo existente apresentava sérios problemas de desempenho e usabilidade. Eventualmente, me pediram para fazer uma alteração que levaria de 3 a 4 meses (principalmente porque trabalhar com esse código me levou muito mais tempo do que o habitual). Eu pensei que poderia reescrever toda a peça (incluindo a implementação do novo recurso desejado) em cerca de 5-6 meses. Trouxe essa proposta para as partes interessadas e elas concordam em reescrevê-la (felizmente para mim).

Depois de reescrever esta peça, eles entenderam que eu poderia entregar coisas muito melhores do que o que elas já tinham. Então, pude reescrever o aplicativo inteiro.

Minha abordagem foi reescrevê-lo peça por peça. Primeiro substituí a interface do usuário inteira (Windows Forms), depois comecei a substituir a camada de comunicação (chamadas de serviço da Web) e, por último, substitui toda a implementação do servidor (era um tipo de aplicativo cliente / servidor espesso).

Alguns anos depois, esse aplicativo se transformou em uma bela ferramenta-chave usada por toda a empresa. Tenho 100% de certeza de que isso nunca seria possível se eu não tivesse reescrito tudo.

Embora eu tenha conseguido fazer isso, a parte importante é que as partes interessadas o aprovaram e pude convencê-los de que valeu a pena. Portanto, enquanto você precisa manter o aplicativo existente, faça o possível para não quebrar nada e, se conseguir convencer os proprietários sobre o benefício de reescrevê-lo, tente fazê-lo como Jack, o Estripador: em pedaços.

Alex
fonte
1
+1. Mas a última frase se refere a um serial killer. Isso é mesmo necessário?
5133 MarkJ
Eu gosto de parte disso, mas, ao mesmo tempo, deixar projetos quebrados no lugar e adicionar coisas geralmente leva a um design pior. É melhor descobrir como corrigi-lo e corrigi-lo, embora em uma abordagem medida, é claro.
Ricky Clarkson
@RickyClarkson Eu concordo. É realmente difícil saber quando você está exagerando (uma refatoração não é realmente necessária) e quando você está realmente prejudicando o aplicativo adicionando alterações.
Alex #
@RickyClarkson Eu concordo tanto com isso que fui capaz de reescrever o projeto em que estava envolvido. Mas, durante muito tempo, tive que adicionar cuidadosamente coisas tentando causar o mínimo de dano possível. A menos que a solicitação seja para corrigir algo que a causa principal seja uma má decisão de design (o que geralmente não é o caso), eu diria que o design do aplicativo não deve ser tocado.
7772 Alex
@RickyClarkson Isso fazia parte da experiência da comunidade que eu queria explorar. Declarar ruim todo o código legado é um anti-padrão igualmente ruim. No entanto, há coisas neste código em que estou trabalhando que realmente não devem ser feitas como parte de um bloco finalmente. O feedback e a resposta de Alex são o que eu estava procurando ler.
2

Se não houver necessidade de alterar o código, deixe-o como está. Mas se você precisar alterar o código porque precisa corrigir algum bug ou alterar alguma funcionalidade, precisará alterá-lo, se você gosta ou não. IMHO, geralmente é mais seguro e menos propenso a erros, se você o refatorar primeiro em pedaços menores e melhor compreensíveis e depois adicionar a funcionalidade. Quando você tem ferramentas automáticas de refatoração em mãos, isso pode ser feito de forma relativamente segura, com apenas um risco muito pequeno de introdução de novos bugs. E eu recomendo que você obtenha uma cópia do livro de Michael Feathers

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

que lhe dará dicas valiosas sobre como tornar o código mais testável, adicionar testes de unidade e evitar a quebra do código ao adicionar novos recursos.

Se você fizer isso da maneira certa, esperamos que seu método se torne simples o suficiente, pois o try-catch-finalmente não conterá mais nenhum código não relacionado nos lugares errados.

Doc Brown
fonte
2

Suponha que você tenha seis métodos para chamar, quatro dos quais devem ser chamados apenas se o primeiro for concluído sem erros. Considere duas alternativas:

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

vs

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

No segundo código, você está tratando exceções como códigos de retorno. Conceitualmente, isso é idêntico a:

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

Se formos agrupar todos os métodos que podem lançar uma exceção em um bloco try / catch, qual é o sentido de haver exceções em um recurso de idioma? Não há benefício e há mais digitação.

A razão pela qual temos exceções nos idiomas em primeiro lugar é que, nos velhos tempos ruins dos códigos de retorno, geralmente tínhamos situações acima em que tínhamos vários métodos que poderiam retornar erros, e cada erro significava abortar todos os métodos completamente. No início da minha carreira, nos velhos tempos C, vi códigos como este em todo o lugar:

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

Esse era um padrão incrivelmente comum e que as exceções resolviam muito bem, permitindo que você voltasse ao primeiro exemplo acima. Uma regra de estilo que diz que cada método tem seu próprio bloco de exceção joga completamente isso fora da janela. Por que se preocupar, então?

Você deve sempre tentar fazer com que o bloco try substitua o conjunto de códigos que é conceitualmente uma unidade. Deve incluir código para o qual, se houver algum erro na unidade de código, não há sentido em executar outro código na unidade de código.

Voltando ao objetivo original das exceções: lembre-se de que elas foram criadas porque muitas vezes o código não pode lidar facilmente com os erros exatamente quando eles realmente ocorrem. O ponto das exceções é permitir que você lide com os erros significativamente mais tarde no código ou avance na pilha. As pessoas esquecem que, e em muitos casos as capturam localmente, quando seria melhor simplesmente documentar que o método em questão pode lançar essa exceção. Há muito código no mundo que se parece com isso:

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

Aqui, você está apenas adicionando camadas, e as camadas adicionam confusão. Apenas faça o seguinte:

void method0() : throws MyOtherException
{
    method1();
    method2();
}

Além disso, meu sentimento é que, se você seguir essa regra e se encontrar com mais de alguns blocos try / catch no método, deverá questionar se o método em si é muito complexo e precisa ser dividido em vários métodos. .

A resposta: um bloco try deve conter o conjunto de códigos que deve ser abortado se ocorrer um erro em qualquer lugar do bloco. Se esse conjunto de códigos é de uma linha ou de mil linhas, não importa. (Embora você tenha mil linhas em um método, você tem outros problemas.)

Gort the Robot
fonte
Steven - obrigado pela resposta bem definida, e eu concordo plenamente com seus pensamentos. Não tenho nenhum problema com o código de tipo seqüencial ou relacionado a um único bloco de tentativa. Se eu tivesse conseguido postar um trecho de código real, ele mostraria de 5 a 10 chamadas completamente não relacionadas antes da primeira chamada de lançamento. Um bloco finalmente em particular foi muito pior - vários novos threads estavam sendo gerados e rotinas adicionais de não limpeza foram chamadas. E, nesse caso, todas as ligações naquele bloco finalmente não estavam relacionadas a nada que pudesse ter sido lançado.
Um grande problema dessa abordagem é que ela não faz distinção entre os casos em method0que quase podemos esperar que uma exceção seja lançada e os casos em que não. Por exemplo, suponha method1que às vezes possa lançar um InvalidArgumentException, mas se não o fizer, ele colocará um objeto em um estado temporariamente inválido, o qual será corrigido method2, o que deve sempre colocar o objeto novamente em um estado válido. Se o method2 lançar um InvalidArgumentException, o código que espera capturar uma exceção method1irá capturá-la, mesmo que ... #
317
... o estado do sistema corresponderá às expectativas do código. Se uma exceção lançada do método1 deve ser tratada de maneira diferente de uma lançada pelo método2, como isso pode ser realizado de maneira sensata sem agrupá-las?
Supercat 27/03
Idealmente, suas exceções são granulares o suficiente para que essa seja uma situação rara.
Gort the Robot