Refatoração: motivações e práticas

Fotografia de dois conjuntos de elásticos: no primeiro, elásticos bagunçados e emaranhados e, no segundo, elásticos segurando e apoiando uns aos outros enquanto formam as camadas de uma esfera sólida. (imagem extraída de altexsoft.com)

Refatoração é o nome dado ao processo de alterar o código de um software com o objetivo de melhorar a sua estrutura interna sem alterar seu comportamento externo. Ao trazer a prática da refatoração para o dia-a-dia de um projeto, é possível manter a base de código sempre legível e enxuta, reduzindo bugs desconhecidos e acelerando o desenvolvimento de novos comportamentos e funcionalidades.

Por mais bem pensada que seja uma arquitetura, com o passar do tempo e alterações no código, o desenho original tende a ir se perdendo gradativamente em meio ao emaranhado de código novo.

A prática da refatoração visa combater essa deterioração natural dos projetos de software. Através da combinação de passos bem simples, como "mover um atributo de uma classe para outra" ou "extrair código de um método longo para criar um método privado", podemos manter um código saudável enquanto evoluímos a arquitetura para refletir o estado atual do projeto.

Imagem ilustrando um ciclo de três etapas no desenvolvimento de software: partindo de um estado onde os testes falhando, escrevemos código para que os testes passem e, por fim, revisamos esse código através da refatoração.

Antes (e depois) de pensar em qualquer refatoração é preciso pensar em testes. Quando o projeto apresenta uma boa cobertura de testes, com contratos bem definidos, podemos refatorar com tranquilidade sabendo que o código novo vai apresentar as mesmo comportamento do código antigo, com a diferença de ser um código bem mais fácil de entender e evoluir.

Por que refatorar?

  • Para manter e refinar a arquitetura de um projeto de software;
  • Para tornar o código mais legível;
  • Para ajudar a identificar e eliminar bugs em códigos complexos;
  • Para garantir a qualidade e adaptabilidade do código com o objetivo de acelerar a inclusão de novas funcionalidades ou comportamentos.
Quadrinho com uma analogia ao dia-a-dia de quem desenvolve código, partindo de um "campo verde" até uma construção complexa, cheia de níveis e adaptações. (ilustração adaptada de bonkersworld.net)

Quando refatorar?

  • Quando estiver adicionando novas funcionalidades;
  • Quando estiver investigando ou consertando algum bug;
  • Quando estiver documentando o código ou a arquitetura;
  • Quando estiver revisando código de seus colegas de time ou de projetos open-source.

Pensando de forma imediatista, refatoração não entrega valor perceptível para pessoas mais distantes do código. Por isso, pode ser que seja difícil justificar a necessidade de investir tempo e esforço nesse tipo de atividade para clientes, chefes ou pessoas não-técnicas envolvidas no projeto. Mas é importante que todas as pessoas do time entendam que manter um código limpo, testado e flexível é essencial para alcançar agilidade — tanto pra entregar novas funcionalidades quanto pra responder a mudanças.

Com o tempo e a experiência, a refatoração começa a fazer parte da rotina de quem desenvolve código, podendo ser aplicada antes, durante ou depois de escrever código novo. Dito isso, praticar refatorações menores no dia-a-dia não elimina a necessidade de, de tempos em tempos, parar para reavaliar o desenho do código numa perspectiva mais macro.

Onde refatorar?

Listar e descrever todas as situações onde um código pode ser refatorado é uma tarefa interminável. No entanto, podemos manter nosso faro atento para detectar alguns conhecidos “bad smells” (cheiros ruins) no código.

Atualmente, muitas plataformas de desenvolvimento oferecem ferramentas que auxiliam na refatoração, sinalizando problemas usuais como "código duplicado" ou "variável não-utilizada" e oferecendo correções instantâneas. Entretanto, problemas arquiteturais acabam sendo percebidos somente por quem está colocando a mão-na-massa todos os dias. Escrever código novo deveria ser uma atividade criativa, divertida e estimulante e, se isso está sendo incômodo, massante ou repetitivo, refatorar pode melhorar.

