Boas Práticas Desenvolvimento Java

Objetivo.

objetivo deste documento é estabelecer os padrões e diretrizes arquiteturais para o desenvolvimento de sistemas utilizando a arquitetura de microserviços com Java/Spring Boot. Este conteúdo é um compilado das melhores práticas de codificação em Java.
Tópicos:

  • Padronização de API Java.
  • Estrutura de pacotes
  • Padronização de Logs
  • Padronização de APIs REST

Padronização de API Java

Indentação:

Embora o Java não seja tão rigoroso quanto algumas linguagens, como o Python, em relação à indentação, há convenções amplamente aceitas que são seguidas pela comunidade de desenvolvedores Java..

Tamanho da Indentação: Use quatro espaços para cada nível de indentação.

Blocos de Código:
Coloque as chaves ({}) na mesma linha da declaração de controle de fluxo (if, for, while, etc.).

Exemplo:
                            
                                public class Exemplo {

                                    public static void main(String[] args) {
                                        int x = 10;

                                        // Exemplo de if com chaves na mesma linha
                                        if (x > 5) {
                                            System.out.println("x é maior que 5");
                                        } else {
                                            System.out.println("x é menor ou igual a 5");
                                        }

                                        // Exemplo de for com chaves na mesma linha
                                        for (int i = 0; i < 5; i++) {
                                            System.out.println("Número: " + i);
                                        }

                                        // Exemplo de while com chaves na mesma linha
                                        int y = 0;
                                        while (y < 5) {
                                            System.out.println("y é menor que 5");
                                            y++;
                                        }
                                    }
                                }
                            
                        

Alinhamento:
Alinhe os elementos relacionados verticalmente para melhorar a legibilidade.

Exemplo:
                            
                                public class Exemplo {
                                    public static void main(String[] args) {
                                        // Chamada do método
                                        imprimirValores();
                                    }
                                
                                    public static void imprimirValores() {
                                        // Declaração e inicialização das variáveis usanndo o padrão acima
                                        int valor1              = 10;
                                        int valorElemento1      = 20;
                                        String texto            = "Exemplo de método";
                                        boolean condicao        = true;
                                
                                        // Impressão dos valores
                                        System.out.println("Valor 1: " + valor1);
                                        System.out.println("Valor Elemento 1: " + valorElemento1);
                                        System.out.println("Texto: " + texto);
                                        System.out.println("Condição: " + condicao);
                                    }
                                }
                                
                            
                        

Quebra de Linha:
Quebre linhas após um número razoável de caracteres (geralmente 80-120 caracteres) para evitar linhas muito longas.

Exemplo:
                            
                                public class Exemplo {

                                    public static void main(String[] args) {
                                        // Chamada do método
                                        imprimirTexto();
                                    }
                                
                                    public static void imprimirTexto() {
                                        // Texto longo que será quebrado em várias linhas
                                        String texto = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
                                                     + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "
                                                     + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi "
                                                     + "ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit "
                                                     + "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
                                                     + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui "
                                                     + "officia deserunt mollit anim id est laborum.";
                                
                                        // Impressão do texto
                                        System.out.println(texto);
                                    }
                                }                                
                            
                        

Operadores:
Adicione espaços ao redor dos operadores para melhorar a clareza.

                            
                                public class Exemplo {
                                    int resultado = numero1 + numero2;
                                }                                
                            
                        

Métodos
Coloque o parâmetro de método aberto na mesma linha que o nome do método e use quebras de linha para parâmetros longos.

Exemplo:
                            
                                public class Exemplo {
                                    public void metodoExemplo(int parametro1, String parametro2,
                                                              double parametro3, boolean parametro4,
                                                              Object parametro5) {
                                        // Corpo do método aqui
                                    }
                                }                                                     
                            
                        

Documentação de API Java

Padronização Swagger

