Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Web Development
Code

Refatorando Código Legado: Parte 6 - Atacando Métodos Complexos

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Refactoring Legacy Code.
Refactoring Legacy Code: Part 5 - Game's Testable Methods
Refactoring Legacy Code: Part 7 - Identifying the Presentation Layer

Portuguese (Português) translation by Erick Patrick (you can also view the original English article)

Código velho. Código feio. Código complicado. Código macarrônico. Código sem sentido. Em outras palavras, Código Legado. Esta é uma série que ajudará você a trabalhar e lidar com esse tipo de código).

Nas últimas cinco lições, investimos bastante tempo no entendimento do nosso sistema legado e na escrita de testes para qualquer trecho testável que pudéssemos encontrar. Chegamos a um ponto que já temos vários métodos testados, mas ainda evitamos os métodos com lógica difícil, de alta complexidade. É hora para o trabalho pesado de codificação.

Compreendendo o Método roll()

Nosso primeiro candidato é o método roll(). Como ele não retorna valor, parece incerto o que ele faz e como testá-lo. Quando não tenho certo por onde começar a testar algum trecho de código, tento lê-lo linha por linha para compreendê-lo, passo-a-passo. Algumas vezes, isso é possível, outras vezes o código, simplesmente, é muito complicado.

Enquanto estou nesse processo de leitura e aprendizado, tento realizar algumas refatorações que tenho certeza que minha IDE é capaz de fazer sem problemas. A maioria delas são renomeações de variáveis e de método que acredito ser capaz de entender e que quero torná-los óbvios para mim e para leitores futuros. E, em um segundo caso, nosso resultado esperado sempre estará lá para algum teste ocasional.

Olha a assinatura do método, roll($roll), pergunto-me: para que serve o parâmetro $roll? Ele é um objeto? É uma ação? É um número? Minha IDE pode ser bem útil, agora. Apenas posicionando o cursor sobre o parâmetro $roll, todos os seus usos serão destacados, levemente, com uma cor azulada.

Podemos perceber a variável $roll destacada nas linhas 63, 67 e 71. E essas são as únicas aparições que são possíveis de ver na tela. Mesmo que haja inúmeras outras abaixo, essas três aparições são ótimas candidatas a auxiliar-nos na descoberta do papel da variável $roll.

Na linha 63, ela é usada para imprimir texto na tela. echoln("Eles rolaram um " . $roll);. Essa linha é bem simples de entendermos, além de ser bastante útil para nós. Ela nos diz que algum jogador obteve um "$roll". Mas, o que você obtem numa jogada de dados? Um número. Poderíamos renomear $roll para $number. Isso fará com que a assinatura do nosso método pareça bem natural: roll($number).

E em relação a linha 67? A declaração condicional ainda tem algum sentido no contexto da função, caso renomeemos $roll em $number?

Não gosto muito disso. Se eu olhar apenas para esse trecho de código, não sou capaz de entender para que serve a variável $number. A definição do método está cinco linhas acima. Talvez eu o tenha perdido ou talvez nem tenha lido. Além disso, estamos no terceiro nível de indentação, nosso contexto inicial já mudou bastante. Talvez um nome mais descritivo seja preciso. Que tal $rolledNumber? Isso explicaria o fato de ser um número e também manteria sua fonte no próprio nome. Sabemos que ele é um número obtido pelo jogador. Sabemos que pode seu valor pode ser de um a seis. Isso é importante? Talvez. No final das contas, estamos tentando entender um sistema legado.

Agora que resolvemos o problema da nomeação de parâmetros, e entendemos que as duas outras linhas estão apenas retornando texto, podemos continuar e analisar nossa primeira declaração if. Também há uma atribuição de variável, logo antes dele, mas não nos preocuparemos com isso, ainda.

A primeira parte da declaração if é bem grande. Mais precisamente, 20 linhas de tamanho, indo da linha 66 até a linha 86. É bastante informação para lidar. Talvez a parte do else seja menor. Podemos rolar a barra para ver se ele é fácil o bastante para ser entendido. Essa outra parte possui de 10 a 12 linhas, apenas. E, metade delas, são de impressão de coisas, ou vazias, de modo que podemos perceber que não há tanta lógica nele. Talvez seja o melhor a ser analisado, agora.

A primeira linha da declaração if parece posicionar o jogador atual em um novo lugar do tabuleiro. Ele avança o jogador com base no número obtido. Isso é bem típico de jogos de tabuleiros e parece bem lógico.