Nesse texto, você vai ler termos comuns as linguagens orientadas à objeto, mas as principais ideias podem ser transpostas para qualquer paradigma.

Código duplicado

A mesma estrutura de código aparece em mais de um lugar no programa.

Método longo

Um método muito cumprido deve estar fazendo mais do que deveria.

Classe longa

Uma classe com muitas responsabilidades pode gerar confusão sobre seu propósito e também ser instanciada desnecessariamente.

Parâmetros em excesso

Métodos com muitos parâmetros indicam grande dependência entre classes.

Classe dependente

Uma classe (ou um conjunto delas) que precisam sempre ser alterados para acompanhar mudanças no código.

Código dependente

Diversos trechos do código que precisam sempre ser alterados para acompanhar mudanças no código.

Método invejoso

Método que parece mais interessado em outras classes do que em sua própria.

Atributos agrupados

Atributos vistos juntos com frequência deveriam se tornar uma classe.

Tipos primitivos

Tipos primitivos que apresentam comportamentos específicos deveriam ser encapsulados em classes.

Expressões condicionais

Blocos com condicionais podem ser extraídos para métodos explicativos.

Herança paralela

Classes diferentes que precisam ser herdadas juntas por subclasses.

Classe preguiçosa

Classes sem propósito ou com poucas responsabilidades.

Generalização especulativa

Classes e métodos genéricos criados antes da necessidade específica.

Atributos temporários

Classes com atributos que raramente são utilizados.

Métodos em cadeia

Métodos que apenas chamam outros métodos podem indicar dependência.

Classe intermediária

Classe que existe apenas para delegar chamadas para outras classes.

Intimidade inapropriada

Classes que fazem muito uso de código privado de outras classes.

Classes alternativas com interfaces diferentes

Classes que apresentam implementações alternativas dos mesmos métodos deveriam ser extensões da mesma interface.

Bibliotecas incompletas

Comportamentos complementares a uma biblioteca de terceiros poderiam ser agrupados em uma classe utilitária que extende a biblioteca original.

Classe de dados

Classe que contém apenas atributos e seus respectivos getters e setters poderia ser incorporada a outra classe até a necessidade da individuação aparecer.

Herança desnecessária

Classe que herdam código que não precisam de suas superclasses.

Comentários

Códigos exaustivamente explicados podem estar precisando de refatoração.

Como refatorar?

Como cada software é único, marcado por regras de negócio e decisões de arquitetura e tecnologia específicas, não existe uma receita de bolo com o passo-a-passo necessário para refatorar qualquer projeto até o ponto ideal.

Dito disso, projetos de software absurdamente distintos acabam passando por situações de deterioração de código bastante semelhantes e, por isso, contam com estratégias de refatoração bem conhecidas.

Como você vai poder notar, algumas estratégias de refatoração resultam em situações que são o ponto de partida para outras estratégias (e vice-versa). Isso quer dizer que não existe um ponto final para o processo de refatoração e somente análises subjetivas que consideram o contexto atual do projeto e time podem determinar quando um código está suficientemente refatorado.

Compor métodos

Muitos problemas encontrados em projetos tem origem em métodos longos. Conforme vão agrupando mais e mais instruções, os métodos vão se tornando menos legíveis. Para reduzir a ocorrência desse tipo de problema, podemos empacotar blocos de código de acordo com seus respectivos propósitos.

Extrair método
Situação:
Um trecho de código que apresenta uma funcionalidade específica.
Solução: Transformar esse fragmento num método com nome explicativo.

void printOwing() {
printBanner();
//print details
System.out.println ("name: " + _name);
System.out.println ("amount: " + getOutstanding());
}
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails (double outstanding) {
System.out.println ("name: " + _name);
System.out.println ("amount: " + outstanding);
}

