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:
- Usuário digita login / senha.
- O usuário clica no botão "Login".
- O aplicativo cliente envia uma solicitação ao servidor.
- O servidor verifica com êxito o nome de usuário / senha (considera a presença do par correspondente como bem-sucedida).
- O servidor envia informações ao cliente que a autenticação foi bem-sucedida e um link para a página de transição.
- 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);
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();
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.
Bring Gloss
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.