Então, temos outro condicional, apenas um simples if, verificando se algum jogador está iniciando uma nova volta. Caso positivo, posicionamos o jogador atual para a posição em questão. Você se lembra quando simplificamos uma declaração if, extraímos um método e chamamos de playerShouldStartANewLap()? Faz um bom tempo, não é? Pois bem, o quão útil não foi aquele passo para entendermos a lógica do negócio, hein?

Finalmente, algumas coisas são apresentadas e a pergunta é feita na última linha de código.

Wow. Acabei de perceber que posso explicar o que está acontecendo, em apenas uma sentença: "O jogador é posicionado com base no número obtido, alguma informação é dita ao jogador e fazemos uma pergunta". Quando sou capaz de fazer isso, sinto uma urgência em criar, rapidamente, um método para cada parte identificada. Três métodos simples já estão zanzando na minha mente. Embora eu possa apenas depender da capacidade da minha IDE em extrair método, estarei muito mais confortável tendo testes para essa parte do código. Podemos, de alguma forma, criar um objeto game, apenas com os jogadores certos, de modo que a segunda parte da declaração if seja ativada?

Testando a Segunda Parte de roll()

Uma das maiores dificuldades em testar código legado é conseguir deixar o Sistema Sob Testes no estado apropriado, para que possamos verificar o estado que estamos interessados. Já sabemos que inicializar uma classe Game é bem fácil. Não é preciso de parâmetros para o construtor. Então, se olharmos a lista de variáveis da classe, veremos que elas foram definidas usando a palavra-chave var. No PHP, elas são consideradas variáveis públicas. E já usamos a variável currentPlayer em nossos testes anteriores, então, podemos ter certeza que podemos acessar as variáveis de fora do objeto.

A essa altura, gosto de começar a criar testes. Não um teste completo, mas o suficiente para que possa descobrir como exercitar todo o sistema sob testes.

O nome do teste ainda não é bem específico. Descobrimos três coisas estão acontecendo dentro da parte else da declaração if, mas ainda não está muito claro como definimos isso em apenas duas ou três palavras. Então, por hora, podemos usar algo para descrever o trecho de código que queremos exercitar. Teremos tempo para refatorar o nome posteriormente, se necessário.

Então, configuramos as variáveis requeridas pelo código. Basicamente, copiei e colei as variáveis e adicionei game após cada $this->. Então invoquei roll() com um número. O número é irrelevante a essa altura, escolha um número arbitrariamente.

Embora esse código não possua qualquer assertiva, podemos descobrir qual parte do código é executa, apenas olhando para o retorno.

Podemos observar que "John" é o nosso jogador atual, exatamente como definimos em nosso teste, algumas linhas acima. Então, podemos identificar os pontos chaves que só estão presentes na parte else da declaração if: "a nova posição é".

Assim, o retorno nos ajudou a ter certeza que estamos onde deveríamos estar. O próximo passo é descobrir o que deveríamos verificar dentro de nosso teste.

Poderíamos verificar a próxima posição do jogador de quando ele não inicia uma nova volta.

Certo. Escrevemos várias linhas ali. Primeiro de tudo, definimos o jogador atual e rolamos alguns valores. Depois, configuramos a posição atual do jogador no tabuleiro, de acordo com a posição especificada acima. Após a rolagem, podemos verificar a nova posição do jogador, que é quando o jogador não precisa começar uma nova volta, é a soma de dois números.

Como nossos testes estão passando, é hora de realizar alguma refatoração. Não podemos fazer muito no código de produção, ainda, mas nossos testes precisam de um pouco de atenção.

Nada demais, por hora, apenas alguns métodos extraídos para esconder as horrorosas chamadas de parâmetros que lotavam nossa classe. Agora está mais fácil de entender e, se precisarmos alterar a forma como a informação é configurada ou lida a partir do objeto Game, não precisaremos modificar o método de teste. Modificaremos o método privado, apenas.

Próxima pergunta é: "O que mais podemos testar a partir desse código de produção?" Não queremos entrar na declaração if de quando um jogador precisa começar uma nova volta. Isso será o tópico de outro teste. As duas declarações echoln() enviam dados para o retorno padrão. Há pouquíssima coisa que podemos testar sobre eles. Podemos capturar o retorno e testá-los, mas isso envolveria a parte da apresentação. Podemos sentir o peso da camada de apresentação integrada à lógica de negócios aqui, mas não podemos ver, claramente, como extraí-la. Por hora, apenas a deixemos ali, sem testes. Por fim, a uma invocação ao método askQuestion(). Precisamos verificar o que esse método faz e se podemos testá-lo de alguma forma.