Fundir método
Situação:
O corpo do método é tão simples e claro quanto o seu nome.
Solução: Substituir a chamada ao método por seu corpo e remover o método.

int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}

Fundir variável temporária
Situação:
Variável temporária é referenciada apenas uma vez no código.
Solução: Substituir referência a variável temporária pelo método que a retorna.

double basePrice = anOrder.basePrice();
return (basePrice > 1000)
return (anOrder.basePrice() > 1000);

Substituir variável temporária por método
Situação:
Variável temporária que armazena o resultado de uma expressão.
Solução: Extrair a expressão armazenada na variável para um novo método.

double basePrice = _quantity * _itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
if (basePrice() > 1000) {
return basePrice() * 0.95;
} else {
return basePrice() * 0.98;
}
double basePrice() {
return _quantity * _itemPrice;
}

Introduzir variável explicativa
Situação:
Expressão condicional muito complexa.
Solução: Quebrar partes da expressão em variáveis com nomes explicativos.

if (platform.toUpperCase().indexOf("MAC") > -1 &&
browser.toUpperCase().indexOf("IE") > -1 &&
wasInitialized() &&
resize > 0 ) {

// fazer algo
}
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIE && wasInitialized() && wasResized) {
// fazer algo
}

Separar variável temporária
Situação:
Variável temporária é alterada no decorrer do código.
Solução: Criar uma variável temporária única para armazenar cada resultado.

double temp = 2 * (height + width);
System.out.println(temp);
temp = height * width;
System.out.println(temp);
final double perimeter = 2 * (height + width);
System.out.println(perimeter);
final double area = height * width;
System.out.println(area);

Remover atribuições de valores aos parâmetros
Situação:
Método altera o valor de seus argumentos.
Solução: Criar uma variável temporária para receber o valor do parâmetro e fazer manipulações sem alterar o valor original.

int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) {
inputVal -= 2;
}
return inputVal;
}
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (result > 50) {
result -= 2;
}
return result;
}

Substituir método longo por classe dedicada
Situação:
Classe contém método longo ou complexo com propósito definido.
Solução: Mover o método longo para uma nova classe, onde a complexidade pode ser decomposta em métodos menores e atributos com escopo definido.

class Order...double originalPrice;double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// operações complexas para calcular o preço
return finalPrice;
}

Substituir algoritmo
Situação:
Algoritmo demasiadamente confuso, legado ou incompleto.
Solução: Escrever testes para o algoritmo existente e substituir por um novo.

