Backend

Refactoring, a arte de alterar o código sem estragar o que funciona

Conheça o refactoring, técnica disciplinada de reestruturação de código que muda a estrutura sem alterar o seu comportamento.

Todos nós já escrevemos aquele código ruim, difícil entender, ou até mesmo um código legível, mas com um design de classes que só faz sentido para quem escreveu. Geralmente isso ocorre por alguns motivos, como:

  • Desconhecimento técnico
  • Prazo curto

Quando falamos de desconhecimento técnico, falamos de falta de conhecimento de algumas técnicas como o Clean Code, SOLID, DRY, Design Patterns, TDD. Ignorando uma dessa técnicas, desvalorizamos o código, já que queremos escrever para que futuramente possamos ler.

Referente as técnicas, o Clean Code é praticamente uma filosofia de desenvolvimento, que tem como objetivo facilitar a escrita e leitura do código, onde se prega, entre outras coisas:

  • Nomes significativos
  • Métodos pequenos
  • Evitar comentários
  • Boa formatação 
  • Tratamento de erro
  • Respeitar limites 
  • Criar testes unitários

O refactoring está muito ligado a essas técnicas, e são elas que podemos utilizar para realizar essas melhorias!

Um outro motivo que podemos considerar que um código pode ser mal escrito é o prazo curto das entregas. Sabemos que nunca teremos o tempo necessário para fazer o código perfeito, mas é preciso sempre pensar que, se não fizermos o melhor que podemos com o tempo que temos, os problemas irão voltar em algum momento. Nesse caso, a recomendação é que faça o melhor que puder, crie testes unitários (que ajudem a documentar) e marque esse código com débito técnico – ou seja, volte e revise esse código, antes que ele seja um problema para o time.

Mas o que conceitua o refactoring, de fato?

O refactoring é uma técnica disciplinada de reestruturação de código, que consiste em mudar a estrutura sem alterar o comportamento do dele.

Cada transformação deve ser feita em pequena escala, mas o conjunto dessas transformações podem produzir uma estrutura significativa. Se a transformação for pequena, é menos provável que essa alteração dê errado.

Geralmente é motivada por alguns code smells, como por exemplo:

  • Métodos muito longos 
  • Códigos duplicados
  • Código difícil de entender
  • Códigos complexos
  • Nome não expressivos 

A consequência de usar o refactoring é a melhora do design, a redução da complexidade para melhorar a manutenção de código fonte e, a partir daí, da legibilidade de porções de código – isso, sem o refactoring, se deterioraria. Outro benefício é a melhora no entendimento do código, o que facilita a manutenção e evita a inclusão de defeitos.

Como o refactoring melhora a qualidade do software?

Refatorar código tem tudo a ver com melhorar as qualidades software. Algumas características que podem proporcionar um refactoring:

  • Manutenibilidade – a facilidade com que você pode fazer alterações em seu software. A capacidade de manutenção inclui a adição de novos recursos, ajustes de desempenho e facilidade de correção de bugs.
  • Flexibilidade – até que ponto você pode modificar seu software para outros usos. Pense na facilidade com que você pode dinamizar o software.
  • Portabilidade – a facilidade com que você pode fazer o software operar em outro ambiente. Pense no desenvolvimento local versus a execução em um servidor em produção.
  • Reuso – a facilidade com que você pode usar partes de seu software em outros sistemas.
  • Legibilidade – a facilidade com que você pode ler e entender o código-fonte. Não apenas no nível da interface, mas também nos detalhes essenciais das implementações.
  • Testabilidade – a facilidade de escrever testes de unidade, testes de integração, etc.
  • Entendimento – a facilidade entender seu software em um nível geral. Sua base de código está estruturada de uma maneira significativa?

Antes de começar uma refactoring em um código é importante, no mínimo, fazer testes unitários, que servirão para garantir que o comportamento do modulo está funcionado corretamente.
Podemos dizer que TDD (Teste Driven Development) está totalmente ligado a refactoring, já que a base e a parte mais importante de dele é, de fato, os testes. Como saberemos que o código continua funcionando, se não posso testar se a alteração que fizemos não estragou a funcionalidade? Fica muito difícil. 

Podemos dizer que o refactoring é um ciclo interativo de trasformar o código e de testá-lo, para garantir a exatidão e fazer uma ou outra pequena alteração.