O método askQuestion() verifica a categoria atual e retorna uma cadeia de caracteres para o usuário, com pergunta para ele. A categoria atual é determinada pelo método currentCategory(), que apenas verifica a posição atual e se ela corresponder a um determinado número, uma categoria relacionada será selecionada. O número três que usamos em nosso teste, corresponde à categoria "Rock". O método askQuestion() apenas retorna algo para a tela. Outra coisa relacionada à apresentação que não queremos testar ainda. Mas o método currentCategory() retorna uma cadeia de caracteres, uma que é essencial para o método askQuestion(). Talvez possamos invocar currentCategory() e garantir que a categoria correta foi retornada?

A última linha que adicionamos faz, exatamente, isso. Parece que conseguimos testar toda a funcionalidade do nosso trecho de código alvo. Agora, podemos começar a refatorar nosso código de produção.

Mas, espere! E o caso de quando precisamos iniciar uma nossa volta? Não deveríamos testá-lo também, antes de tocar no código de produção? Acredito que seja uma boa ideia continuar com os testes por hora e deixar para refatorar o código de produção, apenas estivermos mais certo possível, para não precisamos quebrar algo.

Copiamos e colamos o teste anterior, renomeamo de acordo e especificamos lugares diferentes e um número rolado. Sabemos que o tamanho do tabuleiro é de 12 lugares. Obtemos dois com posição 11, de modo que terminamos na posição um. A numeração do tabuleiro começa com a posição zero.

Mas nossa segunda assertiva falha. A categoria é "Ciências". Esse teste destacou alguns problemas em nossa abordagem: 1) Precisamos renomear nosso primeiro teste, e 2) Precisamos testar a categoria em um teste diferente. É hora de refatorar novamente.

Renomeamos o primeiro teste para refletir exatamente o que estávamos verificando. Em ambos os testes, removemos a verificação de categoria. Sabemos que temos duas categorias diferentes e duas posições. Com base em nosso conhecimento e na estrutura do método currentCategory(), podemos deduzir que há diversos lugares para várias categorias. Primeiro, definimos os lugares em vetores e, então, esperamos os dois valores diferentes para nossas categorias.

Por hora, nosso alvo não é testar o método currentCategory(). Poderíamos parar nosso processo atual e escrever testes para todas as combinações de lugares e categorias. Contudo, não quero fazer isso ainda. Agora, precisamos mantermo-nos focados no método roll e em nosso pequeno trecho de código. Ainda podemos remover a duplicação entre os dois testes, além de extrair a verificação em um método privado. Isso nos ajudará no futuro, quando escrevermos os testes para currentCategory().

Refatorando a Segunda Parte de roll()

Agora, que nossos testes estão bem feitos e passando, modificar o código fonte é o próximo passo lógico. A declaração if com a lógica de movimentação do jogador vem primeiro.

Parece que nosso método precisará dois parâmetros, pelo menos. O tamanho do tabuleiro e o número obtido. O resto das informações usadas vem das variáveis da classe, então, eles não precisam ser passados como parâmetros. Entretanto, o tamanho do tabuleiro também parece ser um valor que pertence ao método ao invés da classe game. Depois, veremos se podemos movê-la para o método em si.

O próximo passo é com relação às linhas que mostram as coisas para o usuário, que devem ir em seus próprios métodos especializados. Poderíamos ter colocado tudo relacionado a apresentação em um único método, mas seria bem difícil de nomeá-lo. É preferível termos método menores com nomes mais legíveis.

Com isso, finalizamos essa parte do código. Mas, e o resto do método roll()?

Dois Programadores, Dois Soluções Diferentes

Até agora, este tutorial focou-se em pequenos trechos de código. Nosso nível de magnificação estava bem próximo do código. Fizemos muitas coisas pensando em relação a dez, oito ou menos linhas de código. Concentramo-nos em coisas pequenas, como nomes de variáveis ou movendo uma, duas linhas de código de um lugar para outro.

Nós, da Syneto, realizamos muita programação em par. Basicamente, toda vez que há alguma tarefa, no mínimo, de nível moderado, realizamos programação em par. Refatoração é uma tarefa bem complicada, então, na maior parte do tempo, há duas pessoas olhando o mesmo código. Isso nos permite entender e pensar sobre o que estamos fazendo no código em níveis diferentes de magnificação. Enquanto um programador realiza pequenas mudanças e concentra-se nos detalhes a nível de linha e caracteres, o outro pode ver algo mais, à distância.

Embora o formato dessas páginas não permita muito espaço horizontal, na vida real, qualquer programador deveria ter, no mínimo, um monitor de 25 polegadas com uma resolução decente que permita a visualização do código em diversos níveis. Eis a forma que sou capaz de ver o método roll() em meu monitor de 27 polegadas.

Nesse nível, podemos observar a forma e a indentação, e relaxar.

Uma pessoa pode pensar sobre complexidade, projeto do código e possíveis métodos que poderiam ser extraídos. Essa pessoa pode avaliar a complexidade da lógica, enquanto a outra realizando pequenas mudanças pode lidar com a complexidade do código em si. O nível mais alto pode destacar duplicação de forma bem efetiva.

Isso não é interessante? Você tinha percebido essa enorme duplicação antes dessa imagem? Você foi capaz de se concentrar tanto nos detalhes pequenos como no nível mais alto? Talvez, sim. Algumas pessoas tem um talento natural para entender código, mas a maioria de nós não consegue concentrar-se efetivamente em mais de um nível.

E a melhor parte disso? Você também pode fazer isso sozinho! Isso é, se você não tiver a oportunidade de programar em par com outro programador, você ainda pode usar o zoom. Mas você precisa fazer isso de forma sequencial para que seja efetivo. Foi isso o que fizemos bem no começo. Olhamos o método a um nível mais alto, identificamos pequenos trechos de código que poderíamos trabalhar e demos o zoom. A mudança no foco alterou, efetivamente, a nossa forma de pensar. Permanecemos nesse estado magnificado, sem pensar no resto do código até que terminamos de refatorá-lo. Agora, podemos voltar, alterar nossa linha de pensamento e continuar.

Refatorando a Primeira Parte de roll()

Algumas pessoas conseguem mudar o nível de visão de modo de pensar, facilmente, em menos de um minuto. Outras precisam de um certo tempo para "esquecer" os detalhes e poder ir para um nível com menos zoom, e vice-versa, deixando de pensar na forma e começando a visualizar o código caractere a caractere. Se precisar de 15 minutos, não ache que tem algo errado com você, é mais comum do que parece. Tente organizar seu trabalho de forma que permita você descansar entre as magnificações.

E, mais uma vez alternando o modo de pensar, precisamos chegar mais próximo da primeira parte da declaração if, quando o usuário está na caixa de penalidades.

Esse código começa com outra declaração if() para verificar se o número obtido é ímpar. Se for, ele faz algo complicado. Se for par, ele realiza algo muito mais simples. Apenas deixa o jogador na caixa de penalidades.

Isso deve ser fácil de teste e também nos forçará a definir formas de configurar o sistema sob teste.

Sim. Foi bem fácil. Um ótimo teste com quatro linhas. E o método setAPlayerThatIsInThePenaltyBox() é muito parecido com sua contraparte. A única diferença sendo a caixa de penalidade.

Podemos começar a construir o teste, ou testes, para a primeira parte do if, quando o número obtido for ímpar.

Isso é um começo promissor. A primeira linha: testada. O resto será bem parecido com os testes para a parte do else do primeiro nível do if.

Pares Devem Ser Pares Até o Fim

Um dilema que eu e meus colegas enfrentamos na Syneto ao realizar programação em pares ou refatorações como essa, é que, quando uma tarefa torna-se bastante clara e chega bem próximo do fim, um dos programadores acaba tentado a deixar a dupla.

Se você está concentrado na escrita dos testes e na movimentação de código de um lado do outro, seu par pode ficar tentado em achar que seu papel já não tem mais serventia. Ele percebeu os problemas de alto nível, avisou você, agora é sua hora de ajeitar o código. Quando ele tenta sair, impeça-o. Diga que ele precisa ajudar você em relação aos testes, às nomeações, às estruturas, de modo que você possa concentrar-se nos procedimentos e passos requeridos pelas técnicas de refatoração que precisam ser usadas, nos passos que aprendemos nas lições anteriores.

Por exemplo, ele poderia pensar nos nomes dos testes, enquanto você está ocupado com a cópia, colagem e modificação de testes anteriores, forçando na entrada dessa parte do if.

Por outro lado, se seu par decidir que após a descoberta de todas as duplicações e dos problemas de alto nível, ele deve ser o responsável pela criação dos testes e refatoração do código, você pode achar que não tem mais o que fazer. Você pode ficar e ajudá-lo na nomeação, duplicação de baixo nível e outras coisas menores. Você também pode ajudá-lo quando ele deixar passar alguns passos, ou quando ele empacar em um teste falho por conta de um pequeno erro de digitação.

É assim que os testes serão bem nomeados, como testPlayerGettingOutOfPenaltyNextPositionWithNewLap() e as variáveis expressarão o que elas representam para o teste atual e não o que elas fizeram no teste anterior, de onde você copiou: $numberRequiredToGetOutOfPenaltyBox.

Não está bem melhor que antes? Todos os testes unitários passaram. Mas acredito que movimentos muito código que é responsável pela apresentação. Não seria bom executarmos o teste do resultado esperado?

Voltando Um Passo

E ele falhou! Faz um bom tempo, desde que executamos nosso resultado esperado, mas todas nossas modificações estão localizadas no método roll(). Então, a pior coisa que pode acontecer é a reversão de nossas mudanças.

Comecemos com um pequeno passo para trás. Suspeito que, onde vimos duplicação no retorno, na verdade, havia alguma pequena diferença. Talvez uma letra ou um espaço que não percebemos. Poderíamos reverter o retorno da primeira parte do método roll() e ver se funciona.

Ainda falha. Nossa primeira suspeita estava errada e talvez precisemos dar um passo maior. Nosso resultado esperado passava, antes de começarmos nosso trabalho? Talvez, da próxima vez, devamos começar por sua execução. Agora, precisamos pegar nossas mudanças e colocá-las em um lugar seguro e reverter todo o código para verificar nossa hipótese.

Revertendo para o estado original do método roll() faz o resultado esperado passar. Bom saber. Então, quebramos ele. Mas, quando e onde?

Agora que nosso código foi revertido ao estado original, podemos observar o retorno, colocá-lo em um arquivo texto e compará-lo ao refatorado.

Como podemos observar imediatamente, deixamos algumas linhas de fora da versão refatorada. As cadeias de caracteres nos dizem que a funcionalidade de fazer o jogador sair da caixa de penalidades está faltando. Hmm...

Vejamos o código que começamos, novamente. Aha!!! Aí está!

Uma chamada a echoln() fixa no topo da lógica movimentada. Um simples erro. Não observamos direito e substituímos todo o bloco de código pro uma chamada a um método.

Agora, isso faz com que todos os testes passem. Mesmo que você fale com um ursinho de pelúcia, é interessante dizer para alguém e explicar o problema pelo qual está passando, assim, você analisar melhor o que fez, o que está dizendo e se é a melhor solução. Além do mais, isso permite você notar erros que você cometeu e tinha deixado passar.

Adicionando o Toque Final

Antes de finalizarmos esse tutorial, deveríamos deixar o método roll() na melhor forma possível. Primeiro, todas as chamadas a echoln() vão para métodos privados.

O passo delineado acima leva na direção certa, mas meu par diz que podemos fazer melhor.

Podemos agrupar as funções de apresentação consecutivas em outras funções de apresentação.

Não está melhor? Apenas uma invocação de apresentação em cada caminho de nosso método.

Você lembra da variável $boardSize? Podemos movê-la para dentro do método movePlayer(), agora? Sim, podemos. Então, façamos.

Nosso código está ficando bem pequeno. Mas, ainda assim, esse método possui 18 linhas. Isso é muito. Você lembra dos ensinamentos do Robert C. Martin em que ele diz que o "Número mágico é sete, mais ou menos dois"? Nossos métodos ficariam melhores se eles estivessem contidos em mais ou menos 4 linhas de código.

O primeiro passo para isso é reduzir a uma única invocação de função para cada caminho possível.

Agora, chegamos às 12 linhas de código. Mas podemos melhorar ainda mais. O if mais interno pode ir dentro desse método.

E, Terminamos!

Com isso, chegamos a sete linhas de código em nosso método. Apenas cinco linhas dentro do método, com apenas quatro realizando algum tipo de lógica. Agora, esse é um método razoável e chegamos a um ponto que me sinto confortável em parar de mexer nele. Além disso, isto não é apenas um exemplo. Isso é "Extraia até cansar" e é como a maioria dos métodos na Syneto parecem. Esse é um exemplo da vida real de até onde podemos chegar, dia após dia, em todos os seus códigos. É aqui onde paramos nossa lição, por hoje, também.

Mantenha-se ligado para o próximo tutorial, onde falaremos sobre as camadas da aplicação e começaremos a separar cada uma delas.

Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!

Advertisement
Advertisement
Advertisement
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.