Advertisement
  1. Code
  2. Web Development

Refatorando Código Legado: Parte 9 - Analisando Interesses

Scroll to top
Read Time: 9 min
This post is part of a series called Refactoring Legacy Code.
Refactoring Legacy Code: Part 8 - Inverting Dependencies for a Clean Architecture
Refactoring Legacy Code - Part 10: Dissecting Long Methods with Extractions

() 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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.