String foundPerson(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")) {
return "Don";
}
}
if (people[i].equals ("John"))
return "John";
}
return "";
}
String foundPerson(String[] people) {
List candidates = Arrays.asList(new String[] {"Don","John"});
for (int i = 0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}

Mover responsabilidades

Em qualquer arquitetura, um dos fatores mais importantes para mensurar a qualidade do código é a distribuição de responsabilidades. Por mais bem planejada que seja uma arquitetura, a tendência é que o código deteriore com o passar do tempo e das mudanças de requisitos. A prática da refatoração contínua é essencial para manter uma arquitetura polida.

Mover método
Situação:
Método usa mais código de outra classe do que de sua própria.
Solução: Mover o corpo do método para a classe onde estão os atributos e métodos mais acessados. Assim, o método antigo pode ser removido ou passar a fazer apenas uma chamada simples para o novo método.

Mover atributo
Situação:
Atributo usado mais por outras classes do que pela classe que o define.
Solução: Mover o atributo para a classe que mais faz uso dele.

Extrair classe
Situação:
Classe realiza operações que poderiam ser feitas por outras classes.
Solução: Criar uma nova classe e mover atributos e métodos relevantes pra lá.

Fundir classe
Situação:
Classe não apresenta comportamentos significativos.
Solução: Mover o conteúdo dessa classe para uma classe existente.

Esconder classe delegada
Situação:
Classe cliente precisa navegar por classes relacionadas para conseguir ter acesso a operações disponíveis em apenas uma delas.
Solução: Criar método que delega a chamada relacionada na classe delegada.

Remover classe intermediária
Situação
: Classe intermediária realiza apenas delegações simples demais.
Solução: Alterar a classe cliente para chamar a classe delegada diretamente.

Introduzir método estrangeiro
Situação
: Classe de biblioteca importada precisa de um comportamento novo.
Solução: Criar um método auxiliar na classe que usa a biblioteca importada.

Date newStart = 
new Date(prev.getYear(), prev.getMonth(), prev.getDate() + 1);
Date newStart = nextDay(prev);private static Date nextDay(Date d) {
return new Date (d.getYear(), d.getMonth(), d.getDate() + 1);
}

Introduzir extensão local
Situação
: Classe de biblioteca importada precisa de n comportamentos novos.
Solução: Criar uma subclasse que estenda a classe importada adicionando os novos comportamentos e fazer uso dessa extensão no código.

Organizar dados

Um processo de refatoração integrado ao desenvolvimento é essencial para manter simples a maneira como manipulamos nossas estruturas de dados.

Auto-encapsular atributo
Situação
: Atributos privados são acessados diretamente pelos métodos da classe.
Solução: Usar sempre os métodos getters para acessar atributos estabelece um ponto de acesso único e pode evitar comportamentos inesperados.

private int low, high;
boolean includes (int arg) {
return arg >= low && arg <= high;
}
private int low, high;boolean includes (int arg) {
return arg >= getLow() && arg <= getHigh();
}
int getLow() {
// alguma lógica poderia acontecer aqui
return low;
}
int getHigh() {
return high;
}

Substituir atributo por classe
Situação
: Atributo de tipo primitivo necessita de comportamentos adicionais.
Solução: Transformar o atributo em uma classe com os novos comportamentos.

Trocar valor por referência
Situação
: Classe com múltiplas instâncias de objetos com o mesmo valor.
Solução: Classe pode fazer referência ao mesmo objeto em todos os usos.

Trocar referência por valor
Situação
: Classe faz referência a objeto demasiadamente simples.
Solução: Classe pode usar valor primitivo pra instanciar o objeto diretamente.

Substituir vetor por classe semântica
Situação
: Vetor onde cada elemento armazena um conceito diferente.
Solução: Substituir vetor por classe com campos mais descritivos.

String[] row = new String[3];
row [0] = "Beyoncé";
row [1] = "23";
Artist row = new Artist();
row.setName("Beyoncé");
row.setGrammyAwards("23");

Duplicar dados observados
Situação
: Dados de domínio em classe com outras responsabilidades (ex: UI)
Solução: Separar dados em classes com responsabilidades bem definidas e garantir a sincronização através da implementação do padrão Observer.

Trocar associação unidirecional por associação bidirecional
Situação
: Classes interdependentes, mas a relação só é explícita de um lado.
Solução: Permitir que dados sejam acessados e manipulados dos dois lados.

Trocar associação bidirecional por associação unidirecional
Situação
: Classes relacionadas desnecessariamente acessíveis de ambos os lados.
Solução: Remover associações que não estejam sendo utilizadas.

Encapsular atributo
Situação
: Atributo público.
Solução: Tornar o atributo privado e controlar acesso usando getters e setters.

public String name;private String name;public String getName() {
return name;
}
public void setName(String arg) {
name = arg;
}

Encapsular coleção
Situação
: Método retorna coleção que pode ser alterada de qualquer maneira.
Solução: Criar métodos que facilitem leitura imutável e alterações seguras.

Encapsular estrutura de dados legados em classe
Situação
: Aplicação precisa trabalhar com dados legados.
Solução: Criar uma classe para acessar estes dados de forma transparente.

Ilustração onde alguém entrega uma caixa com conteúdo aparentemente frágil para uma outra pessoa que a recebe e agradece, sem ter muita certeza disso.

Substituir números por constantes semânticas
Situação
: O código apresenta um número com significado particular.
Solução: Substituir o número por uma constante com nome explicativo.

double potentialEnergy(double mass, double height) {
return mass * height * 9.81;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
double potentialEnergy(double mass, double height) {
return mass * height * GRAVITATIONAL_CONSTANT;
}

Substituir enumerações por enums semânticos
Situação
: Classe com dados expressos através de enumeração.
Solução: Substituir dados enumerados por classe enum com opções explicativas.

Substituir enumeração por polimorfismo
Situação
: Classe com comportamentos definidos por enumeração.
Solução: Substituir enumeração por subclasses usar fazer uso do polimorfismo para habilitar os diferentes comportamentos.

Substituir enumeração por Stage/Strategy
Situação
: Classe não extensível com comportamentos definidos por enumeração.
Solução: Substituir enumeração pelo padrão State/Strategy, associando classe a uma nova estrutura de classes que define opções e comportamentos relacionados.

Substituir subclasses por atributo
Situação
: Classe com subclasses que variam apenas em métodos constantes.
Solução: Eliminar subclasses e expressar informação em um novo atributo.

Simplificar expressões condicionais

Expressões condicionais tem muito potencial para reunir trechos de código desnecessariamente confusos. O conceito chave das técnicas de refatoração é quebrar expressões complexas em expressões menores e mais explícitas.

Decompor expressão condicional
Situação
: Expressão condicional com regras complexas.
Solução: Extrair regras para métodos com nomes explicativos.

if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
charge = quantity * summerRate;
} else {
charge = quantity * winterRate + winterServiceCharge;
}
if (isSummer(date)) {
charge = calculateSummerCharge(quantity);
} else {
charge = calculateWinterCharge(quantity);
}

Consolidar expressão condicional
Situação
: Diferentes expressões condicionais retornando o mesmo resultado.
Solução: Fundir expressões condicionais em uma única expressão.

double calculateDiscount() {
if (stock < 100) {
return 50.0;
}
if (months > 12) {
return 50.0;
}
if (isDiscontinued) {
return 50.0;
}
return 10.0;
}
double calculateDiscount() {
if (isEligibleToSuperDiscount()) {
return 50.0;
}
return 10.0;
}

Consolidar fragmentos de código duplicados
Situação
: Fragmento de código repetido em expressão condicional.
Solução: Mover o fragmento duplicado para fora da expressão.

if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}
if (isSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
send();

Remover variável sentinela
Situação
: Variável sentinela controla condição de parada de laço de repetição.
Solução: Interromper o laço de repetição com break ou return.

String findWonderlander(String[] people) {
String found = "";
for (int i = 0; i < people.length; i++) {
if (found.equals("")) {
if (people[i].equals(“Alice”)) {
found = “Alice”;
}
if (people[i].equals(“Rabbit”)) {
found = “Rabbit”;
}
}
}
return found;
}
String findWonderlander(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals(“Alice”)) {
return “Alice”;
}
if (people[i].equals(“Rabbit”)) {
return “Rabbit”;
}
}
return "";
}

