Como evito duplicar códigos de maneira desconhecida?

33

Eu trabalho em uma base de código bastante grande. Centenas de classes, toneladas de arquivos diferentes, muitas funcionalidades, leva mais de 15 minutos para baixar uma cópia nova, etc.

Um grande problema com uma base de código tão grande é que ela possui muitos métodos utilitários e faz a mesma coisa, ou possui código que não utiliza esses métodos utilitários quando poderia. E também os métodos utilitários não são todos da mesma classe (porque seria uma grande confusão).

Sou bastante novo na base de código, mas o líder da equipe que trabalha nela há anos parece ter o mesmo problema. Isso leva a muito código e duplicação de trabalho e, como tal, quando algo quebra, geralmente é quebrado em 4 cópias basicamente do mesmo código

Como podemos refrear esse padrão? Como na maioria dos projetos grandes, nem todo o código é documentado (embora alguns o sejam) e nem todo o código é ... bem, limpo. Mas, basicamente, seria muito bom se pudéssemos trabalhar para melhorar a qualidade a esse respeito, para que, no futuro, tivéssemos menos duplicação de código e coisas como funções utilitárias fossem mais fáceis de descobrir.

Além disso, as funções utilitárias geralmente estão em alguma classe auxiliar estática, em alguma classe auxiliar não estática que funciona em um único objeto ou é um método estático na classe com a qual ela "ajuda" principalmente.

Eu tive um experimento em adicionar funções utilitárias como métodos de extensão (não precisava de nenhum elemento interno da classe, e isso definitivamente só era necessário em cenários muito específicos). Isso teve o efeito de impedir a bagunça da classe principal e tal, mas não é mais possível descobrir a menos que você já saiba sobre ela

Earlz
fonte

Respostas:

30

A resposta simples é que você realmente não pode impedir a duplicação de código. No entanto, você pode "corrigi-lo" através de um processo incremental repetitivo contínuo difícil que se resume em duas etapas:

Etapa 1. Comece a escrever testes no código legado (de preferência usando uma estrutura de teste)

Etapa 2. Reescreva / refatorar o código duplicado usando o que aprendeu com os testes

Você pode usar ferramentas de análise estática para detectar código duplicado e, para C #, existem várias ferramentas que podem fazer isso por você:

Ferramentas como essa o ajudarão a encontrar pontos no código que faz coisas semelhantes. Continue escrevendo testes para determinar o que realmente fazem; use os mesmos testes para simplificar o uso do código duplicado. Essa "refatoração" pode ser feita de várias maneiras e você pode usar esta lista para determinar a correta:

Além disso, há também um livro inteiro sobre esse tópico, de Michael C. Feathers, Trabalhando efetivamente com o código legado . Ele detalha diferentes estratégias que você pode adotar para alterar o código para melhor. Ele tem um "algoritmo de alteração de código herdado", que não está longe do processo de duas etapas acima:

  1. Identificar pontos de mudança
  2. Encontre pontos de teste
  3. Quebrar dependências
  4. Escrever testes
  5. Faça alterações e refatorar

O livro é uma boa leitura, se você estiver lidando com desenvolvimento de campo integral, ou seja, código legado que precisa ser alterado.

Nesse caso

No caso do OP, posso imaginar que o código não testável é causado por um pote de mel para "métodos e truques de utilidade" que assumem várias formas:

  • métodos estáticos
  • uso de recursos estáticos
  • classes singleton
  • valores mágicos

Observe que não há nada errado com elas, mas, por outro lado, elas geralmente são difíceis de manter e mudar. Os métodos de extensões no .NET são métodos estáticos, mas também são relativamente fáceis de testar.

Antes de prosseguir com as refatorações, converse com sua equipe sobre isso. Eles precisam ser mantidos na mesma página que você antes de prosseguir com qualquer coisa. Isso ocorre porque, se você refatorar algo, as chances são altas de causar conflitos de mesclagem. Portanto, antes de refazer algo, investigue-o e peça à sua equipe para trabalhar nesses pontos de código com cautela por um tempo até terminar.

Como o OP é novo no código, há outras coisas a fazer antes de fazer qualquer coisa:

  • Tire um tempo para aprender com a base de código, ou seja, quebre "tudo", teste "tudo", reverta.
  • Peça a alguém da equipe para revisar seu código antes de confirmar. ;-)

Boa sorte!

Spoike
fonte
Na verdade, temos bastante testes de unidade e integração. Não é 100% de cobertura, mas algumas das coisas que fazemos são quase impossíveis de realizar testes unitários sem alterações radicais em nossa base de códigos. Eu nunca considerei usar a análise estática para encontrar duplicação. Vou ter que tentar isso a seguir.
Earlz
@Earlz: A análise de código estático é incrível! ;-) Além disso, sempre que você precisa fazer a mudança, pensar em soluções para fazer mudanças mais fácil (verifique o refactor ao catálogo padrões para isso)
Spoike
+1 Eu entenderia se alguém colocasse uma recompensa nesse Q para atribuir essa resposta como "extra útil". O catálogo Refatorar para padrões é ouro, coisas como essa na moda de GuidanceExplorer.codeplex.com são ótimos auxiliares de programação.
Jeremy Thompson
2

Também poderíamos tentar ver o problema de outro ângulo. Em vez de pensar que o problema é a duplicação de código, podemos considerar se o problema se origina na falta de políticas para reutilização de código.

Recentemente, li o livro Engenharia de software com componentes reutilizáveis e ele realmente tem um conjunto de idéias muito interessantes sobre como promover a reutilização de código no nível da organização.

O autor deste livro, Johannes Sametinger, descreve um conjunto de barreiras à reutilização de código, algumas conceituais e outras técnicas. Por exemplo:

Conceitual e Técnico

  • Dificuldade em encontrar software reutilizável : o software não pode ser reutilizado, a menos que possa ser encontrado. É improvável que a reutilização ocorra quando um repositório não possui informações suficientes sobre componentes ou quando os componentes são mal classificados.
  • Não reutilização de software encontrado : o fácil acesso ao software existente não aumenta necessariamente a reutilização do software. Involuntariamente, o software raramente é escrito de uma maneira que os outros possam reutilizá-lo. Modificar e adaptar o software de outra pessoa pode se tornar ainda mais caro do que programar a funcionalidade necessária do zero.
  • Componentes legados não adequados para reutilização : A reutilização de componentes é difícil ou impossível, a menos que eles tenham sido projetados e desenvolvidos para reutilização. Simplesmente reunir componentes existentes de vários sistemas de software legados e tentar reutilizá-los para novos desenvolvimentos não é suficiente para reutilização sistemática. A reengenharia pode ajudar na extração de componentes reutilizáveis, no entanto, o esforço pode ser considerável.
  • Tecnologia orientada a objetos : Acredita-se amplamente que a tecnologia orientada a objetos tenha um impacto positivo na reutilização de software. Infelizmente e de forma errada, muitos também acreditam que a reutilização depende dessa tecnologia ou que a adoção de tecnologia orientada a objetos é suficiente para a reutilização de software.
  • Modificação : os componentes nem sempre serão exatamente da maneira que queremos. Se forem necessárias modificações, poderemos determinar seus efeitos no componente e em seus resultados de verificação anteriores.
  • Reutilização de lixo : a certificação de componentes reutilizáveis ​​para determinados níveis de qualidade ajuda a minimizar possíveis defeitos. Os controles de baixa qualidade são uma das principais barreiras à reutilização. Precisamos de alguns meios para julgar se as funções necessárias correspondem às funções fornecidas por um componente.

Outras dificuldades técnicas básicas incluem

  • Concordando sobre o que constitui um componente reutilizável.
  • Compreendendo o que um componente faz e como usá-lo.
  • Entendendo como fazer a interface de componentes reutilizáveis ​​para o restante de um design.
  • Projetando componentes reutilizáveis ​​para que sejam fáceis de adaptar e modificar de maneira controlada.
  • Organizar um repositório para que os programadores possam encontrar e usar o que precisam.

Segundo o autor, diferentes níveis de reutilização acontecem dependendo da maturidade de uma organização.

  • Reutilização ad-hoc entre grupos de aplicativos : se não houver compromisso explícito com a reutilização, a reutilização poderá ocorrer de uma maneira informal e casual, na melhor das hipóteses. A maior parte da reutilização, se houver, ocorrerá nos projetos. Isso também leva à eliminação do código e acaba na duplicação de código.
  • Reutilização baseada em repositório entre grupos de aplicativos : a situação melhora um pouco quando um repositório de componentes é usado e pode ser acessado por vários grupos de aplicativos. No entanto, não existe um mecanismo explícito para colocar componentes no repositório e ninguém é responsável pela qualidade dos componentes no repositório. Isso pode levar a muitos problemas e dificultar a reutilização de software.
  • Reutilização centralizada com um grupo de componentes: Neste cenário, um grupo de componentes é explicitamente responsável pelo repositório. O grupo determina quais componentes devem ser armazenados no repositório e garante a qualidade desses componentes e a disponibilidade da documentação necessária, além de ajudar a recuperar componentes adequados em um cenário de reutilização específico. Grupos de aplicativos são separados do grupo de componentes, que atua como um tipo de subcontratado para cada grupo de aplicativos. Um objetivo do grupo de componentes é minimizar a redundância. Em alguns modelos, os membros deste grupo também podem trabalhar em projetos específicos. Durante a inicialização do projeto, seu conhecimento é valioso para promover a reutilização e, graças ao envolvimento em um projeto específico, eles podem identificar possíveis candidatos para inclusão no repositório.
  • Reutilização baseada em domínio : a especialização de grupos de componentes equivale a reutilização baseada em domínio. Cada grupo de domínio é responsável pelos componentes em seu domínio, por exemplo, componentes de rede, componentes da interface do usuário, componentes do banco de dados.

