Depois de trazer alguns ensinamentos sobre Clean Code no meu último artigo, estou trazendo agora um conteúdo mais prático (em Java), demonstrando alguns princípios de refatoração em um código para atender melhor as práticas de Clean Code. Lembrando que, segundo Robert C. Martin (conhecido como Uncle Bob), é recomendável aplicar estes procedimentos de Clean Code em projetos maiores que 5 mil linhas, onde a complexidade do código acaba se tornando um impeditivo de melhorias e correções de bugs.
Vou utilizar como exemplo uma aplicação que desenvolvi enquanto fazia o curso "Criando um blog com Spring Boot e deploy na AWS Elastic Beanstalk" da Michelli Brito. Inclusive, recomendo este curso para quem está aprendendo Spring Boot ou Thymeleaf (link para o curso, https://www.youtube.com/watch?v=UdJYuwnqL3I&list=PL8iIphQOyG-AdKMQWtt1bqdVm8QUnX7_S&index=1&ab_channel=MichelliBrito).
Para quem estiver utilizando o Eclipse ou STS ou IntelliJ no desenvolvimento de aplicações em Java, recomendo instalar o plugin SonarLint. Este plugin exibe diversas métricas de Code Smells diretamente nas classes, apontando uma estatística do que precisa ser melhorado e o quais correções são necessárias para se adequar aos princípios de Código Limpo, além de dar uma visão geral de todos os pontos do programa apresentam code smells ou vulnerabilidades.
Loja de plug-ins do STS exibindo o SonarLint
Análise do código através do SonarLint
Como essa aplicação que irei refatorar agora possui apenas algumas centenas de linhas de código, não conseguirei abranger todos os pontos de boas práticas, mas vou dar uma visão geral. Vou apresentar cada classe antes e depois da refatoração, explicando os pontos que foram alterados e as motivações.
Começando com a classe SecurityConfig, vou retirar apenas o comentário na linha 31, pois não devemos deixar código comentado em nossas classes.
package com.spring.codeblog.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_LIST = { "/", "/posts", "/posts/{id}" };
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers(AUTH_LIST).permitAll().anyRequest().authenticated().and()
.formLogin().permitAll().and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("michelli").password("{noop}123").roles("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/bootstrap/**");
// web.ignoring().antMatchers("/bootstrap/**", "/style/**");
}
}
Classe SecurityConfig antes da refatoração
package com.spring.codeblog.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_LIST = { "/", "/posts", "/posts/{id}" };
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers(AUTH_LIST).permitAll().anyRequest().authenticated().and()
.formLogin().permitAll().and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("michelli").password("{noop}123").roles("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/bootstrap/**");
}
}
Classe SecurityConfig depois da refatoração
Prosseguindo para a classe CodingBlogController, o atributo da linha 24 não possui o modificador de acesso, vamos adicionar o modificador private nele. Também vamos trocar os quatro tipos @RequestMapping genéricos por seus tipos mais específicos (@GetMapping e @PostMapping).
Depois, vamos extrair todas as String que estão hardcore e transformá-las em constantes, mantendo o código mais limpo e reutilizando-as em vários locais da classe. Comentando alguns pontos de Código Limpo que já estão sendo aplicados nesta Classe: os nomes dos métodos são claros, exemplificando sua função e são constituídos de verbos mais substantivos, além de terem poucas linhas. Os nomes das variáveis poderiam ser refatorados para explicar de maneira mais explicita qual a sua função no código. Porém, como eles identificam seu uso, ainda que de forma resumida, vamos mantê-los do mesmo jeito.
Os métodos possuem poucos parâmetros, com exceção do savePost, o que é uma boa prática segundo o Clean Code. Com relação ao método savePost, além de termos muitos parâmetros, temos uma refatoração a fazer no laço condicional if. Neste if, caso ocorra um erro no resultado, ele retomará uma mensagem de erro no próprio corpo da condicional. Essa mudança vai permitir enxugarmos um pouco o método, já que agora não precisaremos mais utilizar o else, pois caso a condição seja de erro, ele retornará o erro e encerrará o método.
package com.spring.codeblog.controller;
import java.time.LocalDate;
import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.spring.codeblog.model.Post;
import com.spring.codeblog.service.CodeBlogService;
@Controller
public class CodingBlogController {
@Autowired
CodeBlogService codeblogService;
@RequestMapping(value="/posts", method=RequestMethod.GET)
public ModelAndView getPosts() {
ModelAndView mv = new ModelAndView("posts");
List<Post> posts = codeblogService.findAll();
mv.addObject("posts", posts);
return mv;
}
@RequestMapping(value="/posts/{id}", method=RequestMethod.GET)
public ModelAndView getPostsDetails(@PathVariable("id") long id) {
ModelAndView mv = new ModelAndView("postDetails");
Post post = codeblogService.findById(id);
mv.addObject("post", post);
return mv;
}
@RequestMapping(value="/newpost", method=RequestMethod.GET)
public String getPostForm() {
return "postForm";
}
@RequestMapping(value="/newpost", method=RequestMethod.POST)
public String savePost(@Valid Post post, BindingResult result, RedirectAttributes attributes) {
if(result.hasErrors()) {
attributes.addFlashAttribute("mensagem", "verifique se os campos obrigatórios foram preenchidos");
return "redirect:/newpost";
}
post.setData(LocalDate.now());
codeblogService.save(post);
return "redirect:/posts";
}
}
Classe CodingBlogController antes da refatoração
package com.spring.codeblog.controller;
import java.time.LocalDate;
import java.util.List;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.spring.codeblog.model.Post;
import com.spring.codeblog.service.CodeBlogService;
@Controller
public class CodingBlogController {
private static final String POSTS = "posts";
private static final String POST_DETAILS= "postDetails";
private static final String POST_FORM = "postForm";
private static final String MESSAGE = "mensagem";
private static final String VERIFICA_CAMPOS = "verifique se os campos obrigatórios foram preenchidos";
@Autowired
private CodeBlogService codeblogService;
@GetMapping(value="/posts")
public ModelAndView getPosts() {
ModelAndView mv = new ModelAndView(POSTS);
List<Post> posts = codeblogService.findAll();
mv.addObject(POSTS, posts);
return mv;
}
@GetMapping(value="/posts/{id}")
public ModelAndView getPostsDetails(@PathVariable("id") long id) {
ModelAndView mv = new ModelAndView(POST_DETAILS);
Post post = codeblogService.findById(id);
mv.addObject(POSTS, post);
return mv;
}
@GetMapping(value="/newpost")
public String getPostForm() {
return POST_FORM;
}
@PostMapping(value="/newpost")
public String savePost(@Valid Post post, BindingResult result, RedirectAttributes attributes) {
if(result.hasErrors()) {
attributes.addFlashAttribute(MESSAGE, VERIFICA_CAMPOS);
return "redirect:/newpost";
}
post.setData(LocalDate.now());
codeblogService.save(post);
return "redirect:/posts";
}
}
Classe CodingBlogController depois da refatoração
A classe Post está atendendo os princípios básicos de Código Limpo, pois todos os atributos estão seguindo o padrão CamelCase, a variável date está especificando o formato de saída (pattern="dd-MM-yyyy"), e os métodos get, set e toString também estão seguindo as boas práticas.
package com.spring.codeblog.model;
import java.time.LocalDate;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import com.fasterxml.jackson.annotation.JsonFormat;
@Entity
@Table(name="TB_POST")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotBlank
private String titulo;
@NotBlank
private String autor;
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="dd-MM-yyyy")
private LocalDate data;
@NotBlank
@Lob
private String texto;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public String getAutor() {
return autor;
}
public void setAutor(String autor) {
this.autor = autor;
}
public LocalDate getData() {
return data;
}
public void setData(LocalDate data) {
this.data = data;
}
public String getTexto() {
return texto;
}
public void setTexto(String texto) {
this.texto = texto;
}
@Override
public String toString() {
return "Post [id=" + id + ", titulo=" + titulo + ", autor=" + autor + ", data=" + data + ", texto=" + texto
+ "]";
}
}
Classe Post
Como a classe CodedBlogRepository é apenas uma interface, não há nada que precisamos refatorar.
package com.spring.codeblog.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.codeblog.model.Post;
public interface CodedBlogRepository extends JpaRepository<Post, Long> {
}
Classe CodedBlogRepository
Quanto a classe CodeBlogService, já está sendo seguidas as boas práticas em Java (de utilizar uma interface para isolar os métodos da classe, algo que também deveriam ter sido feita na Controller).
Os nomes dos métodos estão seguindo o padrão verbo mais substantivos e os atributos da classe apresentam com nomes claros e bem explicativos. O que poderia ser melhorado seria especificar melhor o nome do método save, alterando-o para algo como savePostData, o que deixaria seu objetivo mais claro para um leitor externo.
package com.spring.codeblog.service;
import java.util.List;
import com.spring.codeblog.model.Post;
public interface CodeBlogService {
List<Post> findAll();
Post findById(long id);
Post save(Post post);
}
Classe CodeBlogService
Na classe CodeblogServiceImpl, vamos colocar o modificador de acesso no atributo da Repository, na linha 17. No método findById, o ideal é verificarmos primeiro se o método codeBlogRepository.findById(id) está retornando algum valor, para depois pegarmos este valor. Para fazer isso, vamos declarar uma variável do tipo Optional e receber o retorno da busca do método findById.
Após isso, vamos verificar se há algum valor no método retorno. Caso haja, vamos retomar o valor, do contrário, vamos retornar uma nova instância da classe. Como sugestão de melhoria, o método "retorno" poderia ter um nome mais claro, algo como "retornoBuscaDadosPost".
package com.spring.codeblog.service.serviceImp;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.spring.codeblog.model.Post;
import com.spring.codeblog.repository.CodedBlogRepository;
import com.spring.codeblog.service.CodeBlogService;
@Service
public class CodeblogServiceImpl implements CodeBlogService {
@Autowired
CodedBlogRepository codeBlogRepository;
@Override
public List<Post> findAll() {
return codeBlogRepository.findAll();
}
@Override
public Post findById(long id) {
return codeBlogRepository.findById(id).get();
}
@Override
public Post save(Post post) {
return codeBlogRepository.save(post);
}
}
Classe CodeBlogServiceImpl antes da refatoração
package com.spring.codeblog.serviceimpl;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.spring.codeblog.model.Post;
import com.spring.codeblog.repository.CodedBlogRepository;
import com.spring.codeblog.service.CodeBlogService;
@Service
public class CodeblogServiceImpl implements CodeBlogService {
@Autowired
private CodedBlogRepository codeBlogRepository;
@Override
public List<Post> findAll() {
return codeBlogRepository.findAll();
}
@Override
public Post findById(long id) {
Optional<Post> retorno = codeBlogRepository.findById(id);
if (retorno.isPresent()) {
return retorno.get();
}
return new Post();
}
@Override
public Post save(Post post) {
return codeBlogRepository.save(post);
}
}
Classe CodeBlogServiceImpl depois da refatoração
Na Classe DummyData, vamos adicionar o modificador de acesso private no atributo CodeBlogRepository, na linha 17. Em seguida, vamos excluir o código comentado da linha 19 (pois já mantemos o histórico de alterações no GitHub).
No método savePost, temos dois blocos grandes de código que fazem quase a mesma coisa. Eles podem ser refatorados para um único método e passados os seus valores através dos parâmetros. Além disso, vamos extrair os valores para constantes para melhorar a legibilidade do código. Como este novo método é interno da classe, ele será do tipo private.
Ainda no método savePost, temos um laço for que vai percorrer a lista de dados e salvar o objeto no banco de dados e podemos substituí-lo por um forEach para melhorar a legibilidade. Por fim, não é recomendado utilizar o System.out.println para exibir informações na tela, então vamos substituí-lo por um LOGGER do nível Info.
package com.spring.codeblog.util;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.spring.codeblog.model.Post;
import com.spring.codeblog.repository.CodedBlogRepository;
@Component
public class DummyData {
@Autowired
CodedBlogRepository codedBlogRepository;
//@PostConstruct
public void savePosts() {
List<Post> postList = new ArrayList<>();
Post post1 = new Post();
post1.setAutor("Bruno Alexandre");
post1.setData(LocalDate.now());
post1.setTitulo("Docker");
post1.setTexto(
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has "
+ "been the industry's standard dummy text ever since the 1500s, when an unknown printer took a"
+ " galley of type and scrambled it to make a type specimen book. It has survived not only five "
+ "centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It "
+ "was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, "
+ "and more recently with desktop publishing software like Aldus PageMaker including versions of "
+ "Lorem Ipsum.");
Post post2 = new Post();
post2.setAutor("Michelli Brito");
post2.setData(LocalDate.now());
post2.setTitulo("API REST");
post2.setTexto(
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has "
+ "been the industry's standard dummy text ever since the 1500s, when an unknown printer took a"
+ " galley of type and scrambled it to make a type specimen book. It has survived not only five "
+ "centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It "
+ "was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, "
+ "and more recently with desktop publishing software like Aldus PageMaker including versions of "
+ "Lorem Ipsum.");
postList.add(post1);
postList.add(post2);
for (Post post : postList) {
Post postSaved = codedBlogRepository.save(post);
System.out.println(postSaved.getId());
}
}
}
Classe DummyData antes da refatoração
package com.spring.codeblog.util;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.spring.codeblog.model.Post;
import com.spring.codeblog.repository.CodedBlogRepository;
@Component
public class DummyData {
private static final Logger LOGGER = LoggerFactory.getLogger(DummyData.class);
private static final String TEXTO = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has "
+ "been the industry's standard dummy text ever since the 1500s, when an unknown printer took a"
+ " galley of type and scrambled it to make a type specimen book. It has survived not only five "
+ "centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It "
+ "was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, "
+ "and more recently with desktop publishing software like Aldus PageMaker including versions of "
+ "Lorem Ipsum.";
@Autowired
private CodedBlogRepository codedBlogRepository;
public void savePosts() {
String nomeAutor = "Denis Machado";
String titulo = "API REST";
Post post1 = persisteDadosPost(nomeAutor, titulo);
nomeAutor = "Michelli Brito";
titulo = "API REST";
Post post2 = persisteDadosPost(nomeAutor, titulo);
List<Post> postList = new ArrayList<>();
postList.add(post1);
postList.add(post2);
postList.forEach(post -> {
Post postSaved = codedBlogRepository.save(post);
LOGGER.info("Post salvo: {}", postSaved.getId());
});
}
private Post persisteDadosPost(String nomeAutor, String titulo) {
Post post = new Post();
post.setAutor(nomeAutor);
post.setData(LocalDate.now());
post.setTitulo(titulo);
post.setTexto(TEXTO);
return post;
}
}
Classe DummyData depois da refatoração
Finalizamos assim a refatoração deste pequeno programa. Como podem ver, é um programa bem simples e que já foi construído seguindo diversos princípios de Código Limpo, mas conseguimos melhorá-lo um pouco mais.
A refatoração é um processo de melhoria contínua do código, onde sempre precisamos se colocar no lugar de uma pessoa que não conhece nosso programa e o que ela pensará quando o ler (podemos medir as métricas de WTF/linhas, como descrito por Robert C. Martin, em seu livro Clean Code).
Por conta disso, devemos escrever um código que seja legível para qualquer programador, para que ele seja capaz de identificar rapidamente o fluxo de dados, os objetivos de cada classe, variável ou método, além de identificar a finalidade do programa em si, através apenas da análise do código.
Métrica WTF por linhas de código
Antes de finalizar este artigo, gostaria de fazer algumas ressalvas. O primeiro ponto é que devemos sempre buscar uma padronização do nosso código. Como vocês puderam observar em algumas classes foi utilizado a língua portuguesa para definir os nomes de métodos e atributos, enquanto em outra foi utilizada o idioma inglês. Essa falta de padronização de uma língua pode gerar uma confusão no entendimento por outros programadores, e aumenta a complexidade do código. Devemos manter o nosso código em apenas um idioma, sempre que possível, acordado entre a empresa e/ou o time de desenvolvedores.
O segundo ponto diz respeito aos testes. Testes é um dos principais pilares que nos ajuda a saber se o código ainda continua funcionando como deveria, durante uma refatoração. Isso porque testes bem feito e com uma boa cobertura, indicam que aquele código está funcionando como deveria. Quando começamos a fazer a refatoração alguns destes testes naturalmente vão quebrar, e é dever do desenvolvedor que está refatorando o programa corrigir os testes quebrados logo após refatorar o método em questão, para se assegurar que o programa continua funcionando como deveria.
Neste artigo, resolvi não desenvolver nenhum teste, pois meu objetivo foi demonstrar alguns princípios básicos e práticos de Código Limpo e o artigo ficaria grande demais. Mas eu posso fazer um novo artigo com esse programa, focando apenas nos testes unitários e de integração, caso vocês desejem.
Para quem quiser aprender um pouco mais sobre testes, recomendo estudar sobre testes unitários, testes de integração, testes automatizados, BDD e TDD. Algum tempo atrás, escrevi um artigo bem básico sobre o tema, segue o link para quem quiser conferir https://guilherme-manzano.medium.com/devops-e-testes-com-spring-unit%C3%A1rio-cobertura-api-tdd-sonar-jenkins-etc-61d080989622
Pirâmide de teste. Fonte: https://cucumber.io/blog/bdd/eviscerating-the-test-automation-pyramid/
O terceiro ponto, é que muitos programadores acham que o ato de refatorar é apenas "enxugar o código", diminuindo a quantidade de linhas de código e removendo alguns métodos e classes. Porém, o intuito dessa prática é padronizar o código e deixa-lo mais legível para outros desenvolvedores e, na maioria das vezes, isso implica em um aumento na quantidade de linhas do código, pois nomes mais significativos são mais extensos e quebramos métodos grandes em vários métodos menores.
Fonte: https://medium.com/@techindustan/these-are-the-best-7-tips-to-write-clean-code-in-2018-f01ca15dae08
O último ponto, é que a refatoração é um dever de todos os desenvolvedores da equipe, pois além de manter o código mais legível, ele diminuiu o nível de dificuldade em fazer a manutenção do código (reduz a manutenibilidade, que representa o quão difícil é alterar o código sem quebrá-lo ou gerar novos bugs). Afinal, se pegarmos um código sujo com alguns milhares de linhas, o custo e o tempo levado para fazer qualquer alteração, por mais simples que seja, pode ser muito alto. E isso pode levar a um grave problema para a empresa, de não conseguir mais dar a manutenção adequada no código sem causar um estrago maior ainda, impedindo-a de corrigir os bugs ou adicionar novas funcionalidades. Além disso, isso gera um medo nos programadores, pois ninguém vai querer mexer naquele programa e ser o responsável por quebrar a aplicação ou gerar novos bugs.
Clean Code x Dirty Code. Fonte: https://blog.finxter.com/tips-to-write-clean-code/
E a qualidade é um dos pilares igualmente importantes, pois ele garante que o código vai continuar funcionando de maneira correta, com pouco ou nenhum bug/vulnerabilidade, que pode afetar o sistema e a própria marca da empresa ante a seus consumidores e clientes.
Para finalizar, quero ressaltar que comecei a estudar práticas de Clean Code recentemente. Por favor, caso encontrem algum erro ou violação de algum princípio de boas práticas ficam à vontade para me informar. Também se sintam livres para dar fork no projeto e melhorá-lo ainda mais este código (https://github.com/GuilhermeManzano/spring-boot-jpa-codeblog/blob/master/src/main/java/com/spring/codeblog/service/serviceImp/CodeblogServiceI).
Inclusive, no projeto do GitHub, vocês podem ver o código antes e depois da refatoração, seguindo os últimos commits. Para dúvidas e sugestões, podem entrar em contato comigo através dos comentários ou pelo meu LinkedIn (https://www.linkedin.com/in/guilhermemanzano).
Top comments (0)