Porque Documentar APIs?
Documentar uma API é crucial para garantir a compreensão e o uso correto por parte dos desenvolvedores que a utilizam. Aqui estão algumas razões pelas quais a documentação de API é importante:
1 - Comunicação clara: A documentação fornece informações claras sobre como usar a API, incluindo detalhes sobre os endpoints, parâmetros necessários, formatos de dados esperados e respostas retornadas. Isso ajuda os desenvolvedores a entenderem rapidamente como interagir com a API sem a necessidade de examinar o código-fonte.
2 - Facilita a integração: Uma boa documentação torna mais fácil para outros desenvolvedores integrarem a API em seus próprios aplicativos. Isso é especialmente importante em projetos de colaboração, onde diferentes equipes ou desenvolvedores podem estar trabalhando em partes diferentes do sistema.
3 - Reduz o tempo de integração: Ao fornecer uma documentação clara e abrangente, os desenvolvedores podem economizar tempo valioso que seria gasto tentando entender como a API funciona por meio de tentativa e erro.
4 - Promove a adoção da API: Uma documentação bem elaborada pode atrair mais desenvolvedores para usar sua API, pois eles se sentirão mais confiantes e confortáveis em trabalhar com ela.

O Swagger (agora conhecido como OpenAPI Specification) é uma ferramenta popular para documentar APIs em Java (e em várias outras linguagens). Aqui estão algumas razões pelas quais o Swagger é frequentemente usado:
1 - Padrão amplamente adotado: O Swagger se tornou um padrão de fato para documentação de API, sendo amplamente adotado pela comunidade de desenvolvimento de software. Isso significa que muitos desenvolvedores já estão familiarizados com o formato e sabem onde procurar informações sobre como usar uma API documentada com o Swagger.
2 - Automatização da documentação: O Swagger permite gerar automaticamente a documentação da API com base em anotações Java diretamente no código-fonte. Isso significa que os desenvolvedores podem manter a documentação atualizada junto com o código, reduzindo o risco de inconsistências entre a implementação real da API e sua documentação.
3 - Ferramentas de suporte: O ecossistema do Swagger oferece uma variedade de ferramentas que ajudam os desenvolvedores a trabalhar com APIs documentadas com o Swagger, incluindo geradores de código, validadores de especificações e ferramentas de teste automatizado.
4 - Interface interativa: O Swagger geralmente fornece uma interface interativa, conhecida como Swagger UI, que permite aos desenvolvedores explorar e testar a API diretamente no navegador, o que pode facilitar o processo de integração e depuração.

Exemplo:
                    
                        import org.springframework.http.ResponseEntity;
                        import org.springframework.web.bind.annotation.*;
                        
                        @RestController
                        @RequestMapping("/api")
                        public class ExemploController {
                        
                            @GetMapping("/exemplo")
                            @ApiOperation(value = "Obtém um exemplo", notes = "Retorna um exemplo específico")
                            @ApiResponses(value = {
                                    @ApiResponse(code = 200, message = "Sucesso"),
                                    @ApiResponse(code = 404, message = "Não encontrado")
                            })
                            public ResponseEntity obterExemplo(
                                    @ApiParam(value = "ID do exemplo", required = true) @RequestParam("id") String id) {
                                // Lógica para obter o exemplo com o ID fornecido
                                return ResponseEntity.ok("Exemplo com ID " + id);
                            }
                        
                            @PostMapping("/exemplo")
                            @ApiOperation(value = "Cria um exemplo", notes = "Cria um novo exemplo")
                            @ApiResponses(value = {
                                    @ApiResponse(code = 201, message = "Criado com sucesso"),
                                    @ApiResponse(code = 400, message = "Solicitação inválida")
                            })
                            public ResponseEntity criarExemplo(
                                    @ApiParam(value = "Dados do exemplo a ser criado", required = true) @RequestBody String dados) {
                                // Lógica para criar um novo exemplo com os dados fornecidos
                                return ResponseEntity.status(HttpStatus.CREATED).body("Exemplo criado com sucesso");
                            }
                        }
                                                                      
                    
                


Convenções de nomenclatura:

Utilizar nomes significativos e seguir as convenções de nomenclatura Java, como camelCase para nomes de variáveis e métodos, PascalCase para nomes de classes e UPPER_CASE para constantes.

Exemplo:
                        
                            public class ExemploConvencaoNomenclatura {

                                // Variáveis devem seguir camelCase
                                private int numeroTotalDeItens;
                                private String nomeDoUsuario;
                            
                                // Métodos também devem seguir camelCase
                                public void calcularTotalDeItens() {
                                    // Implementação do método aqui
                                }
                            
                                // Classes devem seguir PascalCase
                                public class ClasseExemplo {
                                    // Implementação da classe aqui
                                }
                            
                                // Constantes devem seguir UPPER_CASE
                                public static final int TAMANHO_MAXIMO = 100;
                            
                                // Parâmetros de método devem seguir camelCase
                                public void exibirMensagem(String mensagemDeBoasVindas) {
                                    // Implementação do método aqui
                                }
                            }
                                                                          
                        
                    


