Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Boas práticas de programação: melhore o código de uma API Java

Boas práticas de programação: melhore o código de uma API Java

Separação de responsabilidades - Apresentação

Boas-vindas ao curso Boas práticas de Programação em Java: melhore o código de uma API da Alura! Meu nome é Rodrigo Ferreira, sou um dos instrutores da escola de programação, e vou te acompanhar ao longo dessa jornada.

Audiodescrição: Rodrigo é um homem de pele clara, cabelo curto e liso castanho-escuro, sobrancelhas castanhas, e olhos castanhos. Ele usa um fone de ouvido branco e está sentado em uma cadeira preta vestindo uma camisa preta. No ambiente ao fundo, há uma parede branca iluminada em gradiente de azul para roxo com dois quadros de moldura preta à esquerda.

Qual o objetivo do curso?

Vamos falar um pouco sobre esse curso, qual é o objetivo dele, e o que você vai aprender conosco. Como o próprio nome já diz, aprenderemos boas práticas que podemos aplicar em uma aplicação Java, que no caso desse curso, será uma API REST.

Esse é um tipo de aplicação comum de trabalhar nas empresas. Em uma API REST, existem vários pontos em que podemos aplicar boas práticas, e ao longo do curso, aprenderemos que práticas são essas.

Para começar, vamos analisar uma frase de um autor chamado Martin Fowler, originalmente em inglês, que funciona como inspiração para esse curso:

"Qualquer tolo pode escrever um código que um computador possa entender. Bons programadores escrevem códigos que os humanos possam entender."

Essa frase é a essência do nosso curso. Com isso, Martin Fowler quis dizer que programar, isto é, escrever códigos, qualquer pessoa consegue fazer, seja ele bonito e seguindo boas práticas ou não. O computador, ao executar um código, não se preocupa com isso; ele irá executar o código de qualquer forma.

Porém, além de escrever o código que o computador irá executar, é extremamente importante escrever um código simples, fácil de ler, e fácil de dar manutenção, para que outras pessoas sejam capazes de entender e ajustá-lo da melhor maneira possível.

O que vamos aprender?

Na prática, iremos aplicar boas práticas em um projeto. Entenderemos que existem zilhões de possibilidades que podemos aplicar a um projeto. Então, em quais boas práticas iremos focar nesse curso?

Refactoring

Começaremos pela famosa refactoring, ou refatoração, que nada mais é do que melhorar um código existente. Temos um código com determinada funcionalidade, mas que não está tão legal em termos de estrutura. Nesse caso, podemos aplicar uma técnica de refatoração, melhorando o código do ponto de vista estrutural.

Note que não falamos em mudar o comportamento; a funcionalidade deverá continuar fazendo o que fazia antes. Estamos melhorando apenas a estrutura para deixar o código mais legível, entendível e simples para receber manutenção.

Aplicaremos o tempo inteiro as refatorações, que são melhorias em códigos existentes.

Design Patterns

Além disso, eventualmente, as melhorias que faremos irão implicar a utilização de padrões de projeto, chamados também de design patterns. Ao longo do curso, aprenderemos alguns dos padrões de projeto que podem ser aplicados em uma API REST.

Em alguma prática, poderemos pensar em resolver determinado problema utilizando uma solução comum, que já é adotada no mundo inteiro e é padronizada.

Porém, existe um catálogo gigantesco com dezenas de padrões de projeto, e não iremos aprender todos eles. Aplicaremos apenas os que forem necessários e fizerem sentido para o nosso contexto.

SOLID

Além dos padrões de projeto, também iremos aplicar princípios SOLID, sigla que representam cinco princípios de programação relacionados a boas práticas.

Da mesma forma, iremos aplicar apenas os que fizerem sentido no contexto do nosso projeto.

Otimizações

Por fim, faremos otimizações no código da nossa aplicação, principalmente na parte de acesso a banco de dados, para evitar problemas relacionados a performance. Aprenderemos a identificar esses pontos de melhoria e aplicar as otimizações, também no acesso ao banco de dados.

Requisitos

Para que você consiga aproveitar esse curso com a maior tranquilidade possível, existem alguns requisitos que não iremos ensinar aqui e exigimos como conhecimento prévio para acompanhar o conteúdo. São eles:

Como o projeto em que iremos trabalhar no decorrer do curso é uma API REST, não vamos ensinar o que é uma API REST. Esse não é o foco do curso. Você já precisa saber o que ela é, como ela funciona, conhecer requisições, verbos do protocolo HTTP, e assim por diante.

Iremos apenas usar os conceitos, e não aprendê-los.

O framework mais utilizado no mundo Java é usado na nossa aplicação e não iremos ensinar a usar o Spring Boot e a criar um projeto com ele. Você já precisa conhecer a ferramenta.

Junto ao Spring Boot, é importante que você conheça a JPA, especificação que cuida da parte de persistência e de acesso a banco de dados.

Esses são os três conhecimentos que você precisa ter para conseguir acompanhar o curso. Existem outros materiais na Alura que ensinam cada um desses requisitos. Explore a escola de Programação e descubra!

Esperamos que você goste do conteúdo! Ao longo do curso, além dos vídeos, você terá acesso a materiais complementares e às atividades. É muito importante que você as realize, e se surgir alguma dúvida, lembre-se de usar o fórum ou a comunidade no Discord.

Vamos começar nosso curso? Te vejo na primeira aula!

Separação de responsabilidades - Conhecendo o projeto

Vamos começar nosso curso? Conforme comentado no vídeo anterior, neste curso, focaremos em boas práticas de programação com Java.

No curso anterior, focamos bastante na parte da aplicação console, aplicação que pessoas funcionárias da AdoPet vão utilizar para fazer o cadastro dos abrigos, a listagem dos pets, e principalmente, fazer a importação do arquivo que contém os pets.

Nesse projeto, eram feitas chamadas HTTP para uma API. Porém, nesse curso anterior, o foco não era na API, e sim no projeto console. Agora vamos focar justamente no projeto da API.

Na atividade anterior, deixamos um material explicando como baixar e configurar o projeto.

Conhecendo o projeto

Começaremos com o IntelliJ aberto e com o projeto adopet-api importado. Neste vídeo, iremos conhecer o projeto que era utilizado no projeto do curso anterior.

Nesse primeiro momento, vamos explorar o código-fonte do projeto para entender quais são os problemas e o que podemos fazer para melhorar o código.

Na aba "Project" à esquerda, a princípio, identificamos ser uma aplicação que utiliza o Maven, então ela possui a estrutura de diretórios padrão do Maven. No diretório "src", temos os caminhos "main > java" (onde fica o código-fonte, as classes, e interfaces Java), "main > resources" (onde ficam os arquivos de configuração), e "test > java" (onde ficam os testes automatizados).

No diretório raiz, temos o arquivo pom.xml, que tem as dependências da aplicação. No caso, é uma API que usa o Spring Boot, seus módulos, JPA, o banco de dados MySQL, o padrão para aplicações que utilizam o Spring Boot.

Vamos ao que interessa: o código. No diretório "src > main > java", encontramos um pacote principal chamado br.com.alura.adopet.api que contém três subpacotes:

De forma solta no diretório da API, temos o arquivo AdopetApiApplication.java, classe que contém o método main() e onde rodamos a aplicação.

Conforme mencionado anteriormente, o foco desse curso não será Spring Boot e JPA, não iremos aprender o que eles são e como desenvolver uma API REST usando o Spring Boot e o JPA. Então, precisamos que você já tenha esse conhecimento bem consolidado, para podermos focar na parte de boas práticas de Programação em uma API que usa essas tecnologias.

Abaixo, temos o código da classe principal AdopetApiApplication, que executa o projeto:

package br.com.alura.adopet.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AdopetApiApplication {

  public static void main(String[] args) {
    SpringApplication.run(AdopetApiApplication.class, args);
  }

}

Nesse projeto, temos basicamente três camadas: os controladores, os modelos, e os repositórios.

Camada de controladores

Vamos acessar, por exemplo, o arquivo AbrigoController.java. Trata-se de uma classe controller onde chegam as requisições na API relacionadas a abrigos.

Essa é a classe utilizada pelo projeto console, então todas as requisições que ele disparar, cairão em AbrigoController. Nesse arquivo, temos as funcionalidades relacionadas com Abrigo, então existem métodos, por exemplo, para cadastrar um novo abrigo (cadastrar()), para listar os pets de determinado abrigo (listarPets()), para cadastrar pets do abrigo (cadastrarPet()), e assim por diante.

É nesse arquivo em especial que o projeto console faz o consumo.

No projeto, além da parte de abrigos, tem também a parte de adoção (arquivo AdocaoController.java), que corresponde ao coração do sistema, onde encontramos outras funcionalidades.

Temos, por exemplo, a funcionalidade de solicitar uma adoção (solicitar()), a de aprovar uma adoção (aprovar()), e outra para reprovar uma adoção (reprovar()).

Além disso, há o arquivo PetController.java, que possui uma funcionalidade para listarTodosDisponiveis(), e o arquivo TutorController.java, que contém as funcionalidades para a pessoa tutora se cadastrar (cadastrar()) e atualizar seus dados pessoais (atualizar()).

A nível de funcionalidade, é bastante simples. Temos as funcionalidades do abrigo, utilizadas pelo projeto console, bem como outras funcionalidades que não são usadas pelo projeto console, como as de PetController, TutorController, e principalmente a de AdocaoController.

Funcionalidade AdocaoController

Como a funcionalidade mais importante é a de adoção, vamos começar analisando o arquivo AdocaoController.java. Visto que o foco deste curso são boas práticas de programação, nosso objetivo é explorar o código-fonte e encontrar onde não são seguidas as boas práticas e, aos poucos, fazer mudanças e melhorias em cima dos códigos.

Qual é o objetivo de uma classe Controller? Controlar o fluxo de execução da aplicação, então em uma classe Controller, não deveríamos ter códigos de regra de negócio e de validação. A classe deve apenas coordenar o fluxo de execução de uma requisição.

Então, ao chegar uma requisição, será chamada a classe que vai executar uma lógica, em seguida outra classe para executar outra lógica, e com base em um retorno, ela devolverá uma informação ou outra. Esse é o objetivo de uma classe Controller.

Porém, analisando a classe AdocaoController, identificamos alguns métodos com as funcionalidades de solicitar(), aprovar() e reprovar() uma adoção.

Avaliando o escopo do método solicitar(), por exemplo, percebemos que não são é seguida a boa prática de uma classe Controller.

public ResponseEntity<String> solicitar(@RequestBody @Valid Adocao adocao) {
    if (adocao.getPet().getAdotado() == true) {
        return ResponseEntity.badRequest().body("Pet já foi adotado!");
    } else {
        List<Adocao> adocoes = repository.findAll();
        for (Adocao a : adocoes) {
            if (a.getTutor() == adocao.getTutor() && a.getStatus() == StatusAdocao.AGUARDANDO_AVALIACAO) {
                return ResponseEntity.badRequest().body("Tutor já possui outra adoção aguardando avaliação!");
            }
        }
        for (Adocao a : adocoes) {
            if (a.getPet() == adocao.getPet() && a.getStatus() == StatusAdocao.AGUARDANDO_AVALIACAO) {
                return ResponseEntity.badRequest().body("Pet já está aguardando avaliação para ser adotado!");
            }
        }
        for (Adocao a : adocoes) {
            int contador = 0;
            if (a.getTutor() == adocao.getTutor() && a.getStatus() == StatusAdocao.APROVADO) {
                contador = contador + 1;
            }
            if (contador == 5) {
                return ResponseEntity.badRequest().body("Tutor chegou ao limite máximo de 5 adoções!");
            }
        }
    }

Toda a regra de negócio está solta dentro do controller. Nesse caso, temos todo um algoritmo que faz uma verificação em cima da Adocao, consulta as informações no banco de dados, percorre uma lista, faz os blocos condicionais if, faz validações, lança erros, ou seja, um código de regra de negócio dentro do controller, o que não é uma boa prática.

Já encontramos um ponto de melhoria! Nos vídeos seguintes, faremos as melhorias no código.

Para os outros métodos, temos o mesmo: é executada uma lógica, feito o disparo de um e-mail, tudo dentro do controller, o que não é considerado boa prática.

@PutMapping("/aprovar")
@Transactional
public ResponseEntity<String> aprovar(@RequestBody @Valid Adocao adocao) {
    adocao.setStatus(StatusAdocao.APROVADO);
    repository.save(adocao);

    SimpleMailMessage email = new SimpleMailMessage();
    email.setFrom("adopet@email.com.br");
    email.setTo(adocao.getTutor().getEmail());
    email.setSubject("Adoção aprovada");
    email.setText("Parabéns " +adocao.getTutor().getNome() +"!\n\nSua adoção do pet " +adocao.getPet().getNome() +", solicitada em " +adocao.getData().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) +", foi aprovada.\nFavor entrar em contato com o abrigo " +adocao.getPet().getAbrigo().getNome() +" para agendar a busca do seu pet.");
    emailSender.send(email);

    return ResponseEntity.ok().build();
}

