Refatorando Código Legado: Parte 9 - Analisando Interesses
() translation by (you can also view the original English article)
Neste tutorial, continuaremos a focar na lógica de negócios. Avaliaremos se o conteúdo do arquivo RunnerFunctions.php
pertence a uma classe e, em caso positivo, a qual classe. Pensaremos sobre os interesses e sobre onde os métodos pertencem. Por fim, aprenderemos um pouco mais sobre o conceito de simulação. Então, o que estamos esperando? Vamos lá!
RunnerFunctions
- Dos Procedimentos à Orientação a Objetos
Mesmo que tenhamos a maior parte de nosso código orientado a objetos, bem organizado em classes, simplesmente, algumas funções vagam em um arquivo. Precisamos parar um pouco para dar um aspecto mais orientado a objetos às funções do arquivo RunnerFunctions.php
.
1 |
const WRONG_ANSWER_ID = 7; |
2 |
const MIN_ANSWER_ID = 0; |
3 |
const MAX_ANSWER_ID = 9; |
4 |
|
5 |
function isCurrentAnswerCorrect($minAnswerId = MIN_ANSWER_ID, $maxAnswerId = MAX_ANSWER_ID) { |
6 |
return rand($minAnswerId, $maxAnswerId) != WRONG_ANSWER_ID; |
7 |
}
|
8 |
|
9 |
function run() { |
10 |
$display = new CLIDisplay(); |
11 |
$aGame = new Game($display); |
12 |
$aGame->add("Chet"); |
13 |
$aGame->add("Pat"); |
14 |
$aGame->add("Sue"); |
15 |
|
16 |
do { |
17 |
$dice = rand(0, 5) + 1; |
18 |
$aGame->roll($dice); |
19 |
} while (!didSomebodyWin($aGame, isCurrentAnswerCorrect())); |
20 |
}
|
21 |
|
22 |
function didSomebodyWin($aGame, $isCurrentAnswerCorrect) { |
23 |
if ($isCurrentAnswerCorrect) { |
24 |
return ! $aGame->wasCorrectlyAnswered(); |
25 |
} else { |
26 |
return ! $aGame->wrongAnswer(); |
27 |
}
|
28 |
}
|
Meu primeiro instinto é de, simplesmente, envolvê-las em uma classe. Isso não é nada genioso, mas é algo que nos faz começar a alterar as coisas. Vejamos se a ideia funciona.
1 |
const WRONG_ANSWER_ID = 7; |
2 |
const MIN_ANSWER_ID = 0; |
3 |
const MAX_ANSWER_ID = 9; |
4 |
|
5 |
class RunnerFunctions { |
6 |
|
7 |
function isCurrentAnswerCorrect($minAnswerId = MIN_ANSWER_ID, $maxAnswerId = MAX_ANSWER_ID) { |
8 |
return rand($minAnswerId, $maxAnswerId) != WRONG_ANSWER_ID; |
9 |
}
|
10 |
|
11 |
function run() { |
12 |
// ... //
|
13 |
}
|
14 |
|
15 |
function didSomebodyWin($aGame, $isCurrentAnswerCorrect) { |
16 |
// ... //
|
17 |
}
|
18 |
}
|
Se fizermos isso, precisaremos modificar os testes e o arquivo GameRunner.php
para que usem a nova classe. Nomeamos a classe com algo genérico, por hora, já que renomeá-la será fácil quando for preciso. Nem sabemos ainda se essa classe existirá por conta própria ou se será assimilada à classe Game
. Então, não se preocupe com o nome dela ainda.
1 |
private function generateOutput($seed) { |
2 |
ob_start(); |
3 |
srand($seed); |
4 |
(new RunnerFunctions())->run(); |
5 |
$output = ob_get_contents(); |
6 |
ob_end_clean(); |
7 |
return $output; |
8 |
}
|
Em nosso arquivo GoldenMasterTest.php
, precisamos modificar a forma que executamos o código. A terceira linha da função generateOutput()
precisa ser alterada para que seja criado um novo objeto e invocada a função run()
nele. Mas isso dá em erro.
1 |
PHP Fatal error: Call to undefined function didSomebodyWin() in ... |
Precisamos modificar nossa classe, mais ainda.
1 |
do { |
2 |
$dice = rand(0, 5) + 1; |
3 |
$aGame->roll($dice); |
4 |
} while (!$this->didSomebodyWin($aGame, $this->isCurrentAnswerCorrect())); |
Só precisamos alterar a condição da declaração while
no método run()
. O novo código invoca didSomebodyWin()
e isCurrentAnswerCorrect()
a partir da classe atual, colocando $this->
antes desses métodos.
Isso faz com que o resultado esperado passe, mas faz com que o executador quebre os testes.
1 |
PHP Fatal error: Call to undefined function isCurrentAnswerCorrect() in /.../RunnerFunctionsTest.php on line 25 |
O problema está na função assertAnswersAreCorrectFor()
, mas é facilmente ajustável, criando um objeto executador antes.
1 |
private function assertAnswersAreCorrectFor($correctAnserIDs) { |
2 |
$runner = new RunnerFunctions(); |
3 |
foreach ($correctAnserIDs as $id) { |
4 |
$this->assertTrue($runner->isCurrentAnswerCorrect($id, $id)); |
5 |
}
|
6 |
}
|
Esse mesmo problema também precisa ser ajustado em outras três funções:
1 |
function testItCanFindWrongAnswer() { |
2 |
$runner = new RunnerFunctions(); |
3 |
$this->assertFalse($runner->isCurrentAnswerCorrect(WRONG_ANSWER_ID, WRONG_ANSWER_ID)); |
4 |
}
|
5 |
|
6 |
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() { |
7 |
$runner = new RunnerFunctions(); |
8 |
$this->assertTrue($runner->didSomebodyWin($this->aFakeGame(), $this->aCorrectAnswer())); |
9 |
}
|
10 |
|
11 |
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() { |
12 |
$runner = new RunnerFunctions(); |
13 |
$this->assertFalse($runner->didSomebodyWin($this->aFakeGame(), $this->aWrongAnswer())); |
14 |
}
|
Embora isso faça os testes passarem, traz duplicação de código. Como estamos com todos os testes passando, podemos extrair uma função executador para o método setUp()
.
1 |
private $runner; |
2 |
|
3 |
function setUp() { |
4 |
$this->runner = new Runner(); |
5 |
}
|
6 |
|
7 |
function testItCanFindCorrectAnswer() { |
8 |
$this->assertAnswersAreCorrectFor($this->getCorrectAnswerIDs()); |
9 |
}
|
10 |
|
11 |
function testItCanFindWrongAnswer() { |
12 |
$this->assertFalse($this->runner->isCurrentAnswerCorrect(WRONG_ANSWER_ID, WRONG_ANSWER_ID)); |
13 |
}
|
14 |
|
15 |
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() { |
16 |
$this->assertTrue($this->runner->didSomebodyWin($this->aFakeGame(), $this->aCorrectAnswer())); |
17 |
}
|
18 |
|
19 |
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() { |
20 |
$this->assertFalse($this->runner->didSomebodyWin($this->aFakeGame(), $this->aWrongAnswer())); |
21 |
}
|
22 |
|
23 |
private function assertAnswersAreCorrectFor($correctAnserIDs) { |
24 |
foreach ($correctAnserIDs as $id) { |
25 |
$this->assertTrue($this->runner->isCurrentAnswerCorrect($id, $id)); |
26 |
}
|
27 |
}
|
Muito bom. Todas essas novas criações e refatorações me fizeram pensar. Nomeamos nossa variável de runner
. Talvez nossa classe devesse ser nomeada da mesma forma. É hora de refatoração. Deve ser muito fácil.



Se você não marcou "Search for text occurrences" na caixa acima, não esqueça de alterar suas inclusões manualmente, uma vez que a refatoração também renomeará o arquivo.
Agora, temos um arquivo chamado GameRunner.php
, outro de Runner.php
e um terceiro de Game.php
. Não sei você, mas isso está bem confuso para mim. Se eu visse esses três arquivos pela primeira vez na minha vida, não saberia o que cada uma faz. Precisamos eliminar um deles, no mínimo.
O motivo de criarmos o arquivo RunnerFunctions.php
lá no começo da refatoração, foi para podermos incluir todos os métodos e arquivos para testes. Precisávamos acessar tudo, mas não executar a não ser em um ambiente preparado em nosso resultado esperado. Ainda podemos fazer a mesma coisa, só que sem executar nosso código a partir do GameRunner.php
. Precisamos atualizar as inclusões e criar uma classe, antes de continuarmos.
1 |
require_once __DIR__ . '/Display.php'; |
2 |
require_once __DIR__ . '/Runner.php'; |
3 |
(new Runner())->run(); |
Isso servirá. Precisamos incluir Display.php
explicitamente, para quando Runner
tentar criar um novo objeto CLIDisplay
, ele saiba o que implementar.
Analisando os Interesses
Acredito que uma das características mais importantes da programação orientada a objetos é definir os interesses. Sempre me pergunta algo como "essa classe realiza o que seu nome diz?", "esse método é de interesse desse objeto?", "meu objeto deve preocupar-se sobre aquele valor em específico?"
Supreendentemente, esses tipos de perguntas tem uma grande capacidade de clarificação sobre o domínio do negócio e sobre a arquitetura do software. Perguntamos e respondemos esses tipos de questões em grupo, lá na Syneto. Muitas vezes, quando um programador encontra-se em um dilema, ele ou ela se levanta, pede por alguns minutos de atenção da equipe para que todos deem suas opiniões sobre o assunto. Aqueles que estão familiarizados com a arquitetura do código, responderão do ponto de vista do software, enquanto os outros, mais familiarizados com o domínio do negócio, podem dar uma luz e comentários sobre os aspectos comerciais.
Pensemos nos interesses do nosso caso. Podemos continuar com foco sobre a classe Runner
. É muito mais provável eliminar ou transformar essa classe que a classe Game
.
Primeiro, um executador deveria saber se isCurrentAnswerCorrect()
funciona? Um executador deve ter qualquer conhecimento sobre perguntas e respostas?
Parece que esse método estaria melhor na classe Game
. Acredito, fortemente, que uma classe Game
de um jogo sobre perguntas e respostas deva preocupar-se se uma resposta está correta ou não. Também acredito que uma classe Game
deve estar preocupada em prover a resposta para a pergunta atual.
É hora de agir. Realizaremos uma refatoração de movimentação de método. Como vimos isso em tutoriais anteriores, apenas mostrarei o resultado final.
1 |
require_once __DIR__ . '/CLIDisplay.php'; |
2 |
include_once __DIR__ . '/Game.php'; |
3 |
|
4 |
class Runner { |
5 |
|
6 |
function run() { |
7 |
//...//
|
8 |
}
|
9 |
|
10 |
function didSomebodyWin($aGame, $isCurrentAnswerCorrect) { |
11 |
//...//
|
12 |
}
|
13 |
}
|
É essencial perceber que não foi apenas o método que foi movido, mas, também, a constante que define os limites das respostas.
Mas e o método didSomebodyWin()
? Um executador deve decidir se alguém ganhou o jogo? Se olharmos o corpo do método, poderemos ver um problema bem destacado, como se fosse uma lanterna ligada no escuro.
1 |
function didSomebodyWin($aGame, $isCurrentAnswerCorrect) { |
2 |
if ($isCurrentAnswerCorrect) { |
3 |
return !$aGame->wasCorrectlyAnswered(); |
4 |
} else { |
5 |
return !$aGame->wrongAnswer(); |
6 |
}
|
7 |
}
|
O que quer que seja que esse método faz, ele age apenas sobre o objeto Game
. Ele verifica a resposta atual retornada pelo jogo. E, então, retornar o que quer que seja que o objeto Game
retorna de seus métodos wasCorrectlyAnswered()
ou wrongAnswer()
. Esse método não faz qualquer coisa por conta própria. Tudo com o que ele se importa é o objeto Game
. Esse é um exemplo clássico sintoma de má programação, chamado de Inveja de Funcionalidade. Uma classe faz algo que outra classe deveria fazer. Hora de movê-la.
1 |
class RunnerFunctionsTest extends PHPUnit_Framework_TestCase { |
2 |
|
3 |
private $runner; |
4 |
|
5 |
function setUp() { |
6 |
$this->runner = new Runner(); |
7 |
}
|
8 |
|
9 |
}
|
Como de costume, movemos os testes primeiro. TDD? Alguém?
Isso nos deixa sem testes a serem executados, logo, podemos dar fim a esse arquivo. Remoção é a minha parte favorita da programação.
E executamos nossos testes e obtemos um erro.
1 |
Fatal error: Call to undefined method Game::didSomebodyWin()
|
É hora de alterar o código também. Copiar e colar o método na classe Game
fará com que todos os testes passem automagicamente. Tanto os antigos quanto os novos movidos para GameTest
. Mas, embora isso coloque os métodos no lugar certo, há dois problemas: o executador também precisa ser alterado, e enviamos um objeto Game
falso que não precisamos mais, uma vez que tudo faz parte da classe Game
, agora.
1 |
do { |
2 |
$dice = rand(0, 5) + 1; |
3 |
$aGame->roll($dice); |
4 |
} while (!$aGame->didSomebodyWin($aGame, $this->isCurrentAnswerCorrect())); |
Consertar o executador é bem fácil. Apenas alterarmos $this->didSomebodyWin(...)
para $aGame->didSomebodyWin(...)
. Precisaremos voltar aqui e alterá-la novamente, após nosso próximo passo. A refatoração dos testes.
1 |
function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided() { |
2 |
$aGame = \Mockery::mock('Game[wasCorrectlyAnswered]'); |
3 |
$aGame->shouldReceive('wasCorrectlyAnswered')->once()->andReturn(false); |
4 |
$this->assertTrue($aGame->didSomebodyWin($this->aCorrectAnswer())); |
5 |
}
|
É hora da simulação! Ao invés de usar nossa classe falsa, criada ao final dos nossos testes, usaremos a biblioteca Mockery. Ela nos permite sobrescrever um método de Game
, esperar que ele seja invocado e que tenha retornado o valor que queremos. Claro, poderíamos alcançar isso fazendo com que nossa classe falsa estendesse Game
e sobrescrevêssemos o método por conta própria. Mas, por que ter esse trabalho todo quando temos uma ferramenta para isso?
1 |
function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided() { |
2 |
$aGame = \Mockery::mock('Game[wrongAnswer]'); |
3 |
$aGame->shouldReceive('wrongAnswer')->once()->andReturn(true); |
4 |
$this->assertFalse($aGame->didSomebodyWin($this->aWrongAnswer())); |
5 |
}
|
Após nosso segundo método ser reescrito, podemos remover a classe game
falsa e quaisquer métodos que a inicializava. Problema resolvido!
Pontos Finais
Mesmo que, basicamente, tenhamos trabalhado apenas com a classe Runner
, obtivemos um ótimo progresso, hoje. Aprendemos sobre responsabilidade, identificamos métodos e variáveis que pertencem a outra classe. Pensamos em um nível mais alto e evoluímos nosso código em direção a uma solução melhor. Na equipe da Syneto, há uma forte crença que sempre há alguma forma boa de programar e que nunca devemos enviar uma mudança a não ser que o código esteja um pouco mais limpo e claro. Essa é uma técnica que, como tempo, pode levar a uma base de código melhor, com menos dependências, mais testes e, eventualmente, menos erros.
Obrigado pelo seu tempo.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!