Refatorando Código Legado: Parte 10: Dissecando Métodos Longos Através de Extrações
Portuguese (Português) translation by Erick Patrick (you can also view the original English article)
Na sexta parte de nossa série, falamos sobre como trabalhar com métodos longos, através do uso da programação em par e da visualização de código em diferentes níveis. Continuamente, aumentamos e diminuímos o nível de visualização, e observamos tanto coisas pequenas como a parte da nomeação, quanto a forma e a indentação do código.
Hoje, usaremos outra abordagem: Assumiremos que estamos sozinhos, sem companheiros de trabalho para nos ajudar. Usaremos uma técnica chamada "Extrair até a exaustão" que divide o código em várias pequenas partes. Esforçar-nos-emos para fazer com que essas partes sejam o mais entendíveis possível, de modo que nós do futuro, ou qualquer outro programador, sejam capazes de entendê-las facilmente.
Extrair Até A Exaustão
A primeira vez que ouvi falar desse conceito foi a partir do Robert C. Martin. Ele apresentou essa ideia em um de seus vídeos, como uma forma simples de refatorar códigos que são difíceis de entender.
A ideia básica é pegar partes pequenas e entendíveis de código e extraí-las. Não importa se você conseguir identificar quatro linhas de código ou quatro caracteres que podem ser extraídos. Ao identificar algo que pode ser encapsulado em um conceito mais claro, você deve extraí-lo. Você continua a fazer isso tanto no método original quanto nas partes recém extraídas, até que você não possa mais encontrar partes de código que possam ser encapsuladas de alguma outra forma.
Essa técnica é bastante útil quando se trabalha sozinho. Ela força você a pensar tanto nas partes pequenas quanto nas partes grandes de código. Ela tem um outro ótimo efeito: ela faz você pensar, e muito, sobre o código! Além da refatoração de extração de método ou variável mencionados acima, você realizará várias renomeações de variáveis, funções, classes e muito mais.
Vejamos um exemplo de um código aleatório da Internet. O Stackoverflow é um ótimo lugar para encontrar pequenos trechos de código. Eis um trecho que determina se um número é primo:
1 |
//Verifica se o número é primo
|
2 |
function isPrime($num, $pf = null) |
3 |
{
|
4 |
if(!is_array($pf)) |
5 |
{
|
6 |
for($i=2;$i<intval(sqrt($num));$i++) { |
7 |
if($num % $i==0) { |
8 |
return false; |
9 |
}
|
10 |
}
|
11 |
return true; |
12 |
} else { |
13 |
$pfCount = count($pf); |
14 |
for($i=0;$i<$pfCount;$i++) { |
15 |
if($num % $pf[$i] == 0) { |
16 |
return false; |
17 |
}
|
18 |
}
|
19 |
return true; |
20 |
}
|
21 |
}
|
A essa altura, não tenho ideia como esse código funciona. Apenas o encontrei na Internet enquanto escrevia esse artigo e aprenderei mais sobre ele, junto de você. O processo a seguir pode não ser o mais claro. Ao invés disso, ele refletirá meus pensamentos e refatorações de acordo com que elas forem surgindo, sem qualquer planejamento anterior.
Refatorando o Verificador de Números Primos
De acordo com a Wikipedia:
Um número primo é um número natural maior que 1 que não possui quaisquer outros divisores além do 1 e dele próprio.
Como pode ver, esse é um método simples para um problema matemático simples. Ele retorna true
ou false
, então, deve ser bem fácil testá-lo.
1 |
class IsPrimeTest extends PHPUnit_Framework_TestCase { |
2 |
|
3 |
function testItCanRecognizePrimeNumbers() { |
4 |
$this->assertTrue(isPrime(1)); |
5 |
}
|
6 |
|
7 |
}
|
8 |
|
9 |
// Verifica se um número é primo
|
10 |
function isPrime($num, $pf = null) |
11 |
{
|
12 |
// ... o conteúdo do método como visto mais acima
|
13 |
}
|
Como estamos apenas brincando com um código de exemplo, o caminho mais fácil é colocar tudo dentro de um arquivo de teste. Dessa forma, não precisaremos pensar sobre quais arquivos criar, a quais diretórios eles pertencem ou como incluí-los uns nos outros. Este é um exemplo simples para familiarizarmo-nos com a técnica antes de aplicarmos em um dos métodos do jogo de perguntas e respostas. Então, como tudo irá num só arquivo de testes, você pode nomeá-lo como deseja. O nomeei de IsPrimeTest.php
.
Esse teste passa. Meu instinto me diz que o próximo passo é adicionar alguns outros números primos e escrever um outro teste com números não primos.
1 |
function testItCanRecognizePrimeNumbers() { |
2 |
$this->assertTrue(isPrime(1)); |
3 |
$this->assertTrue(isPrime(2)); |
4 |
$this->assertTrue(isPrime(3)); |
5 |
$this->assertTrue(isPrime(5)); |
6 |
$this->assertTrue(isPrime(7)); |
7 |
$this->assertTrue(isPrime(11)); |
8 |
}
|
Isso passa. Mas, e quanto a isso?
1 |
function testItCanRecognizeNonPrimes() { |
2 |
$this->assertFalse(isPrime(6)); |
3 |
}
|
Inesperadamente, falha: 6 não é um número primo. Esperava que o método retorna-se false
. Não sei como o método funciona, ou o propósito do parâmetro $pf
- simplesmente esperava que ele retornasse false
, baseado no seu nome e descrição. Faço a menor ideia do porque não funciona ou como corrigi-lo.
Esse é um dilema bem confuso. O que deveríamos fazer? A melhor resposta de criar testes que passem para uma grande quantidade de número. É na tentativa e erro, mas, pelo menos, teremos alguma ideia sobre o que o método faz. Depois disso, poderemos começar a refatora-lo.
1 |
function testFirst20NaturalNumbers() { |
2 |
for ($i=1;$i<20;$i++) { |
3 |
echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n"; |
4 |
}
|
5 |
}
|
Ele retorna algo bem interessante:
1 |
1 - true
|
2 |
2 - true
|
3 |
3 - true
|
4 |
4 - true
|
5 |
5 - true
|
6 |
6 - true
|
7 |
7 - true
|
8 |
8 - true
|
9 |
9 - true
|
10 |
10 - false
|
11 |
11 - true
|
12 |
12 - false
|
13 |
13 - true
|
14 |
14 - false
|
15 |
15 - true
|
16 |
16 - false
|
17 |
17 - true
|
18 |
18 - false
|
19 |
19 - true
|
Um padrão começa a emergir. Tudo sai como true
até o número 9, quando começa a alternar com false
até o 19. Mas, há algum padrão em repetição? Tente executá-lo para 100 números e, imediatamente, verá que não é um padrão. Ele parece funcionar para números entre 40 e 99. Ele falha com números entre 30 e 39, dizendo que o 35 é primo. O mesmo acontecem no conjunto 20 a 29, onde ele diz que o 25 é primo.
Esse exercício iniciou como um código simples para demonstrar uma técnica que se mostra muito mais difícil que o esperado. Resolvi mantê-la, porque ela reflete a vida real de uma forma bem pertinente.
Quantas vezes você começou a trabalhar em uma tarefa que pareceu ser simples e acabou descobrindo que era extremamente difícil?
Não queremos corrigir o código. Independentemente do que o método faz, ele deve continuar a fazê-lo. Queremos refatorá-lo para que outros possam entendê-lo melhor.
Como ele não aponta corretamente os número primos, usaremos a mesma abordagem do Resultado Esperado que usamos no primeiro artigo.
1 |
function testGenerateGoldenMaster() { |
2 |
for ($i=1;$i<10000;$i++) { |
3 |
file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND); |
4 |
}
|
5 |
}
|
Execute esse código uma vez para gerar o Resultado Esperado. Ele deve executar bem rápido. Se precisar executá-lo novamente, não esqueça de remover o arquivo gerado antes de executar o teste. Ou o resultado será adicionado ao final do conteúdo anterior.
1 |
function testMatchesGoldenMaster() { |
2 |
$goldenMaster = file(__DIR__ . '/IsPrimeGoldenMaster.txt'); |
3 |
for ($i=1;$i<10000;$i++) { |
4 |
$actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; |
5 |
$this->assertTrue(in_array($actualResult, $goldenMaster), 'O valor ' . $actualResult . ' não é o resultado esperado.'); |
6 |
}
|
7 |
}
|
Agora, crie o testes para o resultado esperado. Essa solução pode não ser a mais rápida, mas é fácil de entender e dirá qual o número exato não combina, caso algo dê errado. Mas, do jeito que está, existe duplicação de código em dois métodos de testes. Poderíamos extraí-la em um método private
.
1 |
class IsPrimeTest extends PHPUnit_Framework_TestCase { |
2 |
|
3 |
function testGenerateGoldenMaster() { |
4 |
$this->markTestSkipped(); |
5 |
for ($i=1;$i<10000;$i++) { |
6 |
file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString($i), FILE_APPEND); |
7 |
}
|
8 |
}
|
9 |
|
10 |
function testMatchesGoldenMaster() { |
11 |
$goldenMaster = file(__DIR__ . '/IsPrimeGoldenMaster.txt'); |
12 |
for ($i=1;$i<10000;$i++) { |
13 |
$actualResult = $this->getPrimeResultAsString($i); |
14 |
$this->assertTrue(in_array($actualResult, $goldenMaster), 'O valor ' . $actualResult . ' não é o resultado esperado.'); |
15 |
}
|
16 |
}
|
17 |
|
18 |
private function getPrimeResultAsString($i) { |
19 |
return $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n"; |
20 |
}
|
21 |
}
|
Agora, podemos continuar para nosso código de produção. Os testes executam em cerca de dois segundos no meu computador, então, é algo aceitável.
Extraindo Tudo Que Pudermos
Primeiro, podemos extrair um método chamado isDivisible()
, na primeira parte do código.
1 |
if(!is_array($pf)) |
2 |
{
|
3 |
for($i=2;$i<intval(sqrt($num));$i++) { |
4 |
if(isDivisible($num, $i)) { |
5 |
return false; |
6 |
}
|
7 |
}
|
8 |
return true; |
9 |
}
|
Isso nos permitirá reusar o código na segunda parte do código, dessa forma:
1 |
} else { |
2 |
$pfCount = count($pf); |
3 |
for($i=0;$i<$pfCount;$i++) { |
4 |
if(isDivisible($num, $pf[$i])) { |
5 |
return false; |
6 |
}
|
7 |
}
|
8 |
return true; |
9 |
}
|
E, tão logo começamos a trabalhar com esse código, percebemos que ele é mal indentado e alinhado. Algumas vezes, as chaves aparecem no começo da linha, outras vezes, no final.
Algumas vezes, vemos o uso de tabulação para indentação, outra vezes vemos espaços. Algumas vezes, há espaços entre operando e operador, outras vezes não. E, não, esse código não foi criado para ser desse jeito. Isso é código da vida real. Código de verdade, não algum exercício artificial.
1 |
//Verifica se um númeor é primo
|
2 |
function isPrime($num, $pf = null) { |
3 |
if (!is_array($pf)) { |
4 |
for ($i = 2; $i < intval(sqrt($num)); $i++) { |
5 |
if (isDivisible($num, $i)) { |
6 |
return false; |
7 |
}
|
8 |
}
|
9 |
return true; |
10 |
} else { |
11 |
$pfCount = count($pf); |
12 |
for ($i = 0; $i < $pfCount; $i++) { |
13 |
if (isDivisible($num, $pf[$i])) { |
14 |
return false; |
15 |
}
|
16 |
}
|
17 |
return true; |
18 |
}
|
19 |
}
|
Assim está muito melhor. Imediatamente, as duas declarações if
estão bem parecidas. Mas não podemos extraí-las por conta das declarações return
. Se não retornarmos algo, quebraremos a lógica.
Se o método extraído retornar um booleano e tivermos de verificar se devemos retornar do isPrime()
, isso não ajudaria em qualquer coisa. Deve existir uma forma de extraí-lo através do uso de conceitos funcionais no PHP, talvez depois. Podemos fazer algo mais simples, primeiro.
1 |
function isPrime($num, $pf = null) { |
2 |
if (!is_array($pf)) { |
3 |
return checkDivisorsBetween(2, intval(sqrt($num)), $num); |
4 |
} else { |
5 |
$pfCount = count($pf); |
6 |
for ($i = 0; $i < $pfCount; $i++) { |
7 |
if (isDivisible($num, $pf[$i])) { |
8 |
return false; |
9 |
}
|
10 |
}
|
11 |
return true; |
12 |
}
|
13 |
}
|
14 |
|
15 |
function checkDivisorsBetween($start, $end, $num) { |
16 |
for ($i = $start; $i < $end; $i++) { |
17 |
if (isDivisible($num, $i)) { |
18 |
return false; |
19 |
}
|
20 |
}
|
21 |
return true; |
22 |
}
|
Extrair o laço for
por completo é um pouco mais fácil, mas, ao tentarmos reutilizar nosso método extraído na segunda parte do if
, percebemos que ele não funciona. Isso se dá pela existência da misteriosa variável $pf
, a qual sabemos quase nada.
Parece que ela verifica se o número é divisível por um conjunto específico de divisores ao invés de pegar todos os divisores até o número mágico determinado por intval(sqrt($num))
. Talvez devêssemos renomear $pf
em $divisors
.
1 |
function isPrime($num, $divisors = null) { |
2 |
if (!is_array($divisors)) { |
3 |
return checkDivisorsBetween(2, intval(sqrt($num)), $num); |
4 |
} else { |
5 |
return checkDivisorsBetween(0, count($divisors), $num, $divisors); |
6 |
}
|
7 |
}
|
8 |
|
9 |
function checkDivisorsBetween($start, $end, $num, $divisors = null) { |
10 |
for ($i = $start; $i < $end; $i++) { |
11 |
if (isDivisible($num, $divisors ? $divisors[$i] : $i)) { |
12 |
return false; |
13 |
}
|
14 |
}
|
15 |
return true; |
16 |
}
|
Essa é uma das formas de fazer. Adicionamos um quarto parâmetro adicional em nosso método de verificação. Se ele possuir algum valor, usamos, caso contrário, usamos $i
.
Podemos extrair alguma outra coisa? Que tal esse pequeno trecho de código: intval(sqrt($num))
?
1 |
function isPrime($num, $divisors = null) { |
2 |
if (!is_array($divisors)) { |
3 |
return checkDivisorsBetween(2, integerRootOf($num), $num); |
4 |
} else { |
5 |
return checkDivisorsBetween(0, count($divisors), $num, $divisors); |
6 |
}
|
7 |
}
|
8 |
|
9 |
function integerRootOf($num) { |
10 |
return intval(sqrt($num)); |
11 |
}
|
Isso não está muito melhor? De certa forma. Está melhor se a pessoa não souber o que intval()
e sqrt()
realizam, mas não ajuda a melhorar o entendimento da lógica. Por que terminamos nosso laço for
naquele número em específico? Talvez essa seja a pergunta o nome da nossa função devesse responder.
1 |
[PHP]//Verifica se um númeor é primo |
2 |
function isPrime($num, $divisors = null) { |
3 |
if (!is_array($divisors)) { |
4 |
return checkDivisorsBetween(2, highestPossibleFactor($num), $num); |
5 |
} else { |
6 |
return checkDivisorsBetween(0, count($divisors), $num, $divisors); |
7 |
}
|
8 |
}
|
9 |
|
10 |
function highestPossibleFactor($num) { |
11 |
return intval(sqrt($num)); |
12 |
}[PHP] |
Isso está melhor, já que explica o porque paramos nele. Talvez, no futuro, possamos inventar uma fórmula diferente para determinar aquele número. A nomeação também trouxe um pouco de inconsistência. Nós chamamos de fatores dos números, que é um sinônimo de divisores. Talvez devêssemos escolher um e usar apenas aquele. Permitirei você realizar a renomeação e refatoração como um exercício.
A pergunta é: podemos extrair algo mais? Bem, devemos tentar até a exaustão. Mencionei a possibilidade de aplicar o paradigma funcional no PHP, alguns parágrafos acima. Existem duas características da programação funcional que podemos aplicar no PHP, facilmente: funções de primeira ordem e recursão. Toda vez que vejo uma declaração if
junto de um return
dentro de um laço for
, como no nosso método checkDivisorsBetween()
, imagino a aplicação de uma ou das duas técnicas.
1 |
function checkDivisorsBetween($start, $end, $num, $divisors = null) { |
2 |
for ($i = $start; $i < $end; $i++) { |
3 |
if (isDivisible($num, $divisors ? $divisors[$i] : $i)) { |
4 |
return false; |
5 |
}
|
6 |
}
|
7 |
return true; |
8 |
}
|
Mas, por que deveríamos passar por esse processo tão complexo? O motivo mais chato para isso é que esse método realiza duas coisas distintas: Ele itera e toma decisões. Quero que ele apenas itere e deixe a decisão para outro método. Um método sempre deve realizar uma única coisa e da melhor forma possível.
1 |
function checkDivisorsBetween($start, $end, $num, $divisors = null) { |
2 |
$numberIsNotPrime = function ($num, $divisor) { |
3 |
if (isDivisible($num, $divisor)) { |
4 |
return false; |
5 |
}
|
6 |
};
|
7 |
for ($i = $start; $i < $end; $i++) { |
8 |
$numberIsNotPrime($num, $divisors ? $divisors[$i] : $i); |
9 |
}
|
10 |
return true; |
11 |
}
|
Nossa primeira tentativa foi extrair a condição e a declaração de retorno em uma variável. Ela será local, por hora. Mas o código não funciona. Na verdade, o laço for
complica bastante as coisas. Acho que precisaremos lançar mão de recursão.
1 |
function checkRecursiveDivisibility($current, $end, $num, $divisor) { |
2 |
if($current == $end) { |
3 |
return true; |
4 |
}
|
5 |
}
|
Quando pensamos sobre recursividade, devemos sempre começar com os casos excepcionais. Nossa primeira exceção é quando chegamos ao final da recursão.
1 |
function checkRecursiveDivisibility($current, $end, $num, $divisor) { |
2 |
if($current == $end) { |
3 |
return true; |
4 |
}
|
5 |
|
6 |
if (isDivisible($num, $divisor)) { |
7 |
return false; |
8 |
}
|
9 |
}
|
Nosso segundo caso excepcional que quebrará a recursão é quando o número for divisível. Não queremos que a recursão continue. E esses sãos todos nossos casos excepcionais.
1 |
ini_set('xdebug.max_nesting_level', 10000); |
2 |
function checkDivisorsBetween($start, $end, $num, $divisors = null) { |
3 |
return checkRecursiveDivisibility($start, $end, $num, $divisors); |
4 |
}
|
5 |
|
6 |
function checkRecursiveDivisibility($current, $end, $num, $divisors) { |
7 |
if($current == $end) { |
8 |
return true; |
9 |
}
|
10 |
|
11 |
if (isDivisible($num, $divisors ? $divisors[$current] : $current)) { |
12 |
return false; |
13 |
}
|
14 |
|
15 |
checkRecursiveDivisibility($current++, $end, $num, $divisors); |
16 |
}
|
Essa é uma outra tentativa de usar recursão em nosso problema, mas, infelizmente, mais de 10mil recursões no PHP induz a uma falha na sistema do PHP ou do PHPUnit, no meu computador. Então, isso talvez seja um outro beco sem saída. Mas, se tivesse funcionando, seria um ótima substituto para a lógica original.
Desafio
Quando criei o Resultado Esperado, deixei passar algo, intencionalmente. Digamos apenas que os testes não cobrem tanto código quanto deveriam. Você é capaz de encontrar o problema? Se sim, como você o resolveria?
Pontos Finais
"Extraia até a exaustão" é uma ótima maneira de dissecar métodos longos. Ela força você a pensar em pequenos trechos de códigos e dar sentido a essas peças, extraindo-as em métodos. Acho incrível como esse simples procedimento, junto de renomeação frequente, ajudam-me a descobrir que algum código é capaz de fazer algo que eu nunca pensei que pudesse.
Em nosso próximo e último artigo sobre refatoração, aplicaremos essa técnica no jogo de perguntas e respostas. Espero que tenha gostado desse tutorial, um tanto quanto diferente. Ao invés de trabalhar com exemplos de livros, pegamos um código de verdade e começamos a lutar contra exemplos reais que vemos todos os dias.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!