Se qualquer teste falhar podemos desfazer a ultima alteração e repetir o processo de uma maneira diferente.

Algumas técnicas de Refactoring:

  • Encapsulate Field – o código de fields, principalmente de pojos, deve ser acessado por métodos getters e setters.
  • Tipos Genéricos – criar tipos genéricos para compartilhar código. 
  • Substituir códigos de validação por tipos de State e Strategy.
  • Substituir ‘IFs’ por polimorfismo/Design Pattern – quebrar código em componentes reutilizáveis com interfaces bem definidas.
  • Extrair Classes – remover excesso de métodos para classes.
  • Extrair Métodos – quebrar grandes métodos em métodos menores (como diz Uncle Bob,”Um método deve ser conter até 20 linhas”) .
  • Mover métodos e fields para classes apropriadas.
  • Dar nomes a métodos e fields que revelam suas finalidades.

As duas técnica que achamos mais interessantes são a Extract Method e Replace IF.

Exemplo de Extract Method:

Abaixo temos um método com diversos condicionais e muitas responsabilidades.

public Trip  getTripsByUser(User user) throws UserNotLoggedInException {
		Trip tripList = new Trip();

		User loggedUser = UserSession.getInstance().getLoggedUser();

		boolean isFriend = false;
		if (loggedUser != null) {
			for (User friend : user.getFriends()) {
				if (friend.getName().equals(loggedUser.getName())) {
					isFriend = true;
					break;
				}
			}
			if (isFriend) {
				tripList = TripDAO.findTripsByUser(user);
				
			}
			return tripList;
		} else {
			throw new UserNotLoggedInException();
		}
	}

Método refatorado:

Agora a mesma ideia, com alguns ajustes de lógica, e com algumas responsabilidade extraídas.

public Trip getTripsByUser(User user) throws UserNotLoggedInException {
 		User loggedUser = UserSession.getInstance().getLoggedUser();
 		if (loggedUser != null)
 			return tripList(user, loggedUser);
 		throw new UserNotLoggedInException();
 	}

 	private Trip tripList(User user, User loggedUser) {
 		for (User friend : user.getFriends()) {
 			if (friend.getName().equals(loggedUser.getName())) {
 				return TripDAO.findTripsByUser(user);
 			}
 		}
 		return new Trip();
 	}

Técnica de substituir um IF por um Design Pattern:

Podemos utilizar diversos Design Pattern para substituir IFs e dar mais clareza ao código como, por exemplo, Factory Method ou Command Pattern.

Exemplo de código:

public int calculate(int a, int b, String operator) {
 	    int result = Integer.MIN_VALUE;
 	    if ("add".equals(operator)) {
 	        result = a + b;
 	    } else if ("multiply".equals(operator)) {
 	        result = a * b;
 	    } else if ("divide".equals(operator)) {
 	        result = a / b;
 	    } else if ("subtract".equals(operator)) {
 	        result = a - b;
 	    }
 	    return result;
 	}

Exemplo de código com Factory Method :

public interface Operation {
    int apply(int a, int b);
}
public class Addition implements Operation {
    @Override
    public int apply(int a, int b) {
        return a + b;
    }
}
public class OperatorFactory {
    static Map<String, Operation> operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }

    public static Optional<Operation> getOperation(String operator) {
        return Optional.ofNullable(operationMap.get(operator));
    }
}
public int calculateUsingFactory(int a, int b, String operator) {
    Operation targetOperation = OperatorFactory
      .getOperation(operator)
      .orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
    return targetOperation.apply(a, b);
}

Neste exemplo, a responsabilidade é delegada a objetos fracamente acoplados servidos por uma Factory Class.

Conclusão

Muitos sistema se deterioram quando não investimos tempo em limpeza e organização. Todo sistema se degrada com o tempo, a medida que novas funcionalidades são inseridas, alterada,s ou erros são corrigidos. 

Para evitar que o código se torne uma casa suja é importante manter um refactoring de código constante, tornando o código entendível, claro e limpo. Assim, de forma sistemática e frequente, estamos investido para que o software se mantenha fácil de alterar e com velocidade de desenvolvimento. Cuide do seu código, cuide dos seus testes, pague seus débitos técnicos, inclua sempre na sua sprint ao menos um debito técnico e, com isso, será possível fazer uma evolução constante do seu software.

Referências:

%d blogueiros gostam disto: