A melhor maneira de analisar uma classe grande antes de refatorá-la em classes menores?

8

Prefácio

Estou não está procurando uma maneira de refatorar uma grande classe código espaguete, o assunto foi abordado em outras questões.

Questão

Estou procurando técnicas para começar a entender um arquivo de classe escrito por outro colega de trabalho que abrange mais de 4000 linhas e tem um único método de atualização enorme que tem mais de 2000 linhas.

Eventualmente, espero criar testes de unidade para esta classe e refatorá-la em muitas classes menores que seguem o DRY e o Princípio de Responsabilidade Única.

Como posso gerenciar e abordar esta tarefa? Eu gostaria de poder eventualmente desenhar um diagrama dos eventos que ocorrem dentro da classe e depois passar para a funcionalidade de abstração, mas estou lutando para obter uma visão de cima para baixo de quais são as responsabilidades e dependências da classe.


Editar: nas respostas as pessoas já abordaram tópicos relacionados aos parâmetros de entrada, essas informações são para iniciantes:

Nesse caso, o método principal da classe não possui parâmetros de entrada e executa um loop while até que seja instruído a parar. O construtor também não aceita parâmetros.

Isso aumenta a confusão da classe, seus requisitos e dependências. A classe obtém referências a outras classes através de métodos estáticos, singletons e alcançando através de classes às quais já tem referência.

sydan
fonte
1
Como isso difere da maioria das tarefas de refatoração? Você não está planejando quebrar a interface, está?
JeffO 18/03/2015
@JeffO Eu acho que ele quer uma visão de 30000 pés, como entender metodologicamente o que a classe está fazendo e o que precisa ser feito; não "extrair método", "extrair variável" etc.
Thomas Junk
1
Não crie eventualmente testes de unidade. Comece a criar testes de unidade agora e nos testes codifique todas as informações que você aprender sobre a classe à medida que avança. (Infelizmente, os testes podem descobrir erros na classe, que vai acabar adicionando confusão para todo o quadro, mas ainda é melhor do que tentar entender o que está acontecendo sem quaisquer testes no lugar.)
Mike Nakis
2
Pode ser uma duplicata: programmers.stackexchange.com/questions/6395/…
JeffO
1
possível duplicata de código estrangeira Decifrando
mosquito

Respostas:

10

Curiosamente, a refatoração é a maneira mais eficiente que encontrei para entender códigos como este. Você não precisa fazer isso de forma limpa no início. Você pode fazer uma passagem de refatoração de análise rápida e suja, depois reverter e fazê-lo com mais cuidado com testes de unidade.

O motivo disso funcionar é porque a refatoração feita corretamente é uma série de pequenas mudanças quase mecânicas que você pode fazer sem realmente entender o código. Por exemplo, eu posso ver um blob de código que está sendo repetido em todos os lugares e fatorá-lo em um método, sem realmente precisar saber como ele funciona. Eu poderia dar a ele algum nome esfarrapado no começo, step1ou algo assim. Então percebo isso step1e step7pareço frequentemente aparecer juntos, e fatoro isso. Então, de repente, a fumaça se dissipou o suficiente para que eu pudesse realmente ver o quadro geral e criar alguns nomes significativos.

Para encontrar a estrutura geral, descobri que a criação de um gráfico de dependência geralmente ajuda. Faço isso manualmente usando o graphviz, pois descobri que o trabalho manual me ajuda a aprendê-lo, mas provavelmente existem ferramentas automatizadas disponíveis. Aqui está um exemplo recente de um recurso em que estou trabalhando. Descobri que essa também é uma maneira eficaz de comunicar essas informações aos meus colegas.

gráfico de dependência

Karl Bielefeldt
fonte
2
Porra, eu gostaria de ter lido isso antes da minha entrevista hoje ... Recebi a tarefa de refatorar uma aula, isso teria me ajudado muito.
Grim
8

Primeiro, uma palavra de cautela (mesmo que não seja o que você está perguntando) antes de responder à sua pergunta: Uma pergunta tremendamente importante a ser feita é por quevocê está querendo refatorar alguma coisa? O fato de você mencionar "colega de trabalho" levanta a questão: existe uma razão comercial para a mudança? Especialmente em um ambiente corporativo, sempre haverá códigos herdados e até mesmo recém-confirmados com os quais você não ficará satisfeito. O fato é que dois engenheiros não resolverão um determinado problema com a mesma solução. Se funciona e não tem implicações horríveis no desempenho, por que não deixar? O trabalho de recursos geralmente é muito mais benéfico do que a refatoração, porque você não gosta particularmente de uma implementação. Dito isto, há algo a ser dito em relação à dívida técnica e à limpeza de código não sustentável. Minha única sugestão aqui é que você já fez a distinção e se comprometeu a fazer as alterações, caso contrário você provavelmente será solicitado "depois de ter afundado muito esforço e provavelmente terá muito mais (e compreensível) reação.


Antes de abordar um refator, sempre quero ter uma compreensão firme do que estou mudando. Geralmente, existem dois métodos que utilizo para lidar com esse tipo de coisa: registro e depuração. Este último é facilitado pela existência de testes de unidade (e testes de sistema, se fizerem sentido). Como você não tem nenhum, eu fortemente exortá-lo a escrever testes com cobertura de ramo completo para garantir que as alterações não criam mudanças comportamentais inesperados. Os testes de unidade e o detalhamento do código ajudam a entender o comportamento de baixo nível em um ambiente controlado. O registro ajuda a entender melhor o comportamento na prática (isso é especialmente útil em situações multithread).

Uma vez que eu obter uma melhor compreensão de tudo o que está acontecendo através de depuração e exploração madeireira, então eu vou começar a ler código. Neste ponto, geralmente tenho uma compreensão muito melhor do que está acontecendo, e sentar e ler 2k LoC deve fazer muito mais sentido nesse momento. Minha intenção aqui é mais obter uma visão de ponta a ponta e garantir que não haja nenhum tipo de casos extremos que os testes de unidade que escrevi não estejam cobrindo.

Então vou pensar no design e elaborar um novo. Se a compatibilidade com versões anteriores tiver alguma importância, assegurarei que a interface pública não seja alterada.

Com um entendimento firme do que está acontecendo, testes e um novo design em mãos, eu o colocarei para revisão. Depois que o design tiver sido testado por pelo menos duas pessoas (espero familiarizado com o código), começarei a implementar as alterações.

Espero que isso não seja dolorosamente óbvio e ajude.

Demian Brecht
fonte
1
Sua resposta é realmente útil para mim. Na verdade, eu não tinha pensado em produzir resultados de log da classe atual primeiro, mas existem alguns problemas: não acredito que possa testar esta classe. Toda a sua principal funcionalidade ocorre em um método sem parâmetros de entrada; esse método captura a maior parte de seu estado de outras classes que não são passadas; o construtor também não aceita argumentos; as dependências são acessadas por meio de singletons. Sua sugestão é fazer o teste de unidade primeiro e depois começar a reprojetar. Como me adaptar se for provável que seja necessário um reprojeto para realizar o teste de unidade?
sydän
Uma observação adicional sobre sua palavra de cautela. Entendo isso, já vi pessoas conversando sobre a melhor maneira de deixar essas coisas de lado, no entanto, a classe ainda está sendo desenvolvida e os novos desenvolvedores estão começando a trabalhar com ela. Com novas modificações e sem testes de unidade, não há como entender como uma mudança afeta a classe. Eu já fui encarregado de abstrair um recurso da classe e concluí esta tarefa, mas isso fez pouca diferença no tamanho gigantesco do problema e acredito que muito mais precisa ser feito.
sydän
1
@ sydan: Você é capaz de zombar da interação com outras classes? Eu sei que isso é mais fácil em alguns idiomas do que em outros. Se não for possível, talvez testes de sistema para começar?
Demian Brecht
Interessante, por que o voto negativo?
Demian Brecht
Eu também estou interessado.
sydän
4

Não existe uma receita geral, mas algumas regras práticas ( supondo uma linguagem de tipo estaticamente, mas isso não deveria realmente importar):

1) Dê uma olhada na assinatura do método. Diz-lhe o que se passa em e espero que vem para fora . Do total estado deste deus de classe, eu acho que este é um primeiro ponto de dor . Suponho que mais de um parâmetro seja usado.

2) Use a função de pesquisa do seu editor / EDI para determinar Exit-Pontos (geralmente um retorno statement_ é usado)

Pelo que você sabe, o que a função precisa para testar e o que você espera em troca .

Assim, um primeiro teste simples seria chamar a função com os parâmetros necessários e esperar que o resultado é não nulo . Isso não é muito, mas um ponto de partida.

A partir daí, você pode entrar em um círculo hermenêutico (um termo cunhado por HG Gadamer - um filósofo alemão). O ponto é: você agora tem um entendimento rudimentar da classe e atualiza esse entendimento com novos conhecimentos detalhados e tem um novo entendimento de toda a classe.

Isso combinado com o método científico : faça suposições e veja se elas se sustentam.

3) Pegue um parâmetro e veja onde na classe ele é transformado de alguma forma:

Por exemplo, você está fazendo Java como eu, geralmente existem getter e setter para os quais você pode procurar. Searchpattern $objectname. (ou $objectname\.(get|set)se você estiver executando Java)

Agora você pode fazer outras suposições sobre o que o método faz.

Rastreie apenas os parâmetros de entrada ( primeiro ), cada um através do método Se necessário, faça alguns diagramas ou tabelas , onde você anota todas as alterações em cada uma das variáveis.

A partir disso, você pode escrever outros testes, descrevendo o comportamento do método. Se você tiver um entendimento aproximado de como cada parâmetro de entrada é transformado ao longo do método, comece a experimentar : passe nulo para um parâmetro ou entrada estranha . Faça suposições, verifique o resultado e varie entradas e suposições.

Se você fizer isso uma vez, terá uma "tonelada" de testes descrevendo o comportamento do seu método.

4) Em uma próxima etapa, eu procuraria dependências : o que o método precisa além de sua entrada para funcionar corretamente ? Existem possibilidades para reduzir ou reestruturar essas? Quanto menos dependências você tiver, mais claramente você verá os pontos onde realizar as primeiras divisões.

5) A partir daí, você pode percorrer toda a estrada de refatoração com padrões de refatoração e refatoração para padrões.

Aqui está um bom vídeo : GoGaRuCo 2014- O método científico de solução de problemas Trata-se de solução de problemas, mas útil para uma metodologia geral de entendimento de como algo funciona .

Você menciona que a função chamada não possui parâmetros de entrada : nesse caso especial, eu tentaria primeiro identificar as dependências e refatorá-las para parâmetros, para que você pudesse trocá-las como quiser.

Thomas Junk
fonte
Olá Thomas, obrigado por sua resposta, como a de Demian, é muito útil e uma boa abordagem metódica. Acredito que vou começar a avançar nessa rota combinando o registro que o Demian sugere com a variedade de entradas que você sugeriu. Infelizmente, o método executado por esta classe não possui parâmetros de entrada. É uma tarefa de relógio que executa um loop while eterno; parece que as dependências são capturadas por meio de singletons, classes estáticas e, em seguida, alcançando classes para capturar outras pessoas.
sydän
O primeiro estágio de estabelecer as entradas dos métodos provavelmente será o mais difícil para mim, pois não há como saber quais variáveis ​​que a classe usa são entradas, constantes, saídas, variáveis ​​temporárias ou variáveis ​​de depuração aleatória.
21139 sydan
Oh, isso parece divertido! Talvez você pule direto para ela e siga o primeiro objeto que encontrar. O círculo então começa como descrito;)
Thomas Junk
Essa pode ser a melhor rota ... um 'ctrl-f' para a variável mais usada pareceria um forte ponto de partida? Devo editar minha pergunta original para mencionar a falta de parâmetros de entrada e a estrutura de métodos?
sydän
1
Gosto do que você mencionou em sua edição.
sydän
4

Você mencionou um updatemétodo de 2000 linhas .

Eu começaria a ler esse método de cima para baixo. Quando você encontrar um conjunto de instruções que pertencem uma à outra, ou seja, compartilhar uma responsabilidade (por exemplo, criar um usuário), extraia essas instruções em uma função.

Dessa forma, você não muda como o código funciona, mas você

  • encurtar esse método enorme
  • obter um entendimento melhor e mais alto desse método
  • preparar o método para refatorações adicionais

Repita essas etapas para outros métodos.

Oliver Weiler
fonte
1
Esse processo, quando realizado de maneira não destrutiva (apenas leia, observe, planeje), às vezes é chamado de revisão de código . O livro de Steven C. McConnell - Code Complete - tem algum bom modelo de lista de verificação para esta ocasião
xmojmr