Substituir condicionais em cascata por condicionais em linha
Situação
: Expressão condicional com fluxo de execução muito complexo.
Solução: Trocar o condicionais em cascata por condicionais em linha.

double getPayAmount() {
double result;
if (isDead) {
result = deadAmount();
} else {
if (isSeparated) {
result = separatedAmount();
} else {
if (isRetired) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
}
return result;
}
double getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount();
if (isRetired) return retiredAmount();
return normalPayAmount();
};

Substituir expressões condicionais por polimorfismo
Situação
: Expressão condicional com comportamento definido por tipo de classe.
Solução: Transformar o método com a condicional em um método abstrato na classe original e implementar os diferentes comportamentos em suas subclasses.

double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * coconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException ("Should be unreachable");
}

Introduzir subclasse nula
Situação
: Expressão verifica se objeto é nulo antes de chamar seu método.
Solução: Criar subclasse com as implementações para os casos de nulidade .

if (customer == null) {
plan = BillingPlan.basic();
} else {
plan = customer.getPlan();
}

Introduzir asserção
Situação
: Uma seção do código assume algo sobre o estado do objeto.
Solução: Tornar a suposição explícita usando uma asserção.

double getExpenseLimit() {
// método joga exceção se não houver nem limite nem projeto
return (limit != NULL_EXPENSE) ? limit : project.getLimit();
}
double getExpenseLimit() {
Assert.isTrue(limit != NULL_EXPENSE || project != null);
return (limit != NULL_EXPENSE) ? limit : project.getLimit();
}