Os outros controllers, como PetController.java, por exemplo, fazem o mesmo: executam os algoritmos e a regras de negócio no código em si do controller.

Esse é o primeiro ponto de melhoria que identificamos. Em breve, aprenderemos a solucionar essa questão usando boas práticas em uma classe Controller.

Camada de repositórios

No pacote repository, existem algumas interfaces. Vamos acessar, por exemplo, a interface AdocaoRepository.java. Trata-se do repository do Spring Boot, do módulo Spring Data JPA, que apenas herda da interface JpaRepository<> e no momento está vazia, pois ela herda os métodos das operações CRUD (cadastrar, excluir, atualizar no banco de dados).

package br.com.alura.adopet.api.repository;

import br.com.alura.adopet.api.model.Adocao;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AdocaoRepository extends JpaRepository<Adocao, Long> {

}

A princípio, não identificamos nenhuma má prática no código. Afinal, essa interface é o tipo de código mais simples no Spring Boot. Existem outros repositórios, como o AbrigoRepository, que têm alguns métodos, mas eles seguem as boas práticas do Spring: eles recebem parâmetros e o Spring cria as consultas no banco de dados.

package br.com.alura.adopet.api.repository;

import br.com.alura.adopet.api.model.Abrigo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AbrigoRepository extends JpaRepository<Abrigo, Long> {
    boolean existsByNome(String nome);

    boolean existsByTelefone(String telefone);

    boolean existsByEmail(String email);

    Abrigo findByNome(String nome);
}

Aparentemente, não há problemas nos repositórios.

Camada de modelos

Por fim, temos o pacote models, onde estão as entidades JPA do projeto. Podemos acessar, por exemplo, a entidade Adocao (Adocao.java) para tentar identificar alguma má prática.

No código, temos os atributos e as anotações. Alguns pontos podem ser ajustados, como os atributos que estão anotados com @Column, pois nem sempre é obrigatório utilizá-la.

Observe o trecho abaixo:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

Quando o nome do atributo é igual ao nome da coluna do banco de dados, no caso, id, não precisamos incluir a anotação @Column. Então, temos trechos de código desnecessários.

Além disso, temos anotações do Bean Validation na entidade, como @NotNull e outras anotações de validação. Há também anotações do JSON (@JsonBackReference), da biblioteca Jackson, usada pelo Spring para converter para JSON.

Então, a entidade está um pouco poluída. Além de ter elementos de JPA, ela tem outros de validação e do JSON, o que não é considerado boa prática.

A entidade Adocao tem alguns relacionamentos com outras entidades, e não segue algumas boas práticas quanto ao carregamento desses relacionamentos.

Conclusão

É um código pequeno e simples, porém, talvez você já tenha trabalhado em um projeto de API e Java dessa forma ou escrito um código parecido com o que temos, e conseguimos identificar algumas más práticas.

Nosso objetivo ao longo do curso será aprender como resolver essas práticas, de modo a melhorar os códigos, seja em um controller, em um repository, em uma entidade JPA, ou em outros trechos da aplicação.

Abordaremos tudo isso com calma na sequência. Pegaremos cada um desses pontos da aplicação para aplicar as boas práticas. Te vejo lá!

Separação de responsabilidades - Extraindo camada Service

Já identificamos os pontos de melhoria no projeto. Existem várias classes e códigos que não seguem as boas práticas, e atacaremos cada problema por vez. A primeira classe que iremos trabalhar para fazer melhorias será a controller.

Boas práticas na classe controller

Vamos usar, por exemplo, o arquivo AdocaoController.java. Conforme mencionado, o objetivo de uma classe controller é controlar o fluxo de uma requisição. Porém, em AdocaoController, os métodos contidos nele executam regras de negócio, o que consideramos uma má prática.

Dessa forma, toda a lógica do método solicitar(), por exemplo, os blocos if, os loops, tudo o que entendemos como regra de negócio não deveria estar no controller.

Geralmente, colocamos esse tipo de código dentro de uma classe Service e o controller apenas a chama. É exatamente isso que faremos: vamos extrair todo o código para a classe Service.

Extraindo a camada Service

Para seguir o mesmo padrão de organização de código do nosso projeto, vamos abrir a aba "Project" à esquerda e criar um novo pacote chamado service, onde ficarão as classes Service do projeto. Para isso, clicaremos sobre o pacote raiz br.com.alura.adopet.api com o botão direito, selecionaremos a opção "Package", e digitaremos .service na janela aberta.

Agora, com o pacote service selecionado, vamos usar o atalho "Alt + Insert" e selecionar a opção "Java Class" para criar uma classe Java chamada AdocaoService.

Seguimos esse padrão de nomenclatura, com o nome da classe Adocao seguido do sufixo Service, para facilitar a identificação.

Feito isso, teremos a seguinte estrutura de código:

AdocaoService.java:

package br.com.alura.adopet.api.service;

public class AdocaoService {
}

Como o projeto está usando o Spring, precisamos adicionar uma anotação acima da classe. Caso contrário, não conseguiremos chamá-la da classe controller. A anotação será @Service justamente para dizer que é uma classe de serviço.

package br.com.alura.adopet.api.service;

import org.springframework.stereotype.Service;

@Service
public class AdocaoService {
}

Declarando os métodos

Agora, no escopo da classe AdocaoService, precisamos criar os mesmos métodos que existem no controller, mas nesse caso, trataremos apenas da regra de negócio.

Em AdocaoController, temos três métodos: o solicitar(), o aprovar(), e o reprovar(). Precisamos ter os três na classe AdocaoService. Vamos escrevê-los vazios por enquanto, e depois iremos extrair do controller para a classe.

Começaremos criando um método public com o retorno vazio (void) chamado solicitar(). Em seguida, faremos o mesmo com os métodos aprovar() e reprovar().

// código omitido

@Service
public class AdocaoService {

    public void solicitar() {
    
    }
    
    public void aprovar() {
    
    }
    
    public void reprovar() {
    
    }

}

Temos os três métodos declarados na classe Service, porém, vazios. Agora precisamos retornar ao arquivo AdocaoController.java e extrair todo o código dos métodos para o arquivo AdocaoService.java, a princípio, recortando e colando. Vamos fazer isso?

Criando o método solicitar()

Do método solicitar(), recortaremos todo o escopo abaixo:

AdocaoController.java:

if (adocao.getPet().getAdotado() == true) {
    return ResponseEntity.badRequest().body("Pet já foi adotado!");
} else {
    List<Adocao> adocoes = repository.findAll();
    for (Adocao a : adocoes) {
        if (a.getTutor() == adocao.getTutor() && a.getStatus() == StatusAdocao.AGUARDANDO_AVALIACAO) {
            return ResponseEntity.badRequest().body("Tutor já possui outra adoção aguardando avaliação!");
        }
    }
    for (Adocao a : adocoes) {
        if (a.getPet() == adocao.getPet() && a.getStatus() == StatusAdocao.AGUARDANDO_AVALIACAO) {
            return ResponseEntity.badRequest().body("Pet já está aguardando avaliação para ser adotado!");
        }
    }
    for (Adocao a : adocoes) {
        int contador = 0;
        if (a.getTutor() == adocao.getTutor() && a.getStatus() == StatusAdocao.APROVADO) {
            contador = contador + 1;
        }
        if (contador == 5) {
            return ResponseEntity.badRequest().body("Tutor chegou ao limite máximo de 5 adoções!");
        }
    }
}
adocao.setData(LocalDateTime.now());
adocao.setStatus(StatusAdocao.AGUARDANDO_AVALIACAO);
repository.save(adocao);

SimpleMailMessage email = new SimpleMailMessage();
email.setFrom("adopet@email.com.br");
email.setTo(adocao.getPet().getAbrigo().getEmail());
email.setSubject("Solicitação de adoção");
email.setText("Olá " +adocao.getPet().getAbrigo().getNome() +"!\n\nUma solicitação de adoção foi registrada hoje para o pet: " +adocao.getPet().getNome() +". \nFavor avaliar para aprovação ou reprovação.");
emailSender.send(email);

return ResponseEntity.ok().build();

Uma vez recortado, podemos colar no escopo do método solicitar() na classe AdocaoService.

Declarando variáveis

Ao fazer isso, serão marcados erros de compilação, pois precisaremos fazer algumas adaptações. Não basta recortar e colar; devemos adaptar alguns pontos.

São reclamadas, por exemplo, algumas variáveis inexistentes, como repository na linha 42 e emailSender na linha 49. Sendo assim, precisamos retornar à classe controller, onde era usada injeção de dependências com os seguintes atributos:

AdocaoController.java:

@Autowired
private AdocaoRepository repository;

@Autowired
private JavaMailSender emailSender;

Vamos recortar esse trecho e mover para o arquivo AdocaoService.java declarando como atributos, antes da declaração do método solicitar(). Com isso, quem irá usar as variáveis repository e emailSender não será o controller, e sim a classe Service.

Usando a classe Service no controller

No arquivo AdocaoController.java, ficamos sem nenhum atributo. Porém, será necessário um atributo da classe Service, então Service usará repository e emailSender e o Controller usará adocaoService.

Para isso, no lugar dos atributos que movemos para a classe Service, vamos declarar o atributo AdocaoService como private, chamaremos ele de adocaoService e ele receberá a anotação @Autowired para o Spring fazer a injeção de dependências.

@Autowired
private AdocaoService adocaoService;

Conforme dito anteriormente, o controller irá usar a classe Service, então no método solicitar() do controller, vamos chamar o método solicitar() do arquivo AdocaoService.java. Para isso, digitamos this.adocaoService.solicitar().

@PostMapping
@Transactional
public ResponseEntity<String> solicitar(@RequestBody @Valid Adocao adocao) {
    this.adocaoService.solicitar();
}

Essa é a ideia do controller: ele não executa a regra de negócio, ele chama a classe que contém essa regra, que no nosso caso, é a classe Service.

No momento, ainda temos algumas variáveis faltando na classe Service. Para a lógica fazer a adoção (adocao), precisamos do objeto que tenha os dados que chegam na requisição.

Na linha 25, por exemplo, ainda há um erro de compilação, pois não temos a variável adocao. Então, o método solicitar() da classe Service precisa receber um parâmetro do tipo Adocao. Dessa forma, os dados da adoção chegam como parâmetro.

public void solicitar(Adocao adocao) {

// código omitido

No controller, o método solicitar() recebe um objeto Adocao como parâmetro, e chama o Service passando esse parâmetro no método solicitar(), então passamos adocao entre parênteses.

@PostMapping
@Transactional
public ResponseEntity<String> solicitar(@RequestBody @Valid Adocao adocao) {
    this.adocaoService.solicitar(adocao);
}

Assim, o parâmetro é recebido e delegado para a classe Service, que ficará responsável por lidar com o parâmetro adocao executando as regras de negócio.

Atribuindo responsabilidades ao controller

De volta à classe Service, não teremos mais o erro de compilação nas variáveis. Porém, note que na linha 25 do método solicitar(), fazemos um return de ResponseEntity.badRequest().

AdocaoService.java:

return ResponseEntity.badRequest().body("Pet já foi adotado!");

Com isso, dizemos que se há um erro de validação, devolvemos badRequest(). Porém, esse trecho não deve ficar na Service; isso é responsabilidade do controller.

A classe Service não deve lidar com coisas do protocolo HTTP, com aspectos de requisição e resposta, pois essa é a responsabilidade do controller.

Nesse caso, não podemos responder com ResponseEntity. Precisamos indicar de alguma forma para o controller que aconteceu um erro e é ele que devolve o ResponseEntity.

Faremos a seguinte mudança: se entramos no bloco if, é porque alguma regra de negócio foi violada, então jogamos um erro. Para isso, podemos usar a exception do Java; vamos digitar throw new seguido do nome da exceção, representada por uma classe que chamaremos de ValidacaoException().

Feito isso, moveremos a string com a mensagem "Pet já foi adotado!" para ValidacaoException() e removeremos o return da linha abaixo.

if (adocao.getPet().getAdotado() == true) {
    throw new ValidacaoException("Pet já foi adotado!");
}

Na classe Service, não retornamos um objeto ResponseEntity. Se houver algum erro de validação, lançamos uma exception, a qual o controller captura e devolve o ResponseEntity.

Criando a classe ValidacaoException

Nesse momento, teremos um erro de compilação, pois a classe ValidacaoException ainda não existe. Vamos usar o atalho "Alt + Enter" e selecionar a primeira opção para o IntelliJ criar a classe em um pacote chamado exception.

Feito isso, teremos a seguinte estrutura de código:

ValidacaoException.java:

package br.com.alura.adopet.api.exception;

public class ValidacaoException extends Throwable {
    public ValidacaoException(String s) {
    }
}

No momento, a classe Exception é identificada, porém, ela é herdada de Throwable. Geralmente, herdamos de RuntimeException quando criamos uma exceção não checada no Java.

public class ValidacaoException extends RuntimeException {

Assim, é criado um construtor que recebe uma mensagem (parâmetro message), mas no momento, ele não faz nada com o parâmetro. Então, precisamos passá-lo para o construtor da classe que estamos herdando. Para isso, basta chamar super() e passar message como parâmetro.

package br.com.alura.adopet.api.exception;

public class ValidacaoException extends RuntimeException {
    public ValidacaoException(String message) {
        super(message);
    }
}

Ajustando os demais return

Agora precisamos fazer o mesmo processo com os outros return do método solicitar(). Não podemos retornar um ResponseEntity, mas sim lançar uma Exception, sendo que cada exceção terá uma mensagem distinta. No caso da linha 32, vamos passar a mensagem "Tutor já possui outra adoção aguardando avaliação!".

AdocaoService.java:

if (a.getTutor() == adocao.getTutor() && a.getStatus() == StatusAdocao.AGUARDANDO_AVALIACAO) {
    throw new ValidacaoException("Tutor já possui outra adoção aguardando avaliação!");
}

Na linha 37, trocamos pela mensagem "Pet já está aguardando avaliação para ser adotado!", e na linha 46, pela mensagem "Tutor chegou ao limite máximo de 5 adoções!".

Linha de código 37:

if (a.getPet() == adocao.getPet() && a.getStatus() == StatusAdocao.AGUARDANDO_AVALIACAO) {
    throw new ValidacaoException("Pet já está aguardando avaliação para ser adotado!");
}

Linha de código 46:

if (contador == 5) {
    throw new ValidacaoException("Tutor chegou ao limite máximo de 5 adoções!");
}

Agora temos um último return na linha 62, o qual iremos apagar, afinal, o método solicitar() tem o retorno do tipo void, ou seja, não devolve nada.

Trecho a ser removido:

return ResponseEntity.ok().build();

A única coisa que o método faz é lançar uma exception se houver algum erro, e se não houver erro, executar a regra de negócio, isto é, salva a adoção no banco de dados e dispara o envio do e-mail.

Criando um bloco try…catch

Agora teremos um erro de compilação no controller, pois precisamos devolver um ResponseEntity. Chamamos o método solicitar() e ele pode lançar uma exception, então se quisermos capturar a exceção, podemos colocar o código dentro de um bloco try…catch.

No bloco try, teremos a chamada de this.adocaoService.solicitar(adocao), enquanto no bloco catch, se acontecer uma ValidationException, iremos capturá-la. No escopo desse bloco, faremos o return de ResponseEntity.badRequest().body(). Entre os parênteses de body(), vamos passar a exceção seguida de getMessage() para pegar a string da mensagem de erro.

Se não houver erro, não entraremos no bloco catch, mas precisamos devolver algo. Então, abaixo de adocaoService.solicitar(), vamos adicionar um return de ResponseEntity.ok() recebendo a mensagem "Adoção solicitada com sucesso!".

Ao final, teremos o seguinte resultado:

AdocaoController.java:

@PostMapping
@Transactional
public ResponseEntity<String> solicitar(@RequestBody @Valid SolicitacaoAdocaoDto dto) {
    try {
        this.adocaoService.solicitar(adocao);
        return ResponseEntity.ok("Adoção solicitada com sucesso!");
    } catch (ValidationException e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

Perceba como o método no controller está muito mais limpo e simples. O método chama a classe Service, e se não der exception, é devolvido ok("Adoção solicitada com sucesso!") como resposta; se der exception, a exceção é capturada, é devolvido o código 400 (badRequest()), e colocada a mensagem que vem na exception.

Temos um código bastante enxuto e simples, seguindo boas práticas para controllers. Ele não executa a regra de negócio; ele chama a classe que executa e apenas controla o fluxo.

É assim que um controller deve ser: ele não pode ter regras de negócio.

Criando os métodos aprovar() e reprovar()

Agora precisamos fazer o mesmo para os métodos aprovar() e reprovar().

Começaremos pelo mesmo processo de extrair o escopo do método aprovar() do arquivo AdocaoController.java para o arquivo AdocaoService.java.

Trecho a ser copiado de AdocaoController.java:

adocao.setStatus(StatusAdocao.APROVADO);
repository.save(adocao);

SimpleMailMessage email = new SimpleMailMessage();
email.setFrom("adopet@email.com.br");
email.setTo(adocao.getTutor().getEmail());
email.setSubject("Adoção aprovada");
email.setText("Parabéns " +adocao.getTutor().getNome() +"!\n\nSua adoção do pet " +adocao.getPet().getNome() +", solicitada em " +adocao.getData().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) +", foi aprovada.\nFavor entrar em contato com o abrigo " +adocao.getPet().getAbrigo().getNome() +" para agendar a busca do seu pet.");
emailSender.send(email);

O método aprovar() também precisa receber como parâmetro um objeto do tipo Adocao.

AdocaoService.java:

public void aprovar(Adocao adocao) {

// código omitido

Em seguida, faremos o mesmo com o método reprovar().

AdocaoController.java:

adocao.setStatus(StatusAdocao.REPROVADO);
repository.save(adocao);

SimpleMailMessage email = new SimpleMailMessage();
email.setFrom("adopet@email.com.br");
email.setTo(adocao.getTutor().getEmail());
email.setSubject("Adoção reprovada");
email.setText("Olá " +adocao.getTutor().getNome() +"!\n\nInfelizmente sua adoção do pet " +adocao.getPet().getNome() +", solicitada em " +adocao.getData().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) +", foi reprovada pelo abrigo " +adocao.getPet().getAbrigo().getNome() +" com a seguinte justificativa: " +adocao.getJustificativaStatus());
emailSender.send(email);

Igualmente, o método reprovar() na classe Service precisa receber o objeto Adocao como parâmetro.

AdocaoService.java:

public void reprovar(Adocao adocao) {

// código omitido

Assim, é executada a lógica para fazer a reprovação. Nesse caso, não há nenhuma validação de regra de negócio, então não temos o throw new exception.

Já no controller, precisamos chamar o método do Service. Então, no escopo do método aprovar() em AdocaoController, vamos digitar this.adocaoService.aprovar() passando para ele o objeto adocao. O mesmo será feito no método reprovar().

Método aprovar():

@PutMapping("/aprovar")
@Transactional
public ResponseEntity<String> aprovar(@RequestBody @Valid Adocao adocao) {
    this.adocaoService.aprovar(adocao);
    return ResponseEntity.ok().build();
}

Método reprovar():

@PutMapping("/reprovar")
@Transactional
public ResponseEntity<String> reprovar(@RequestBody @Valid Adocao adocao) {
    this.adocaoService.reprovar(adocao);
    return ResponseEntity.ok().build();

Conclusão

Com isso, está feita a nossa primeira melhoria no código da API: a classe controller não deve ter regras de negócio. Se você já escreveu algum código de um controller com regras de negócio, ele não segue as boas práticas, algo no sentido de o controller chamar a classe que executa a regra de negócio e essa classe faz todas as validações. Se houver algum erro, ela lança uma exception que pode ser capturada no controller.

Conseguimos deixar o código da classe controller muito mais enxuto e simples, seguindo as boas práticas de uma classe controller. Concluímos nosso primeiro objetivo!

Porém, ainda podemos melhorar o código da classe Service, que está executando regra de negócio. Por exemplo: essa classe faz o envio do e-mail, e o código se repete nos três métodos.

Nesse caso, poderíamos extrair isso para outra classe, afinal, as outras classes Service podem precisar enviar e-mail e queremos evitar a repetição do trecho de código em todas as classes Service do projeto.

Aprenderemos isso na sequência!

Sobre o curso Boas práticas de programação: melhore o código de uma API Java

O curso Boas práticas de programação: melhore o código de uma API Java possui 158 minutos de vídeos, em um total de 38 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Plus

De
R$ 1.800
12X
R$109
à vista R$1.308
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

Matricule-se

Pro

De
R$ 2.400
12X
R$149
à vista R$1.788
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Luri, a inteligência artificial da Alura

    Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com Luri até 100 mensagens por semana.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas