Motivação
A refatoração é um processo de melhoria contínua do código cujo objetivo é otimizar a sua estrutura interna, porém, sem modificar o seu comportamento externo, ou seja, sem afetar a forma como o software funciona. Aplicar essa técnica tem como objetivo obter um software de melhor qualidade, com um design mais aprimorado e um código mais bem escrito.
Existem, atualmente, sete grupos de refatorações: Composing Methods, Moving Features Between Objects, Organizing Data, Simplifying Conditional Expressions, Making Method Calls Simpler, Dealing with Generalization e mais um grupo que trata de refatorações maiores e mais complexas, chamado Big Refactorings.
O grupo Moving Features Between Objects abrange as refatorações Move Method (Mover Método), Move Field (Mover Campo), Extract Class (Extrair Classe), Inline Class (Alinhar Classe), Hide Delegate (Ocultar Delegação) e Remove Middle Man (Remover Homem do Meio). Essas refatorações ajudam nas decisões mais fundamentais no projeto de um código orientado a objetos, que é decidir quais são as responsabilidades de cada componente. Até mesmo os desenvolvedores mais experientes costumam não acertar de primeira se uma responsabilidade adicionada a um objeto está realmente no lugar correto.
Nesse artigo nos concentraremos nas refatorações Move Method e Move Field, analisando como identificar a oportunidade de uso de cada uma e como proceder para realizá-las na linguagem Ruby.
Refatorando com Move Method
O Move Method é aplicado quando um método está utilizando mais funcionalidades de outra classe ou sendo utilizado por ela mais do que pela própria classe na qual ele está definido.
Para corrigir essa situação, primeiro, cria-se um novo método com um corpo similar na classe que ele está sendo mais utilizado. Em seguida, deve-se transformar o método antigo em uma delegação simples, ou removê-lo por completo.
Na Listagem 1 temos um exemplo de método que pode ser refatorado com Move Method.
01 class Conta
02
03 def taxa_cheque_especial
04 if @tipocontacorrente.premium?
05 resultado = 10
06 resultado += (dias_cheque_especial - 14) * 0.65 if @dias_cheque_especial > 14
07 resultado
08 else
09 @dias_cheque_especial * 1.95
10 end
11 end
12
13 def taxa_bancaria
14 resultado = 4.5
15 resultado += taxa_cheque_especial if @dias_cheque_especial > 0
16 resultado
17 end
18 end
Nessa listagem podemos verificar que a classe Conta possui muitos comportamentos, isto é, está fazendo muitas coisas ao mesmo tempo, como verificar o tipo da conta corrente (linhas 3 a 11) e calcular a taxa bancária (linhas 13 a 16). Logo, essa classe está apresentando baixa coesão, e se surgirem novos tipos de conta, teremos uma classe ainda maior e cada vez mais complexa, realizando novas comparações e calculando diferentes taxas. Assim sendo, essa classe precisa ser refatorada.
Ao aplicar a técnica Move Method, podemos obter como resultado o código da Listagem 2.
01 class Conta
02
03 def taxa_bancaria_cc
04 resultado = 4.5
05 if @dias_cheque_especial > 0
06 #chamada redirecionada ao TipoContaCorrente
07 resultado += @tipocontacorrente.taxa_cheque_especial(@dias_cheque_especial)
08 end
09 resultado
10 end
11 end
12
13 class TipoContaCorrente
14 def taxa_cheque_especial(dias_cheque_especial)
15 if premium?
16 resultado = 10
17 resultado += ( dias_cheque_especial - 14) * 0.65 if dias_cheque_especial > 14
18 resultado
19 else
20 dias_cheque_especial * 1.95
21 end
22 end
23 end
Nesse novo código temos a criação de uma nova classe, conforme pode-se notar nas linhas 13 a 21, onde foi implementada a classe TipoContaCorrente para tratar da taxa específica para o tipo conta corrente. Na classe Conta, agora temos apenas uma delegação para o método que foi movido para a nova classe (linha 7). Isso torna a coesão das classes muito mais forte, possibilitando que cada uma cuide das suas próprias atribuições.
Para melhor compreender essa refatoração, podemos dividir o processo em alguns passos:
- Primeiramente, precisamos verificar todos os recursos utilizados pelo método a ser movido e que estão definidos na sua classe, pois esses recursos também devem ser considerados a serem movidos para a classe de destino. Se um recurso é usado somente pelo método a ser movido, esse também pode ser movido junto. Se o recurso é utilizado por outros métodos, esses outros métodos também devem ser avaliados. Essas análises são muito importantes, pois muitas vezes é mais fácil mover diversos métodos do que mover um de cada vez;
- O segundo passo consiste em verificar as subclasses e superclasses da classe de origem, procurando por outras definições do método. Caso existam outras definições, pode não ser possível mover esse método, a não ser que o polimorfismo possa ser utilizado no destino;
- No terceiro passo define-se o método na classe de destino. Essa nova definição do método pode utilizar um nome diferente do original contido na classe de origem;
- O quarto passo ocorre quando se copia o código do método de origem para o de destino, realizando os devidos ajustes. Nesse passo, deve-se verificar se o método utilizará a classe de origem, se existe alguma exceção a ser gerenciada e quem deve tratá-la, por isso alguns ajustes podem ser necessários;
- O quinto passo determina como referenciar o objeto de destino na origem. Nesse passo é decidido se o acesso ao destino será realizado através de um atributo ou de um método. De preferência, cria-se um método temporário na origem que basicamente retorna o resultado da chamada ao novo método do objeto de destino (sexto passo). Assim, o código de origem referencia esse método para obter seu resultado (que antes era produzido na própria classe de origem). No entanto, isso é apenas utilizado temporariamente, apenas para facilitar a refatoração. Ao final do processo deve-se manipular diretamente o método do destino sem nenhum intermediário no código de origem;
- O sexto passo consiste em tornar o método de origem em um método de delegação. Novamente, ressalta-se que esse processo é temporário, até que a refatoração esteja concluída;
- No sétimo passo deve-se verificar se tudo continua funcional como o código anterior, o código antes da refatoração;
- O oitavo passo consiste em analisar e decidir se o método de origem deve ser removido ou se deve ser mantido como um método de delegação. Uma dica nesse momento é mantê-lo como um método de delegação se existirem muitas referências a ele. Caso seja decidido pela sua remoção na classe de origem, deve-se realizar um passo adicional, que consiste em substituir todas as referências ao método de origem para o novo método criado no destino. O ideal é sempre realizar um teste para cada uma dessas modificações;
- Ao fim, testa-se tudo para verificar se o código continua funcional como anteriormente, garantindo que a refatoração não alterou seu comportamento.
Essa refatoração é utilizada quando as classes possuem muitos comportamentos ou as classes estão colaborando demais e assim se tornam fortemente acopladas. O Move Method permite que as classes se tornem mais simples, levando a uma implementação mais clara e objetiva do conjunto de responsabilidades de cada uma.
Uma forma de encontrar oportunidades para essa refatoração é sempre observar os métodos da classe, tentando encontrar um que referencie um objeto externo, observando atentamente se ele está referenciando esse objeto mais do que o próprio em que ele está inserido.
Depois que o método provável para ser movido foi encontrado, verifica-se os métodos que o chamam, os métodos que ele chama e redefinições do método na hierarquia de classes. Essa análise é bastante importante para que o desenvolvedor tenha uma boa base para decidir se vale o esforço de mover o método ou se é melhor deixá-lo no objeto atual.
Refatorando com Move Field
O Move Field deve ser aplicado quando um atributo está sendo mais utilizado por outra classe do que por aquela na qual ele está definido. Nesses casos, cria-se um novo atributo de leitura - e, se necessário, um de escrita - na classe de destino, e altera-se todos os pontos em que ele é referenciado.
Na Listagem 3 podemos ver um exemplo de um método que pode ser refatorado com Move Field.
01 class Conta
02 attr_accessor : taxa_de_juros
03
04 def juros_por_quantidade_dias(quantidade, dias)
05 @taxa_de_juros * quantidade * dias / 365;
06 end
07 end
Nessa classe, tem-se apenas um método que está utilizando a variável taxa_de_juros, no entanto, existe outra classe (TipoContaCorrente) que utiliza muito mais essa variável. Assim, faz-se necessário mover essa variável para essa outra classe, como podemos verificar na Listagem 4.
01 class TipoContaCorrente
02 #Criado o atributo no tipo conta corrente
03 attr_accessor : taxa_de_juros
04 end
05
06 class Conta
07 def juros_por_quantidade_dias(quantidade, dias)
08 @tipocontacorrente.taxa_de_juros * quantidade * dias / 365;
09 end
10 end
Agora, após a refatoração com Move Field, tem-se que a variável foi movida para a classe TipoContaCorrente (linha 3), e na classe Conta criou-se uma chamada para utilizar a variável conforme mostra a linha 8.
De forma geral, a mecânica dessa refatoração pode ser dividida nas seguintes etapas:
- Primeiramente, devemos criar, na classe de destino, um atributo de leitura, e, se necessário, um de escrita também;
- O segundo passo consiste em determinar como referenciar o objeto de destino a partir do objeto da origem. Um atributo ou método que já existam podem dar acesso ao objeto de destino, caso contrário é preciso verificar se é possível criar facilmente um método que faça esse acesso. Outra possibilidade é criar um atributo na origem que possa armazenar o objeto de destino - esse atributo pode ser permanente ou temporário, até que ele não seja mais necessário;
- No terceiro passo, substitui-se todas as referências ao atributo da origem por referências ao método apropriado no destino. Para realizar os acessos à variável, substitui-se as referências pela chamada ao atributo de leitura ao objeto no destino, e no caso de atribuições, substitui-se por uma chamada ao de escrita. Também é preciso investigar todas as subclasses da origem por referências ao campo;
- Por fim, testa-se para verificar se tudo continua funcionando normalmente.
O principal fundamento dessas refatorações é mover responsabilidades entre classes, garantindo que nenhuma possua mais atribuições que o necessário.
Lembre-se que uma decisão de projeto definida no dia de hoje pode se tornar incorreta no dia de amanhã e, a longo prazo, pode comprometer a estrutura e qualidade do código. Por isso, quando nos deparamos com esse tipo de cenário, é importante resolvê-lo o mais rapidamente possível, para evitar mais complicações no futuro. Dessa forma, quando verificado que mais métodos de uma classe utilizam a informação do campo de outra, inclusive mais do que a própria classe em que o campo está definido, essa pode ser uma boa oportunidade para aplicar a refatoração Move Field.