Simplificar chamadas a métodos

Uma forma bastante efetiva de manter a complexidade do código sob controle é encapsular os diferentes comportamentos do sistema em métodos com interfaces que sejam fáceis de entender, localizar e utilizar.

Renomear método
Situação
: Nome de método é ambíguo ou ilegível.
Solução: Trocar o nome do método por um nome mais explicativo.

Adicionar parâmetro
Situação
: Método precisa acessar dados externos.
Solução: Adicionar parâmetro para informar esses dados.

Remover parâmetro
Situação
: Parâmetro de método não é usado em seu corpo.
Solução: Remover parâmetro.

Separar leitura e escrita
Situação
: Método usado para ler um valor e alterar o estado do objeto.
Solução: Separar métodos em operações mais unitárias, como leitura ou escrita.

Parametrizar método
Situação
: Métodos realizam operações muito semelhantes.
Solução: Criar método mais genérico que recebe como parâmetros os valores necessários para aplicar as variações de comportamento.

Preservar todo o objeto
Situação
: Atributos de um objeto são passados como parâmetros de um método.
Solução: Enviar o objeto todo como parâmetro do método.

int low = range.getLow();
int high = range.getHigh();
withinPlan = plan.withinRange(low, high);
withinPlan = plan.withinRange(range);

Substituir parametrização por métodos mais explícitos
Situação
: Método realiza operações distintas (e, potencialmente, desconhecidas) de acordo com o parâmetros de entrada.
Solução: Criar métodos separados com nomes que expressem o comportamento.

void setValue(String name, int value) {
if (name.equals("height")) {
height = value;
return;
}
if (name.equals("width")) {
width = value;
return;
}
Assert.shouldNeverReachHere();
}
void setHeight(int arg) {
height = arg;
}
void setWidth(int arg) {
width = arg;
}

Substituir parâmetro por chamada de método
Situação
: Método é chamado e seu valor de retorno é imediatamente informado como parâmetro de um outro método.
Solução: Remover o parâmetro e delegar a chamada para o corpo do método.

int basePrice = quantity * itemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice(basePrice, discountLevel);
int basePrice = quantity * itemPrice;
double finalPrice = discountedPrice(basePrice);

Introduzir classe parâmetro
Situação
: Dados que sempre são passados juntos como parâmetro de métodos.
Solução: Agrupar esses parâmetros em um objeto.

Remover setter
Situação
: Atributo é instanciado com um valor e nunca mais é alterado.
Solução: Remover o método setter para esse atributo.

Esconder método
Situação
: Método usado apenas internamente.
Solução: Definir método como privado.

Trocar construtor por Factory Method
Situação
: Instanciar objeto vai além de atribuições de valores triviais.
Solução: Esconder construtor original e disponibilizar Factory Method.

public Employee(int type) {
this.type = type;
}
private Employee(int type) {
this.type = type;
}
public static Employee create(int type) {
Employee employee = new Employee(type);
// diversas operações em cima do objeto employee
return employee;
}

Encapsular downcast
Situação
: Método retorna objeto abstrato que precisa ser tipificado antes do uso.
Solução: Fazer o downcast dentro do próprio método e alterar sua assinatura.

Object lastReading() {
return readings.lastElement();
}
Reading lastReading() {
return (Reading) readings.lastElement();
}