Neste exemplo:

As variáveis numeroTotalDeItens e nomeDoUsuario seguem a convenção de nomenclatura camelCase.
O método calcularTotalDeItens() segue a convenção de nomenclatura camelCase.
A classe ClasseExemplo segue a convenção de nomenclatura PascalCase. A constante TAMANHO_MAXIMO segue a convenção de nomenclatura UPPER_CASE. O parâmetro mensagemDeBoasVindas segue a convenção de nomenclatura camelCase.

Seguir essas convenções torna o código mais legível e coeso, facilitando a compreensão do código por outros desenvolvedores e promovendo uma consistência na base de código.



Gerenciamento de exceções
Gerenciar exceções de forma adequada é crucial para garantir que um programa Java seja robusto e seguro. Aqui estão algumas práticas recomendadas para o gerenciamento de exceções:

Prevenir quando possível: Em muitos casos, é possível prevenir a ocorrência de exceções através da validação adequada dos dados de entrada e do estado do programa. Antes de realizar uma operação que possa lançar uma exceção, verifique se todas as pré-condições necessárias são atendidas.

Usar exceções para condições excepcionais: Exceções devem ser usadas para indicar condições excepcionais que ocorrem durante a execução do programa e que não podem ser tratadas de forma normal. Evite o uso de exceções para controlar fluxo de execução ou situações esperadas.

Evitar blocos try-catch desnecessários: Use blocos try-catch apenas onde for realmente necessário. Não envolva grandes blocos de código em try-catch, pois isso pode dificultar a identificação e o tratamento de exceções específicas.

Capturar exceções específicas: Sempre que possível, capture exceções específicas em vez de capturar a exceção genérica Exception. Isso permite um tratamento mais preciso das exceções e evita a captura de exceções que não estão relacionadas ao problema que está sendo tratado.

Lançar exceções apropriadas: Ao lançar exceções em métodos, certifique-se de que a exceção lançada seja apropriada para indicar o problema encontrado. Use exceções predefinidas do Java quando apropriado ou crie suas próprias exceções personalizadas para situações específicas.

Fornecer mensagens informativas: Ao lançar uma exceção, forneça uma mensagem clara e informativa que descreva o problema encontrado. Isso ajuda os desenvolvedores que estão lidando com a exceção a entender a causa do erro.

Limpar recursos em blocos finally: Quando recursos externos, como conexões de banco de dados ou arquivos, são utilizados, certifique-se de fechá-los em um bloco finally para garantir que sejam liberados corretamente, mesmo em caso de exceção.

Aqui está um exemplo simples demonstrando algumas dessas práticas:

Exemplo:
                        
                            public class ExemploGerenciamentoExcecoes {

                                public void lerArquivo(String nomeArquivo) throws FileNotFoundException {
                                    // Tentar abrir o arquivo
                                    try (FileInputStream arquivo = new FileInputStream(nomeArquivo)) {
                                        // Lógica para ler o arquivo
                                    } catch (FileNotFoundException e) {
                                        // Se o arquivo não for encontrado, lançar exceção apropriada
                                        throw new FileNotFoundException("Arquivo não encontrado: " + nomeArquivo);
                                    } catch (IOException e) {
                                        // Lidar com outras exceções de entrada/saída
                                        System.err.println("Erro de entrada/saída: " + e.getMessage());
                                    } finally {
                                        // Fechar recursos no bloco finally
                                        // Isso garante que os recursos serão fechados independentemente de ocorrerem exceções ou não
                                        // Neste caso, o arquivo FileInputStream será fechado
                                    }
                                }
                            }
                                                                          
                        
                    


Neste exemplo, a exceção FileNotFoundException é lançada se o arquivo não for encontrado durante a tentativa de leitura.
Outras exceções de entrada/saída são capturadas e tratadas de forma apropriada.
O bloco finally é usado para garantir que o recurso FileInputStream seja fechado, independentemente de ocorrerem exceções ou não.



Uso de Generics

Uso de generics: Aproveitar ao máximo os generics para escrever código genérico e reutilizável,
aumentando a segurança de tipo e reduzindo a necessidade de coerção de tipo.

Declaração de classes e interfaces genéricas: Você pode definir classes e interfaces genéricas que aceitam um ou mais tipos como parâmetros.
Isso permite que essas classes e interfaces sejam usadas com diferentes tipos de dados de forma segura.

Exemplo:
                        
                            public class ListaGenerica {
                                private List elementos = new ArrayList<>();
                            
                                public void adicionarElemento(T elemento) {
                                    elementos.add(elemento);
                                }
                            
                                public T obterElemento(int indice) {
                                    return elementos.get(indice);
                                }
                            }
                                                                                                      
                        
                    

Neste exemplo, a classe ListaGenerica é genérica e pode ser usada com qualquer tipo de objeto.
Você pode criar uma instância dessa classe para armazenar uma lista de strings, inteiros, objetos personalizados, etc.

Restrições de tipo: Você pode impor restrições nos tipos que podem ser usados com generics, garantindo que apenas tipos específicos sejam permitidos.

Exemplo:
                    
                        public class ListaNumerica {
                            private List elementos = new ArrayList<>();
                        
                            public void adicionarElemento(T elemento) {
                                elementos.add(elemento);
                            }
                        
                            public T obterElemento(int indice) {
                                return elementos.get(indice);
                            }
                        }                                                                                                  
                    
                


Neste exemplo, a classe ListaNumerica é genérica, mas só aceita tipos que são subtipos de Number.
Isso garante que apenas números possam ser armazenados na lista.

Métodos genéricos: Além de classes e interfaces,
você também pode definir métodos genéricos que aceitam parâmetros genéricos e/ou têm um tipo de retorno genérico.

Exemplo:
                    
                        public class Utilidades {
                            public static  T obterPrimeiroElemento(List lista) {
                                if (lista.isEmpty()) {
                                    return null;
                                }
                                return lista.get(0);
                            }
                        }                                            
                    
                


Neste exemplo, o método obterPrimeiroElemento é genérico e pode ser usado com qualquer tipo de lista.
Ele retorna o primeiro elemento da lista, independentemente do tipo de objeto contido na lista.

Ao usar generics em Java, é importante entender como eles funcionam e como podem ser aplicados para escrever código mais genérico, seguro e reutilizável.
Eles são uma parte fundamental da linguagem Java e são amplamente usados em muitas bibliotecas e frameworks populares.


Testes

Testes são uma parte fundamental do desenvolvimento de software, e em Java não é diferente. Eles ajudam a garantir que o código funcione conforme o esperado, mesmo após alterações e adições de novos recursos. Existem diferentes tipos de testes que podem ser implementados em um projeto Java, incluindo testes unitários, testes de integração e testes de aceitação. Vamos explorar cada um deles com exemplos:

Testes Unitários:

Os testes unitários verificam o funcionamento de unidades individuais de código, como métodos ou classes, de forma isolada. Eles são escritos para testar pequenas partes do código e são executados de maneira rápida e eficiente.

Exemplo de um teste unitário em Java usando JUnit:
                    
        import org.junit.Test;
        import static org.junit.Assert.assertEquals;
        
        public class CalculadoraTest {
        
            @Test
            public void testSomar() {
                Calculadora calculadora = new Calculadora();
                int resultado = calculadora.somar(3, 5);
                assertEquals(8, resultado);
            }
        }                                                                      
                    
                

Neste exemplo, estamos testando o método somar() da classe Calculadora, verificando se a soma de 3 e 5 é igual a 8.



Testes de Integração:

Os testes de integração verificam se diferentes partes do sistema funcionam corretamente quando integradas. Eles testam a interação entre os componentes e garantem que eles se comuniquem corretamente uns com os outros.

Exemplo de um teste de integração em Java usando JUnit e Mockito:
                    
        import org.junit.Test;
        import static org.mockito.Mockito.*;
        import static org.junit.Assert.*;
        
        public class ServicoEmailTest {
        
            @Test
            public void testEnviarEmail() {
                ServicoEmail servicoEmail = new ServicoEmail();
                Email emailMock = mock(Email.class);
        
                when(emailMock.getDestinatario()).thenReturn("exemplo@teste.com");
        
                boolean resultado = servicoEmail.enviarEmail(emailMock);
                assertTrue(resultado);
                verify(emailMock, times(1)).getDestinatario();
            }
        }
                                                                    
                    
                

Neste exemplo, estamos testando se o método enviarEmail() do ServicoEmail envia corretamente um e-mail, verificando se o destinatário foi definido corretamente.



