Uncle Bob blogou no Clean Coder sobre uns code reviews que ele fez do código (e das refatorações) de um cara chamado John MacIntyre.
Uncle Bob criticou a conclusão do cara de que refatoração não vale a pena para projetos reais: refatoração é uma maneira efetiva de evitar que o código apodreça e fique difícil de manter.
O que achei de mais interessante sobre o post do Uncle Bob é que ele pegou o código inicial do cara e foi fazendo pequenas refatorações, no estilo Baby Steps. No final, acabou melhorando drasticamente a estrutura do código e tornando-o muito mais extensível. O exemplo é bem pequeno mas, mesmo assim, houve uma melhora significativa.
O código inicial:
package payroll; public class PayCalculator { public static float calculate(double hours, double rate, boolean isHourlyWorker) { if (hours < 0 || hours > 80) { throw new RuntimeException("Hours out of range: " + hours); } float wages = 0; if (hours > 40) { float overTimeHours = hours - 40; if (!isHourlyWorker) { wages += (overTimeHours * 1.5) * rate; } else { wages += overTimeHours * rate; } hours -= overTimeHours; } wages += hours * rate; return wages; } }
O código depois da refatoração:
package payroll; public class ContractorCalculator extends PayCalculator { @Override protected double determinePay(double hours, double rate) { return hours * rate; } }
public class HourlyCalculator extends PayCalculator { @Override protected double determinePay(double hours, double rate) { double overtime = Math.max(0, hours - 40); return hours * rate + overtime * rate * 0.5; } }
package payroll; public abstract class PayCalculator { public final double calculate(double hours, double rate) { validateHours(hours); return determinePay(hours, rate); } protected void validateHours(double hours) { if (hours < 0 || hours > 80) { throw new RuntimeException("Hours out of range: " + hours); } } protected abstract double determinePay(double hours, double rate); }
Para refatorar o código original, imagino que Uncle Bob tomou os seguinte passos (de bebê):
- Criou uma suíte de testes para o código inicial.
- Simplificou um pouco o algoritmo e rodou os testes, para ver se não tinha quebrado nada.
- Livrou-se do boolean isHourlyWorker do método calculate, colocando-o como variável de instância da classe PayCalculator e passando o booleano no construtor. Isso fez com que os testes não compilassem.
- Modificou a instanciação de PayCalculator nos testes, para que compilassem. Isso ficou meio estranho, o que expôs que haviam duas classes derivadas da classe PayCalculator (uma com o parâmetro do construtor true e outra false).
- Criou as subclasses ContractCalculator, que passava false como parâmetro para o construtor da sua superclasse e HourlyCalculator, que passava true.
- Modificou a instanciação dos testes para usar as subclasses.
- Moveu a implementação do método calculate, ainda usando isHourlyWorker, para as subclasses.
- Tornou abstrato o método calculate da superclasse PayCalculator.
- Modificou a implementação do método calculate nas subclasses, só deixando o código correspondente as responsabilidade de cada subclasse.
- Removeu o booleano isHourlyWorker do construtor da classe PayCalculator e removeu a chamada das subclasses a esse construtor.
A cada passo, os testes são rodados. Parece muito complicado para um código tão pequeno, mas é muito flúido programar desse jeito. Lembra o trabalho de um sushi man que, a cada movimento, deixa sua faca e outros utensílios limpos e organizados.
O código ficou muito mais extensível: para criar um outro tipo de PayCalculator, eu não preciso modificar o código da classe PayCalculator. Através de polimorfismo e classes abstratas, criamos um ponto de extensão para o que tinha mais sentido de negócio. A esse modificar por extensão deram o nome de Open/Closed Principle. Uma métafora interessante desse princípio, que é mostrada na figura abaixo, é que para colocar um casaco você não precisa fazer uma cirurgia para abrir o peito.
Uma observação: métodos que recebem booleanos são mais difíceis de ler e de usar, porque em geral o programador tem que olhar o código do método para realmente saber o que o booleano faz. E usar booleanos para desviar o fluxo do código mostra que o método está fazendo duas coisas, mais do que deveria. Além disso, se você quiser criar uma terceira coisa, você tem que trocar o booleano por um inteiro, Enum, ou coisa parecida e modificar o código do método.
Conforme foi dito em um comentário do post do Clean Coder, o código de Uncle Bob teve uma duplicação talvez desnecessária: a chamada do método validateHours() em ambas as subclasses, tanto na ContractCalculator quanto na HourlyCalculator. Quem fez esse comentário colocou uma sugestão de código no Gist, que foi o que reproduzi acima.
No livro Refactoring, Martin Fowler faz algo parecido, pegando um código pseudo-OO, com classes anêmicas e toda a lógica em um só método, e fez pequenas refatorações em , até tornar o código muito mais simples e modificável. Coloquei o código do exemplo no meu github.
Mas, afinal de contas, o que que tem a ver o título deste post?
É que acredito começar de um código estruturado, modificando-o em pequenos passos, até chegar a um código extensível e com as responsabilidades bem definidas, é uma boa maneira de ver a verdadeira utilidade de OO. E de aprender!