Substituir código de erro por exception
Situação
: Método retorna números para indicar sucesso ou erro.
Solução: Remover retorno do método e jogar exceção em casos de erros.

int withdraw(int amount) {
if (amount > balance) {
return -1;
} else {
balance -= amount;
return 0;
}
}
void withdraw(int amount) throws BalanceException {
if (amount > balance) {
throw new BalanceException();
}
balance -= amount;
}

Trocar exception por validação
Situação
: Expressões condicionais podem eliminar a necessidade de exceções.
Solução: Usar expressões condicionais para validar parâmetros recebidos.

double getValueForPeriod(int period) {
try {
return values[period];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
double getValueForPeriod(int period) {
if (period >= values.length) {
return 0;
}
return values[period];
}

Tornar genérico

O polimorfismo das linguagens orientadas à objeto permite refatorar arquiteturas para alcançar soluções tão genéricas quanto especializáveis.

Subir atributo
Situação
: Múltiplas subclasses possuem o mesmo atributo.
Solução: Mover atributo para a superclasse.

Subir método
Situação
: Múltiplas subclasses possuem o mesmo método.
Solução: Mover o método para a superclasse.

Subir construtor
Situação
: Subclasses com construtores iguais ou parecidos.
Solução: Chamar construtor da superclasse no construtor da subclasse.

public Employee(String name, String id) {
this.name = name;
this.id = id;
}
public Manager(String name, String id, int grade) {
this.name = name;
this.id = id;
this.grade = grade;
}
class Manager extends Employee...public Manager(String name, String id, int grade) {
super(name, id);
this.grade = grade;
}

Descer método
Situação
: Método presente na superclasse é relevante em apenas uma subclasse.
Solução: Mover método da superclasse para a subclasse que faz uso dele.

Descer atributo
Situação
: Atributo presente na superclasse é relevante em apenas uma subclasse.
Solução: Mover atributo da superclasse para a subclasse que faz uso dele.

Extrair subclasse
Situação
: Classe possui funcionalidades usada apenas em algumas instâncias.
Solução: Criar uma subclasse para esse conjunto de funcionalidades.

Extrair superclasse
Situação
: Múltiplas classes com funcionalidades similares.
Solução: Criar superclasse e mover funcionalidades comuns pra lá.

Extrair interface
Situação
: Seções da arquitetura precisam acessar apenas partes de uma classe.
Solução: Expor apenas os métodos necessários em uma nova interface.

Fundir hierarquia
Situação
: Classe e subclasse tem atributos e comportamentos semelhantes.
Solução: Fundir classe e subclasse.

Construir método padrão
Situação
: Métodos de subclasses realizam operações com passos semelhantes.
Solução: Criar método na superclasse para definir passos comuns da operação e delegar a implementação das especificidades para suas subclasses.

Substituir herança por delegação
Situação
: Subclasse precisa herdar somente um método da superclasse.
Solução: Criar atributo da superclasse na subclasse. Criar método na subclasse para delegar chamadas para a instância local da superclasse. Remover herança.

Substituir delegação por herança
Situação
: Classe com múltiplos métodos delegando chamadas pra mesma classe.
Solução: Transformar a classe que delega numa subclasse da classe acessada.

Referências

Captura da capa do livro “Refatoração”, lançado por Martin Fowler no ano de 1999. Eu jamais poderia imaginar que 6 anos depois de ter escrito esse texto, eu entraria na ThoughtWorks e me tornaria colega de trabalho de quem escreveu esse livro.

A primeira versão desse texto foi escrita em outubro de 2012, numa atividade da disciplina de Programação Orientada à Objetos II do Bacharelado em Ciência da Computação da UNIFESP (Universidade Federal de São Paulo). Em junho de 2020, foi atualizado para incluir experiências mais recentes e disponibilizado na internet.

--

--

Desenvolvedor de software por formação. Educador por vocação. fernandomachado90.github.io

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store