Portanto, talvez, além de todas as sugestões fornecidas em outras respostas, você possa trabalhar no design de um programa de reutilização, envolver o gerenciamento, formar um grupo de componentes responsável pela identificação de componentes reutilizáveis, fazendo análises de domínio e definindo um repositório de componentes reutilizáveis ​​que outros desenvolvedores possam facilmente consulte e procure soluções prontas para seus problemas.

edalorzo
fonte
1

Existem 2 soluções possíveis:

Prevenção - Tente ter a melhor documentação possível. Torne todas as funções adequadamente documentadas e fáceis de pesquisar em toda a documentação. Além disso, ao escrever um código, torne óbvio para onde o código deve ir; portanto, é óbvio para onde procurar. A quantidade limite de código "utilitário" é um dos pontos principais disso. Toda vez que ouço "vamos fazer aula de utilidade", meu cabelo sobe e meu sangue congela, porque é obviamente um problema. Sempre tenha uma maneira rápida e fácil de solicitar que as pessoas conheçam a base de código sempre que algum recurso já existir.

Solução - Se a prevenção falhar, você poderá resolver rápida e eficientemente a parte problemática do código. Seu processo de desenvolvimento deve permitir a rápida correção de códigos duplicados. O teste de unidade é perfeito para isso, porque você pode modificar o código com eficiência, sem medo de quebrá-lo. Portanto, se você encontrar duas partes de código semelhantes, abstraí-las para uma função ou classe deve ser fácil com um pouco de refatoração.

Pessoalmente, não acho que a prevenção seja possível. Quanto mais você tenta, mais é problemático encontrar recursos já existentes.

Eufórico
fonte
0

Eu não acho que esse tipo de problema tenha a solução geral. O código duplicado não será criado se os desenvolvedores tiverem disposição suficiente para procurar o código existente. Os desenvolvedores também podem corrigir os problemas no local, se quiserem.

Se o idioma for C / C ++, a mesclagem de duplicação será mais fácil devido à flexibilidade do vínculo (pode-se chamar qualquer externfunção sem informações anteriores). Para Java ou .NET, pode ser necessário criar classes auxiliares e / ou componentes de utilitários.

Normalmente, começo a remoção da duplicação do código existente apenas se os principais erros surgirem das partes duplicadas.

9dan
fonte
0

Esse é um problema típico de um projeto maior, que foi tratado por muitos programadores, que tem contribuído sob algumas vezes muita pressão dos colegas. É muito tentador fazer uma cópia de uma classe e adaptá-la a essa classe específica. No entanto, quando um problema foi encontrado na classe de origem, ele também deve ser resolvido nos seus falecidos, que geralmente são esquecidos.

Existe uma solução para isso e é chamado Generics, que foi introduzido no Java 6. É o equivalente ao C ++ chamado Template. Código do qual a classe exata ainda não é conhecida dentro de uma Classe Genérica. Por favor, verifique o Java Generics e você encontrará toneladas e toneladas de documentação para isso.

Uma boa abordagem é reescrever o código que parece ser copiado / colado em muitos lugares, reescrevendo o primeiro que você precisa, ou seja, corrigir por causa de um determinado bug. Reescreva-o para usar Genéricos e também escreva código de teste muito rigoroso.

Verifique se todos os métodos da classe Generic são invocados. Você também pode introduzir ferramentas de cobertura de código: o código genérico deve ser totalmente cobertura de código, pois será usado em vários locais.

Escreva também o código de teste, ou seja, usando JUnit ou similar para a primeira classe designada que será usada em conjunto com o trecho de código genérico.

Comece a usar o código genérico para a segunda versão (na maioria das vezes) copiada quando todo o código anterior funcionar e for totalmente testado. Você verá que existem algumas linhas de código específicas para essa classe designada. Você pode chamar essas linhas de código em um método protegido abstrato que precisa ser implementado pela classe derivada que usa a classe base Genérica.

Sim, é um trabalho tedioso, mas à medida que você avança, será cada vez melhor eliminar classes semelhantes e substituí-lo por algo que é muito, muito limpo, bem escrito e muito mais fácil de manter.

Eu tive uma situação semelhante em que na classe genérica eventualmente substituímos algo como 6 ou 7 outras classes quase idênticas que eram quase idênticas, mas que foram copiadas e coladas por vários programadores ao longo de um período de tempo.

E sim, sou muito a favor do teste automatizado do código. Custará mais no começo, mas certamente poupará uma quantidade enorme de tempo em geral. E tente obter uma cobertura geral de pelo menos 80% e 100% para o código genérico.

Espero que isso ajude e boa sorte.

André van Kouwen
fonte
0

Na verdade, vou ecoar a opinião menos popular aqui e Gangnussugerir que a duplicação de código nem sempre é prejudicial e às vezes pode ser o mal menor.

Se, por exemplo, você me der a opção de usar:

A) Uma biblioteca de imagens estável (imutável) e minúscula, bem testada , que duplica algumas dúzias de linhas de código matemático trivial para matemática vetorial, como produtos com pontos, lerps e grampos, mas é completamente dissociada de qualquer outra coisa e se desenvolve em uma fração de um segundo.

B) Uma biblioteca de imagens instável (que muda rapidamente) que depende de uma biblioteca matemática épica para evitar as dúzias de linhas de código mencionadas acima, com a biblioteca matemática instável e constantemente recebendo novas atualizações e alterações, e, portanto, a biblioteca de imagens também precisa ser reconstruído se não completamente alterado também. Demora 15 minutos para limpar tudo.

... então, obviamente, deve ser um acéfalo para a maioria das pessoas que A, e realmente precisamente devido à sua pequena duplicação de código, é preferível. A ênfase principal que preciso enfatizar é a parte bem testada . Obviamente, não há nada pior do que ter um código duplicado que nem funciona em primeiro lugar; nesse momento, ele está duplicando bugs.

Mas há também o acoplamento e a estabilidade em que pensar, e alguma duplicação modesta aqui e ali pode servir como um mecanismo de dissociação que também aumenta a estabilidade (natureza imutável) do pacote.

Portanto, minha sugestão será focar mais nos testes e tentar criar algo realmente estável (como imutável, encontrando poucas razões para mudar no futuro) e confiável cujas dependências de fontes externas, se houver alguma, são muito estável, tentando eliminar todas as formas de duplicação na sua base de código. Em um ambiente de equipe grande, o último tende a ser um objetivo impraticável, sem mencionar que ele pode aumentar o acoplamento e a quantidade de código instável que você possui em sua base de código.


fonte
-2

Não esqueça que a duplicação de código nem sempre é prejudicial. Imagine: agora você tem alguma tarefa a ser resolvida em módulos absolutamente diferentes do seu projeto. Agora mesmo é a mesma tarefa.

Pode haver três razões para isso:

  1. Algum tema em torno desta tarefa é o mesmo para os dois módulos. Nesse caso, a duplicação de código é ruim e deve ser liquidada. Seria inteligente criar uma classe ou um módulo para apoiar esse tema e usar seus métodos nos dois módulos.

  2. A tarefa é teórica em termos de seu projeto. Por exemplo, é de física ou matemática, etc. A tarefa existe independentemente no seu projeto. Nesse caso, a duplicação de código é ruim e também deve ser liquidada. Eu criaria uma classe especial para essas funções. E use essa função em qualquer módulo em que você precisar.

  3. Mas em outros casos, a coincidência de tarefas é uma coincidência temporária e nada mais. Seria perigoso acreditar que essas tarefas permanecerão as mesmas durante as alterações do projeto devido à refatoração e até depuração. Nesse caso, seria melhor criar duas mesmas funções / partes de código em lugares diferentes. E as mudanças futuras em um deles não tocarão no outro.

E esse terceiro caso acontece com muita frequência. Se você duplicar "sem saber", é principalmente por esse motivo - não é uma duplicação real!

Portanto, tente mantê-lo limpo quando for realmente necessário e não tenha medo de duplicação, se não for necessário.

Gangnus
fonte
2
code duplication is not always harmfulé um péssimo conselho.
Tulains Córdova
1
Devo me curvar à sua autoridade? Eu coloquei minhas razões aqui. Se eu estiver enganado, mostre onde está o erro. Agora parece sua fraca capacidade de manter a discussão.
Gangnus
3
A duplicação de código é um dos principais problemas no desenvolvimento de software e muitos cientistas e teóricos da computação desenvolveram paradigmas e metodologias apenas para evitar a duplicação de código como a principal fonte de problemas de manutenção no desenvolvimento de software. É como dizer "escrever código ruim nem sempre é ruim", para que qualquer coisa possa ser justificada retoricamente. Talvez você está certo, mas evitando a duplicação de código é um bom demais princípio de viver por forma a incentivar o oposto ..
Tulains Córdova
Eu tenho colocado aqui argumentos. Você não tem. A referência às autoridades não funcionará desde o século XVI. Você não pode garantir que os entendeu corretamente e que são autoridades para mim também.
Gangnus
Você está certo, a duplicação de código não é um dos principais problemas no desenvolvimento de software e não foram desenvolvidos paradigmas e metodologias para evitá-lo.
Tulains Córdova