GOTO Return

Agora, todos entendem que o uso do operador GOTO não é apenas uma prática ruim, mas terrível. O debate sobre seu uso terminou nos anos 80 do século XX e foi excluído da maioria das linguagens de programação modernas. Mas, como convém a um mal real, ele conseguiu se disfarçar e ressuscitar no século 21 sob o disfarce de exceções.


Exceções, por um lado, são um conceito bastante simples nas linguagens de programação modernas. Por outro lado, eles são frequentemente usados ​​incorretamente. Existe uma regra simples e bem conhecida - as exceções são apenas para lidar com danos. E é uma interpretação muito vaga do conceito de "colapso" leva a todos os problemas do uso do GOTO.


Exemplo teórico


A diferença entre falhas e cenários de negócios negativos é claramente visível na janela de login com um caso de uso muito simples:


  1. Usuário digita login / senha.
  2. O usuário clica no botão "Login".
  3. O aplicativo cliente envia uma solicitação ao servidor.
  4. O servidor verifica com êxito o nome de usuário / senha (considera a presença do par correspondente como bem-sucedida).
  5. O servidor envia informações ao cliente que a autenticação foi bem-sucedida e um link para a página de transição.
  6. O cliente vai para a página especificada.

E uma extensão negativa:


4.1 O servidor não encontrou o par de login / senha correspondente e envia uma notificação ao cliente sobre isso.


Considerar que o cenário 4.1 é um "problema" e, portanto, deve ser implementado usando uma exceção é um erro bastante comum. Este não é realmente o caso. As incompatibilidades de login e senha fazem parte da nossa experiência padrão do usuário, conforme fornecido pela lógica comercial do script. Nossos clientes comerciais esperam esse desenvolvimento. Portanto, isso não é um detalhamento e você não pode usar exceções aqui.


As quebras são: desconexão da conexão entre o cliente e o norte, inacessibilidade do DBMS, esquema incorreto no banco de dados. E mais um milhão de razões que quebram nossos aplicativos e não têm nada a ver com a lógica de negócios do usuário.


Em um dos projetos, do qual participei, havia uma lógica de logon mais complexa. Ao digitar a senha incorreta três vezes seguidas, o usuário foi temporariamente bloqueado por 15 minutos. Obtendo três vezes seguidas em um bloqueio temporário, o usuário recebeu um bloqueio permanente. Também havia regras adicionais, dependendo do tipo de usuário. A implementação de exceções tornou extremamente difícil a introdução de novas regras.


Seria interessante considerar este exemplo, mas é muito grande e não muito visual. Como o código confuso com a lógica de negócios sobre exceções se torna claro e conciso, mostrarei outro exemplo.


Propriedades de carregamento de exemplo


Tente olhar para este código e entender claramente o que ele faz. O procedimento não é grande com uma lógica bastante simples. Com um bom estilo de programação, a compreensão de sua essência não deve exceder mais de 2 a 3 minutos (não me lembro de quanto tempo levei para entender completamente esse código, mas definitivamente mais de 15 minutos).


private WorkspaceProperties(){ Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true); //These mappings will replace any mappings that this hashtable had for any of the //keys currently in the specified map. getProperties().putAll( loadedProperties ); //     loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH, false); if (loadedProperties != null){ getProperties().putAll( loadedProperties ); } System.out.println("Loaded properties:" + getProperties()); } /** *  ,    . * @param filepath * @param throwIfNotFound -  FileNotFoundException,     * @return    null,      !throwIfNotFound * @throws FileNotFoundException throwIfNotFound        * @throws IOException     */ private Properties readPropertiesFromFile(String filepath, boolean throwIfNotExists){ Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); InputStream is = null; InputStreamReader isr = null; try{ int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0){ try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } System.out.println("Found file " + filepath); } catch( FileNotFoundException e) { System.out.println("File not found " + filepath); if (throwIfNotExists) throw new RuntimeException("Can`t load workspace properties." + filepath + " not found", e ); }catch (IOException e){ throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Então, vamos revelar o segredo - o que está acontecendo aqui. As propriedades de dois arquivos são WORK_PROPERTIES - as WORK_PROPERTIES obrigatórias e as MY_WORK_PROPERTIES adicionais, adicionando ao armazenamento de propriedades compartilhadas. Há uma nuance - não sabemos exatamente onde o arquivo de propriedades específico está localizado - ele pode estar no diretório atual e nos diretórios ancestrais (até três níveis acima).


Pelo menos duas coisas são confusas aqui: o parâmetro throwIfNotExists e o grande bloco lógico na catch FileNotFoundException . Tudo isso sugere opaca - exceções são usadas para implementar a lógica de negócios (mas de que outra maneira explicar isso em um cenário, lançar uma exceção é uma falha e, no outro, não?).


Fazendo o contrato certo


Primeiro, throwIfNotExists lidar com throwIfNotExists . Ao trabalhar com exceções, é muito importante entender onde exatamente ele precisa ser processado em termos de casos de uso. Nesse caso, é óbvio que o readPropertiesFromFile método readPropertiesFromFile não pode decidir quando a ausência de um arquivo é "ruim" e quando "boa". Essa decisão é tomada no momento da sua chamada. Os comentários mostram que decidimos se esse arquivo deve existir ou não. Mas, na verdade, estamos interessados ​​não no arquivo em si, mas nas configurações dele. Infelizmente, isso não segue o código.


Corrigimos essas duas deficiências:


 Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll( loadedProperties ); 

Agora a semântica é mostrada claramente -
WORK_PROPERTIES deve ser especificado, mas MY_WORK_PROPERTIES não. Além disso, ao refatorar, notei que o readPropertiesFromFile nunca poderia retornar null e aproveitou isso ao ler MY_WORK_PROPERTIES .


Verificamos sem quebrar


A refatoração anterior também afetou a implementação, mas não significativamente. Acabei de excluir o throwIfNotExists processamento throwIfNotExists :


 if (throwIfNotExists) throw new RuntimeException(…); 

Tendo examinado a implementação mais de perto, começamos a entender a lógica do autor do código para procurar um arquivo. Primeiro, é verificado se o arquivo está no diretório atual; se não for encontrado, verificamos em um nível superior, etc. I.e. fica claro que o algoritmo prevê a ausência de um arquivo. Nesse caso, a verificação é feita usando uma exceção. I.e. o princípio é violado - a exceção é percebida não como "algo quebrou", mas como parte da lógica de negócios.


Existe uma função para verificar a disponibilidade de um arquivo para ler File.canRead() . Com ele, você pode se livrar da lógica de negócios em um catch


  try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } 

Alterando o código, obtemos o seguinte:


 private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } } System.out.println("Found file " + filepath); } catch (FileNotFoundException e) { System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Também reduzi o nível de variáveis ​​( is , isr ) para o mínimo permitido.


Essa refatoração simples melhora muito a legibilidade do código. O código exibe diretamente o algoritmo (se o arquivo existir, então lemos, caso contrário, reduzimos o número de tentativas e procuramos no diretório acima).


Revelando GOTO


Considere em detalhes o que está acontecendo em uma situação se o arquivo não foi encontrado:


 } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } 

Pode-se ver que aqui a exceção é usada para interromper o ciclo de execução e realmente executar a função GOTO.


Para quem duvida, faremos outra mudança. Em vez de usar uma pequena muleta no formato loadingTryLeft = 0 (muleta, porque na verdade uma tentativa bem-sucedida não loadingTryLeft = 0 número de tentativas restantes), indicamos explicitamente que a leitura do arquivo leva à saída da função (sem esquecer de escrever uma mensagem):


 try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { 

Isso nos permite substituir a condição while (loadingTryLeft > 0) por while(true) :


 try { int loadingTryLeft = 3; String relativePath = ""; while (true) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); // GOTO: FFN } } } } catch (FileNotFoundException e) { // LABEL: FFN System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } 

Para se livrar do óbvio mau cheiro que causa o throw new FileNotFoundException , você precisa se lembrar do contrato da função. De qualquer forma, a função retorna um conjunto de propriedades; se elas não puderam ler o arquivo, retornamos em branco. Portanto, não há razão para lançar uma exceção e capturá-la. A condição usual de while (loadingTryLeft > 0) suficiente:


 private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; } 

Em princípio, do ponto de vista do trabalho correto, com exceções, tudo está aqui. Existe uma dúvida sobre a necessidade de lançar uma RuntimeException no caso de problemas de IOException, mas vamos deixá-la como está por questões de compatibilidade.



Existem algumas pequenas coisas que podemos tornar o código ainda mais flexível e compreensível:


  • O nome do método readPropertiesFromFile expõe sua implementação (a propósito, e também lança FileNotFoundException). Melhor chamar isso de mais neutro e conciso - loadProperties (...)
  • O método pesquisa e lê simultaneamente. Para mim, essas são duas responsabilidades diferentes que podem ser divididas em métodos diferentes.
  • O código foi originalmente escrito em Java 6, mas agora é usado em Java 7. Isso permite o uso de recursos que podem ser fechados.
  • Sei por experiência própria que, ao exibir informações sobre um arquivo encontrado ou não, é melhor usar o caminho completo para o arquivo, em vez de relativo.
  • if (loadingTryLeft > 0) relativePath += "../"; - se você observar cuidadosamente o código, poderá ver - essa verificação é desnecessária, porque se o limite de pesquisa estiver esgotado, o novo valor não será usado. E se houver algo supérfluo no código, isso é lixo que deve ser removido.

A versão final do código fonte:


 private WorkspaceProperties() { super(new Properties()); if (defaultInstance != null) throw new IllegalStateException(); Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } getProperties().putAll(loadedProperties); loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll(loadedProperties); System.out.println("Loaded properties:" + getProperties()); } private Properties readPropertiesFromFile(String filepath) { System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { return read(file); } else { relativePath += "../"; loadingTryLeft -= 1; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return new Properties(); } private Properties read(File file) throws IOException { try (InputStream is = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(is, "UTF-8")) { Properties loadedProperties = new Properties(); loadedProperties.load(isr); System.out.println("Found file " + file.getAbsolutePath()); return loadedProperties; } } 

Sumário


O exemplo analisado ilustra claramente o que leva ao tratamento descuidado do código-fonte. Em vez de usar uma exceção para lidar com o colapso, foi decidido usá-lo para implementar a lógica de negócios. Isso levou imediatamente à complexidade de seu suporte, que se refletiu em seu desenvolvimento para atender aos novos requisitos e, como resultado, um afastamento dos princípios da programação estrutural. O uso de uma regra simples - exceções apenas para falhas - ajudará a evitar o retorno à era GOTO e manterá seu código limpo, compreensível e extensível.

Source: https://habr.com/ru/post/pt484840/


All Articles