Testes de Aceitação:

Os testes de aceitação, também conhecidos como testes funcionais ou testes de aceitação do usuário (ATs), verificam se o sistema atende aos requisitos e comportamentos especificados pelo usuário ou cliente. Eles testam o sistema como um todo, simulando interações do usuário.

Exemplo de um teste de aceitação usando uma ferramenta de automação de testes como o Selenium WebDriver:
                
            import org.openqa.selenium.*;
            import org.openqa.selenium.chrome.ChromeDriver;
            import org.junit.Test;
            import static org.junit.Assert.*;
            
            public class TesteLogin {
            
                @Test
                public void testLoginComCredenciaisCorretas() {
                    WebDriver driver = new ChromeDriver();
                    driver.get("https://exemplo.com");
            
                    WebElement campoUsuario = driver.findElement(By.id("usuario"));
                    WebElement campoSenha = driver.findElement(By.id("senha"));
                    WebElement botaoLogin = driver.findElement(By.id("botao-login"));
            
                    campoUsuario.sendKeys("usuario");
                    campoSenha.sendKeys("senha");
                    botaoLogin.click();
            
                    WebElement mensagemBoasVindas = driver.findElement(By.id("mensagem-boas-vindas"));
                    assertEquals("Bem-vindo, usuário!", mensagemBoasVindas.getText());
            
                    driver.quit();
                }
            }
                                                                                    
                
            

Neste exemplo, estamos testando o processo de login em um site, inserindo credenciais corretas e verificando se a mensagem de boas-vindas é exibida corretamente.

Implementar testes automatizados, incluindo testes unitários, de integração e de aceitação, é essencial para garantir a qualidade do software e aumentar a confiança no código.
Esses testes ajudam a identificar problemas precocemente e fornecem uma maneira de validar se o sistema está funcionando conforme o esperado, mesmo após mudanças no código.


No Geral:

  1. Convenções de nomenclatura: Utilizar nomes significativos e seguir as convenções de nomenclatura Java, como camelCase para nomes de variáveis e métodos, PascalCase para nomes de classes e UPPER_CASE para constantes.

  2. Organização de código: Estruturar o código de forma lógica, dividindo-o em pacotes e classes coesas, mantendo a coesão e minimizando o acoplamento entre diferentes partes do sistema.

  3. Gerenciamento de exceções: Lidar adequadamente com exceções, evitando o uso excessivo de blocos try-catch e lançando exceções apropriadas para indicar condições de erro.

  4. Uso de generics: Aproveitar ao máximo os generics para escrever código genérico e reutilizável, aumentando a segurança de tipo e reduzindo a necessidade de coerção de tipo.

  5. Tratamento de recursos: Garantir que os recursos, como conexões de banco de dados, arquivos e sockets, sejam fechados adequadamente para evitar vazamentos de recursos.

  6. Utilização de bibliotecas padrão: Fazer uso das bibliotecas padrão do Java sempre que possível em vez de reinventar a roda, promovendo a reutilização de código e a adesão a padrões estabelecidos.

  7. Documentação: Documentar o código de forma clara e concisa, incluindo comentários Javadoc para classes, métodos e variáveis públicas, explicando o propósito, comportamento e uso de cada componente.

  8. Segurança: Adotar práticas de segurança, como validar entrada do usuário, evitar injeção de SQL e proteger dados sensíveis durante a transmissão e armazenamento.

  9. Desempenho: Escrever código eficiente e escalável, evitando operações custosas dentro de loops, minimizando o uso de recursos e otimizando consultas de banco de dados.

  10. Testes: Implementar testes automatizados, incluindo testes unitários, de integração e de aceitação, para garantir que o código funcione conforme o esperado e seja resistente a alterações.

Para garantir a qualidade e robustez do código Java, é fundamental seguir boas práticas de codificação. Isso inclui utilizar nomes significativos e seguir as convenções de nomenclatura, estruturar o código de forma lógica em pacotes e classes coesas, gerenciar exceções de maneira adequada, aproveitar os generics para escrever código genérico e seguro, garantir o tratamento correto de recursos como conexões de banco de dados e, por fim, implementar testes automatizados para assegurar a qualidade e resistência do código. Essas práticas não apenas facilitam a manutenção e o entendimento do código, mas também contribuem para a criação de sistemas mais robustos e confiáveis.