O que fazer com um arquivo de origem C ++ de 11000 linhas?


229

Portanto, temos esse enorme arquivo de origem mainmodule.cpp (são 11000 linhas?) Em nosso projeto e toda vez que preciso tocá-lo, encolho-me.

Como esse arquivo é tão central e amplo, ele acumula cada vez mais código e não consigo pensar em uma boa maneira de fazê-lo começar a encolher.

O arquivo é usado e alterado ativamente em várias (> 10) versões de manutenção do nosso produto e, portanto, é realmente difícil refatorá-lo. Se eu fosse "simplesmente" dividi-lo, digamos, para começar, em 3 arquivos, então mesclar as alterações das versões de manutenção se tornará um pesadelo. Além disso, se você dividir um arquivo com um histórico tão longo e rico, rastrear e verificar alterações antigas no SCChistórico torna-se repentinamente muito mais difícil.

O arquivo contém basicamente a "classe principal" (expedição e coordenação principal do trabalho interno) do nosso programa, portanto, toda vez que um recurso é adicionado, ele também afeta esse arquivo e toda vez que ele cresce. :-(

o que você faria nesta situação? Alguma idéia de como mover novos recursos para um arquivo de origem separado sem atrapalhar o SCCfluxo de trabalho?

(Observação sobre as ferramentas: usamos C ++ com Visual Studio; usamos AccuRevcomo, SCCmas acho que o tipo de SCCrealmente não importa aqui; usamos Araxis Mergepara comparar e mesclar arquivos de verdade)


15
@BoltClock: Na verdade, o Vim abrirá bastante rápido.
01/09

58
69305 linhas e contando. Um arquivo em nosso aplicativo no qual meu colega despeja a maior parte de seu código. Não resisti a postar isso aqui. Não tenho ninguém na minha empresa para reportar isso.
Agnel Kurian

204
Eu não entendo. Como o comentário "sair desse emprego" recebe tantos votos positivos? Algumas pessoas parecem morar em um país das fadas, onde todos os projetos são escritos do zero e / ou usam 100% ágil, TDD, ... (coloque qualquer uma das suas palavras-chave aqui).
Stefan

39
@ Stefan: Quando confrontado com uma base de código semelhante, eu fiz exatamente isso. Eu não gostava de gastar 95% do meu tempo trabalhando em uma base de código de 10 anos e 5% realmente escrevendo código. Na verdade, era impossível testar alguns aspectos do sistema (e não quero dizer teste de unidade, quero dizer, executar o código para ver se funcionou). Não perdi meu período de teste de seis meses, me cansei de lutar perdendo batalhas e escrevendo código que não podia suportar.
Preocupante Binário 01/09/10

50
em relação ao aspecto de rastreamento de histórico da divisão do arquivo: use o comando copy do sistema de controle de versão para copiar o arquivo inteiro quantas vezes você desejar dividi-lo e remova todo o código de cada uma das cópias que você não deseja nesse arquivo. Isso preserva o histórico geral, pois cada um dos arquivos divididos pode rastrear seu histórico através da divisão (que parecerá uma exclusão gigante da maior parte do conteúdo do arquivo).
Rmeador 1/09/10

Respostas:


86
  1. Encontre um código no arquivo que seja relativamente estável (não mude rapidamente e não varie muito entre as ramificações) e que possa ser uma unidade independente. Mova isso para seu próprio arquivo e, para esse assunto, para sua própria classe, em todas as ramificações. Por ser estável, isso não causará (muitas) mesclagens "estranhas" que precisam ser aplicadas a um arquivo diferente daquele em que foram originalmente criadas, quando você mescla a alteração de uma ramificação para outra. Repetir.

  2. Encontre algum código no arquivo que basicamente se aplique apenas a um pequeno número de ramificações e que possa ser autônomo. Não importa se está mudando rapidamente ou não, devido ao pequeno número de ramificações. Mova isso para suas próprias classes e arquivos. Repetir.

Então, nos livramos do código que é o mesmo em todos os lugares e do código específico para determinadas ramificações.

Isso deixa você com um núcleo de código mal gerenciado - é necessário em qualquer lugar, mas é diferente em todos os ramos (e / ou muda constantemente para que alguns ramos sejam executados atrás de outros), e, no entanto, é em um único arquivo que você está sem êxito, tentando mesclar entre os ramos. Pare de fazer isso. Ramifique o arquivo permanentemente , talvez renomeá-lo em cada ramificação. Não é mais "principal", é "principal para a configuração X". OK, então você perde a capacidade de aplicar a mesma alteração a várias ramificações mesclando, mas esse é o núcleo do código em que a mesclagem não funciona muito bem. Se você tiver que gerenciar manualmente as mesclagens para lidar com conflitos, não há perda em aplicá-las manualmente independentemente em cada ramificação.

Acho que você está errado ao dizer que o tipo de SCC não importa, porque, por exemplo, as habilidades de mesclagem do git são provavelmente melhores do que a ferramenta de mesclagem que você está usando. Portanto, o problema principal, "a fusão é difícil" ocorre em momentos diferentes para diferentes SCCs. No entanto, é improvável que você consiga alterar os SCCs, portanto o problema é provavelmente irrelevante.


Quanto à fusão: observei o GIT e o SVN, o Perforce e deixe-me dizer que nada que vi em qualquer lugar supera o AccuRev + Araxis pelo que fazemos. :-) (Embora o GIT possa fazer isso [ stackoverflow.com/questions/1728922/… ] e o AccuRev não - todos precisam decidir por si próprios se isso faz parte da fusão ou da análise da história.)
Martin Ba,

É justo - talvez você já tenha a melhor ferramenta disponível. A capacidade do Git de mesclar uma alteração que ocorreu no Arquivo A na ramificação X, no Arquivo B na ramificação Y, deve facilitar a divisão de arquivos ramificados, mas presumivelmente o sistema que você usa tem vantagens que você gosta. Enfim, eu não estou propondo mudar para git, apenas dizendo que a SCC faz a diferença aqui, mas mesmo assim eu concordo com você que isso pode ser descontado :-)
Steve Jessop

129

A fusão não será um pesadelo tão grande quanto será quando você obter 30000 arquivos LOC no futuro. Assim:

  1. Pare de adicionar mais código a esse arquivo.
  2. Divida.

Se você não pode simplesmente parar de codificação durante o processo de refatoração, você pode deixar este arquivo grande , como é por um tempo, pelo menos, sem adicionar mais código para isso: uma vez que contém uma "classe principal" você poderia herdar-lo e mantê classe herdada ( es) com funções sobrecarregadas em vários novos arquivos pequenos e bem projetados.


@ Martin: felizmente você não colou seu arquivo aqui, então eu não tenho idéia sobre sua estrutura. Mas a ideia geral é dividi-lo em partes lógicas. Essas partes lógicas podem conter grupos de funções da sua "classe principal" ou você pode dividi-la em várias classes auxiliares.
Kirill V. Lyadvinsky 01/09/10

3
Com 10 versões de manutenção e muitos desenvolvedores ativos, é improvável que o arquivo possa ser congelado por tempo suficiente.
Kobi

9
@ Martin, você tem alguns padrões GOF que resolveriam o problema , uma única Fachada que mapeia as funções do mainmodule.cpp. Em alternativa (eu recomendei abaixo), crie um conjunto de classes de comando que cada mapeie para uma função / recurso de mainmodule.app. (Eu já ampliou esse em minha resposta.)
ocodo

2
Sim, concordo totalmente, em algum momento você deve parar de adicionar código ou, eventualmente, será 30k, 40k, 50k, o módulo principal do kaboom apenas com falha. :-)
Chris

67

Parece-me que você está enfrentando uma série de cheiros de código aqui. Antes de tudo, a classe principal parece violar o princípio de abrir / fechar . Também parece que está lidando com muitas responsabilidades . Devido a isso, eu assumiria que o código é mais frágil do que precisa.

Embora eu possa entender suas preocupações com relação à rastreabilidade após uma refatoração, espero que essa classe seja bastante difícil de manter e aprimorar e que quaisquer alterações que você faça provavelmente causem efeitos colaterais. Eu suporia que o custo destes supera o custo de refatorar a classe.

De qualquer forma, como o cheiro do código só piora com o tempo, pelo menos em algum momento, o custo destes compensará o custo da refatoração. Pela sua descrição, eu assumiria que você passou do ponto de inflexão.

Refatorar isso deve ser feito em pequenas etapas. Se possível, adicione testes automatizados para verificar o comportamento atual antes de refatorar qualquer coisa. Em seguida, escolha pequenas áreas de funcionalidade isolada e extraia-as como tipos para delegar a responsabilidade.

Em qualquer caso, parece um grande projeto, então boa sorte :)


18
Cheira muito: cheira como o anti-padrão Blob está em casa ... en.wikipedia.org/wiki/God_object . Sua refeição favorita é o código de espaguete: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan

@jdehaan: Eu estava tentando ser diplomático sobre isso :)
Brian Rasmussen

+1 De mim também, não ouso tocar em nem mesmo código complexo que escrevi sem testes para cobri-lo.
Danny Thomas

49

A única solução que eu já imaginei para esses problemas segue. O ganho real pelo método descrito é a progressividade das evoluções. Não há revoluções aqui, caso contrário, você estará com problemas muito rapidamente.

Insira uma nova classe cpp acima da classe principal original. Por enquanto, ele basicamente redirecionaria todas as chamadas para a classe principal atual, mas visa tornar a API dessa nova classe o mais clara e sucinta possível.

Feito isso, você tem a possibilidade de adicionar novas funcionalidades em novas classes.

Quanto às funcionalidades existentes, você deve movê-las progressivamente para novas classes à medida que elas se tornam estáveis ​​o suficiente. Você perderá a ajuda do SCC para esse trecho de código, mas não há muito que possa ser feito sobre isso. Basta escolher o momento certo.

Sei que isso não é perfeito, embora espero que ajude, e o processo deve ser adaptado às suas necessidades!

Informação adicional

Observe que o Git é um SCC que pode seguir trechos de código de um arquivo para outro. Ouvi coisas boas sobre isso, para que possa ajudar enquanto você move progressivamente o seu trabalho.

O Git é construído em torno da noção de blobs que, se bem entendi, representam partes de arquivos de código. Mova essas peças em arquivos diferentes e o Git as encontrará, mesmo se você as modificar. Além do vídeo de Linus Torvalds mencionado nos comentários abaixo, não consegui encontrar algo claro sobre isso.


Uma referência sobre como o GIT faz isso / como você faz isso com o GIT seria muito bem-vinda.
Martin Ba

@Martin Git faz isso automaticamente.
Matthew

4
@ Martin: Git faz isso automaticamente - porque não rastreia arquivos, rastreia conteúdo. Na verdade, é mais difícil no git "apenas obter o histórico de um arquivo".
Arafangion

1
@Martin youtube.com/watch?v=4XpnKHJAok8 é uma conversa em que Torvalds fala sobre git. Ele menciona isso mais tarde na conversa.
Matthew

6
@ Martin, olhe para esta pergunta: stackoverflow.com/questions/1728922/…
Benjol

30

Confúcio diz: "o primeiro passo para sair do buraco é parar de cavar o buraco".


25

Deixe-me adivinhar: dez clientes com conjuntos de recursos divergentes e um gerente de vendas que promove a "personalização"? Já trabalhei em produtos como esse antes. Tivemos essencialmente o mesmo problema.

Você reconhece que ter um arquivo enorme é um problema, mas ainda mais problemas são as dez versões que você precisa manter "atual". Isso é manutenção múltipla. O SCC pode facilitar isso, mas não pode torná-lo correto.

Antes de tentar dividir o arquivo em partes, é necessário sincronizar os dez ramos novamente, para que você possa ver e modelar todo o código de uma só vez. Você pode fazer isso uma ramificação por vez, testando as duas ramificações no mesmo arquivo de código principal. Para impor o comportamento personalizado, você pode usar #ifdef e amigos, mas é melhor usar o if / else comum contra constantes definidas. Dessa forma, seu compilador verificará todos os tipos e provavelmente eliminará o código do objeto "morto" de qualquer maneira. No entanto, você pode desativar o aviso sobre código morto.

Uma vez que existe apenas uma versão desse arquivo compartilhada implicitamente por todas as ramificações, é bastante mais fácil iniciar os métodos tradicionais de refatoração.

Os #ifdefs são principalmente melhores para seções em que o código afetado só faz sentido no contexto de outras personalizações por ramificação. Pode-se argumentar que estes também apresentam uma oportunidade para o mesmo esquema de fusão de agências, mas não se torne uma loucura. Um projeto colossal de cada vez, por favor.

No curto prazo, o arquivo parecerá aumentar. Isto está bem. O que você está fazendo é reunir coisas que precisam estar juntas. Posteriormente, você começará a ver áreas claramente iguais, independentemente da versão; estes podem ser deixados sozinhos ou refatorados à vontade. Outras áreas diferem claramente dependendo da versão. Você tem várias opções neste caso. Um método é delegar as diferenças em objetos de estratégia por versão. Outra é derivar versões de clientes de uma classe abstrata comum. Mas nenhuma dessas transformações é possível desde que você tenha dez "dicas" de desenvolvimento em diferentes ramos.


2
Concordo que o objetivo deve ser ter uma versão do software, mas não seria melhor usar arquivos de configuração (tempo de execução) e não compilação custumization tempo
Esben Skov Pedersen

Ou até mesmo "classes de configuração" para a compilação de cada cliente.
tc.

Eu acho que a configuração em tempo de compilação ou em tempo de execução é funcionalmente irrelevante, mas não quero limitar as possibilidades. A configuração em tempo de compilação tem a vantagem de que o cliente não pode invadir um arquivo de configuração para ativar recursos extras, pois coloca toda a configuração na árvore de origem em vez de como código de "objeto de texto" implementável. O outro lado é que você tende a AlternateHardAndSoftLayers se for em tempo de execução.
Ian

22

Não sei se isso resolve o seu problema, mas acho que você deseja fazer é migrar o conteúdo do arquivo para arquivos menores, independentes um do outro (resumidos). O que eu também entendo é que você tem cerca de 10 versões diferentes do software flutuando e precisa suportá-las sem bagunçar as coisas.

Antes de tudo, não como isso ser fácil e se resolver em poucos minutos de brainstorming. As funções vinculadas ao seu arquivo são vitais para o seu aplicativo, e simplesmente cortá-las e migrá-las para outros arquivos não salvarão o seu problema.

Eu acho que você só tem estas opções:

  1. Não migre e fique com o que você tem. Possivelmente, saia do emprego e comece a trabalhar em software sério, com bom design, além disso. A programação extrema nem sempre é a melhor solução se você estiver trabalhando em um projeto de longo prazo com fundos suficientes para sobreviver a uma falha ou duas.

  2. Elabore um layout de como você gostaria que seu arquivo fosse, assim que ele for dividido. Crie os arquivos necessários e integre-os ao seu aplicativo. Renomeie as funções ou sobrecarregue-as para obter um parâmetro adicional (talvez apenas um booleano simples?). Depois de trabalhar no seu código, migre as funções necessárias para o novo arquivo e mapeie as chamadas de função das funções antigas para as novas. Você ainda deve ter seu arquivo principal dessa maneira e ainda poder ver as alterações feitas nele, quando se trata de uma função específica que você sabe exatamente quando foi terceirizado e assim por diante.

  3. Tente convencer seus colegas de trabalho com um bom bolo de que o fluxo de trabalho está superestimado e que você precisa reescrever algumas partes do aplicativo para fazer negócios sérios.


19

Exatamente esse problema é tratado em um dos capítulos do livro "Trabalhando efetivamente com o código herdado" ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).


O informit.com/store/product.aspx?isbn=0131177052 torna possível ver o sumário deste livro (e 2 capítulos de amostra). Quanto tempo dura o capítulo 20? (Só para ter uma idéia de quão útil pode ser.)
Martin Ba

17
Capítulo 20 é de 10.000 linhas longas, mas o autor está trabalhando para fora como dividi-la em pedaços digeríveis ... 8)
Tony Delroy

1
São cerca de 23 páginas, mas com 14 imagens. Eu acho que você deveria entender, você se sentirá muito mais confiante tentando decidir o que fazer.
Emile Vrijdags

Um excelente livro para o problema, mas as recomendações que ele faz (e outras recomendações neste segmento) compartilham um requisito comum: se você deseja refatorar esse arquivo para todas as suas ramificações, a única maneira de fazer isso é congelar o arquivo arquivo para todas as ramificações e faça as alterações estruturais iniciais. Não há maneira de contornar isso. O livro descreve uma abordagem iterativa para extrair subclasses com segurança, sem suporte automático à refatoração, criando métodos duplicados e delegando chamadas, mas tudo isso é discutível se você não pode modificar os arquivos.
Dan Bryant

2
@ Martin, o livro é excelente, mas depende muito do teste, refatoração, ciclo de teste, que pode ser bastante difícil de onde você está agora. Estive em uma situação semelhante e este livro foi o mais útil que encontrei. Tem boas sugestões para o problema feio que você tem. Mas se você não conseguir colocar algum tipo de equipamento de teste em cena, todas as sugestões de refatoração do mundo não o ajudarão.

14

Eu acho que seria melhor criar um conjunto de classes de comando que mapeiam para os pontos de API do mainmodule.cpp.

Uma vez implementados, você precisará refatorar a base de código existente para acessar esses pontos da API por meio das classes de comando; assim que estiver pronto, você poderá refatorar a implementação de cada comando em uma nova estrutura de classes.

Obviamente, com uma única classe de 11 KLOC, o código provavelmente é altamente acoplado e quebradiço, mas a criação de classes de comando individuais ajudará muito mais do que qualquer outra estratégia de proxy / fachada.

Não invejo a tarefa, mas, com o passar do tempo, esse problema só piorará se não for resolvido.

Atualizar

Eu sugeriria que o padrão de comando é preferível a uma fachada.

É preferível manter / organizar muitas classes de comando diferentes em uma fachada (relativamente) monolítica. O mapeamento de uma única fachada em um arquivo de 11 KLOC provavelmente precisará ser dividido em alguns grupos diferentes.

Por que se preocupar em tentar descobrir esses grupos de fachada? Com o padrão Comando, você poderá agrupar e organizar essas pequenas classes organicamente, para ter muito mais flexibilidade.

Obviamente, ambas as opções são melhores que o arquivo único de 11 KLOC e crescente.


+1 uma alternativa à solução que propus, com a mesma idéia: alterar a API para separar o grande problema em pequenos.
Benoît

13

Um conselho importante: não misture refatoração e correções. O que você deseja é uma versão do seu programa idêntica à versão anterior, exceto que o código fonte é diferente.

Uma maneira poderia ser começar a dividir a menor função / parte grande em seu próprio arquivo e depois incluir um cabeçalho (transformando main.cpp em uma lista de #includes, que soa como um cheiro de código em si * Eu não sou um Guru de C ++), mas pelo menos agora está dividido em arquivos).

Você pode tentar mudar todas as versões de manutenção para o "novo" main.cpp ou qualquer que seja sua estrutura. Novamente: Não há outras alterações ou correções de erros, porque rastrear essas coisas é confuso como o inferno.

Outra coisa: por mais que você queira fazer um grande passe para refatorar a coisa toda de uma só vez, você pode morder mais do que pode mastigar. Talvez apenas escolha uma ou duas "partes", coloque-as em todos os lançamentos e adicione mais valor ao seu cliente (afinal, a refatoração não agrega valor direto, portanto, é um custo que deve ser justificado) e depois escolha outra uma ou duas partes.

Obviamente, isso requer alguma disciplina na equipe para realmente usar os arquivos divididos e não apenas adicionar coisas novas ao main.cpp o tempo todo, mas, novamente, tentar fazer um refatorio massivo pode não ser o melhor curso de ação.


1
+1 para fatorar e # incluir novamente. Se você fizesse isso em todas as 10 ramificações (pouco trabalho, mas gerenciável), ainda teria o outro problema: publicar alterações em todas as ramificações, mas esse problema não seria ' t se expandiram (necessariamente). É feio? Sim, ainda é, mas pode trazer um pouco de racionalidade ao problema. Depois de passar vários anos fazendo manutenção e reparos para um produto realmente grande, sei que a manutenção envolve muita dor. No mínimo, aprenda com ele e sirva como um conto de advertência para os outros.
Jay

10

Rofl, isso me lembra meu antigo emprego. Parece que, antes de entrar, tudo estava dentro de um arquivo enorme (também em C ++). Em seguida, eles o dividiram (em pontos completamente aleatórios usando inclusões) em cerca de três (arquivos ainda enormes). A qualidade deste software foi, como você poderia esperar, horrível. O projeto totalizou cerca de 40k LOC. (contendo quase nenhum comentário, mas MUITO código duplicado)

No final, fiz uma reescrita completa do projeto. Comecei refazendo a pior parte do projeto do zero. É claro que eu tinha em mente uma possível (pequena) interface entre essa nova parte e o resto. Então eu inseri essa parte no projeto antigo. Não refatorei o código antigo para criar a interface necessária, apenas o substituí. Depois dei pequenos passos a partir daí, reescrevendo o código antigo.

Devo dizer que isso levou cerca de meio ano e não houve desenvolvimento da antiga base de código além das correções durante esse período.


editar:

O tamanho ficou em cerca de 40k de LOC, mas o novo aplicativo continha muito mais recursos e presumivelmente menos bugs em sua versão inicial do que o software de 8 anos. Um dos motivos da reescrita foi também que precisávamos dos novos recursos e a introdução deles no código antigo era quase impossível.

O software era para um sistema incorporado, uma impressora de etiquetas.

Outro ponto que devo acrescentar é que, em teoria, o projeto era C ++. Mas não era OO, poderia ter sido C. A nova versão era orientada a objetos.


9
Toda vez que ouço "do zero" no tópico sobre refatoração, eu mato um gatinho!
Kugel

Eu estive em uma situação muito parecida, embora o loop principal do programa que eu tive que enfrentar fosse apenas ~ 9000 LOC. E isso já era ruim o suficiente.
AndyUK

8

OK, então, na maior parte das vezes, reescrever a API do código de produção é uma má ideia para começar. Duas coisas precisam acontecer.

Primeiro, você precisa que sua equipe decida congelar o código na versão atual de produção desse arquivo.

Segundo, você precisa pegar esta versão de produção e criar uma ramificação que gerencia as construções usando diretivas de pré-processamento para dividir o arquivo grande. Dividir a compilação usando as diretivas do pré-processador JUST (#ifdefs, #includes, #endifs) é mais fácil do que recodificar a API. É definitivamente mais fácil para seus SLAs e suporte contínuo.

Aqui você pode simplesmente cortar as funções relacionadas a um subsistema específico da classe e colocá-las em um arquivo, como mainloop_foostuff.cpp, e incluí-lo no mainloop.cpp no ​​local correto.

OU

Uma maneira mais demorada, porém robusta, seria criar uma estrutura de dependências internas com duplo indireção na maneira como as coisas são incluídas. Isso permitirá que você divida as coisas e ainda cuide das co-dependências. Observe que essa abordagem requer codificação posicional e, portanto, deve ser associada aos comentários apropriados.

Essa abordagem incluiria componentes que são usados ​​com base em qual variante você está compilando.

A estrutura básica é que seu mainclass.cpp incluirá um novo arquivo chamado MainClassComponents.cpp após um bloco de instruções como o seguinte:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

A estrutura principal do arquivo MainClassComponents.cpp estaria lá para calcular dependências nos subcomponentes como este:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

E agora, para cada componente, você cria um arquivo component_xx.cpp.

Claro que estou usando números, mas você deve usar algo mais lógico com base no seu código.

O uso do pré-processador permite que você divida as coisas sem ter que se preocupar com alterações na API, que é um pesadelo na produção.

Depois que a produção é liquidada, você pode realmente trabalhar no reprojeto.


Parece que os resultados da experiência funcionam, mas inicialmente são dolorosos.
JBRWilkinson

Na verdade, é uma técnica usada nos compiladores Borland C ++ para emular usos estilo Pascal para gerenciar arquivos de cabeçalho. Especialmente quando eles fizeram a porta inicial do seu sistema de janelas com base em texto.
Elf King

8

Bem, eu entendo sua dor :) Eu já participei de alguns desses projetos e não é bonito. Não há uma resposta fácil para isso.

Uma abordagem que pode funcionar para você é começar a adicionar proteções seguras em todas as funções, ou seja, verificar argumentos, condições pré / pós-métodos nos métodos e, eventualmente, adicionar testes de unidade para capturar a funcionalidade atual das fontes. Depois de ter isso, você estará melhor equipado para re-fatorar o código, pois haverá declarações e erros aparecendo alertando você se você esqueceu alguma coisa.

Às vezes, embora haja momentos em que a refatoração pode trazer mais dor do que benefício. Talvez seja melhor deixar o projeto original e em um estado de pseudo manutenção e começar do zero e, em seguida, adicionar gradualmente a funcionalidade da besta.


4

Você não deve se preocupar em reduzir o tamanho do arquivo, mas em reduzir o tamanho da classe. Tudo se resume a quase o mesmo, mas faz com que você olhe para o problema de um ângulo diferente (como sugere @Brian Rasmussen , sua classe parece ter muitas responsabilidades).


Como sempre, gostaria de obter uma explicação para o voto negativo.
Björn Pollex 01/09/10

4

O que você tem é um exemplo clássico de um antipadrão de design conhecido chamado blob . Reserve um tempo para ler o artigo que aponto aqui e talvez você encontre algo útil. Além disso, se esse projeto for tão grande quanto parece, considere algum design para impedir o crescimento em código que você não pode controlar.


4

Esta não é uma resposta para o grande problema, mas uma solução teórica para uma parte específica:

  • Descubra onde você deseja dividir o arquivo grande em subarquivos. Coloque comentários em algum formato especial em cada um desses pontos.

  • Escreva um script bastante trivial que divida o arquivo em subarquivos nesses pontos. (Talvez os comentários especiais tenham incorporado nomes de arquivos que o script possa usar como instruções para dividi-lo.) Ele deve preservar os comentários como parte da divisão.

  • Execute o script. Exclua o arquivo original.

  • Quando você precisar mesclar a partir de uma ramificação, primeiro recrie o arquivo grande concatenando as peças novamente, faça a mesclagem e depois divida novamente.

Além disso, se você deseja preservar o histórico do arquivo SCC, espero que a melhor maneira de fazer isso seja informar ao sistema de controle de origem que os arquivos individuais das peças são cópias do original. Em seguida, ele preservará o histórico das seções que foram mantidas nesse arquivo, embora, é claro, também registre que grandes partes foram "excluídas".


4

Uma maneira de dividi-lo sem muito perigo seria dar uma olhada histórica em todas as mudanças de linha. Existem certas funções que são mais estáveis ​​que outras? Pontos quentes de mudança, se quiser.

Se uma linha não for alterada em alguns anos, você provavelmente poderá movê-la para outro arquivo sem muita preocupação. Eu daria uma olhada na fonte anotada com a última revisão que tocava uma determinada linha e veria se há alguma função que você possa executar.


Eu acho que outros propuseram coisas semelhantes. Isso é curto e direto ao ponto, e acho que esse pode ser um ponto de partida válido para o problema original.
Martin Ba

3

Uau, parece ótimo. Acho que explicar ao seu chefe que você precisa de muito tempo para refatorar a fera vale a pena tentar. Se ele não concorda, desistir é uma opção.

Enfim, o que eu sugiro é basicamente jogar toda a implementação e reagrupá-la em novos módulos, vamos chamar de "serviços globais". O "módulo principal" somente encaminharia para esses serviços e QUALQUER novo código que você escrever os usará em vez do "módulo principal". Isso deve ser possível em um período de tempo razoável (porque é principalmente copiar e colar), você não quebra o código existente e pode fazer uma versão de manutenção por vez. E se você ainda tiver tempo, poderá gastá-lo refatorando todos os módulos antigos dependentes para também usar os serviços globais.


3

Minhas simpatias - no meu trabalho anterior, encontrei uma situação semelhante com um arquivo que era várias vezes maior do que aquele com o qual você tem que lidar. A solução foi:

  1. Escreva um código para testar exaustivamente a função no programa em questão. Parece que você ainda não terá isso em mãos ...
  2. Identifique algum código que possa ser abstraído em uma classe auxiliar / utilitários. Não precisa ser grande, apenas algo que realmente não faz parte da sua classe 'principal'.
  3. Refatore o código identificado em 2. em uma classe separada.
  4. Execute novamente seus testes para garantir que nada ocorra.
  5. Quando tiver tempo, vá para 2. e repita conforme necessário para tornar o código gerenciável.

As classes que você cria na etapa 3. provavelmente irão crescer para absorver mais código apropriado à sua função recém-limpa.

Eu também poderia adicionar:

0: compre o livro de Michael Feathers sobre como trabalhar com código legado

Infelizmente, esse tipo de trabalho é muito comum, mas minha experiência é que existe um grande valor em tornar o código de trabalho, mas horrível, incrementalmente menos horrível, mantendo-o funcionando.


2

Considere maneiras de reescrever o aplicativo inteiro de uma maneira mais sensata. Talvez reescreva uma pequena seção como um protótipo para ver se sua ideia é viável.

Se você identificou uma solução viável, refatorar o aplicativo adequadamente.

Se todas as tentativas de produzir uma arquitetura mais racional falharem, pelo menos você sabe que a solução provavelmente está redefinindo a funcionalidade do programa.


+1 - reescreva-o em seu próprio tempo, caso contrário, alguém pode cuspir o boneco.
Jon Black

2

Meus 0,05 eurocents:

Redesenhe toda a bagunça, divida-a em subsistemas, levando em consideração os requisitos técnicos e de negócios (= muitas trilhas de manutenção paralelas com base de código potencialmente diferente para cada uma; obviamente há uma necessidade de alta modificabilidade, etc.).

Ao dividir em subsistemas, analise os locais que mais mudaram e separe-os das partes imutáveis. Isso deve mostrar os pontos problemáticos. Separe as partes que mais mudam para seus próprios módulos (por exemplo, dll) de forma que a API do módulo possa ser mantida intacta e você não precise interromper o BC o tempo todo. Dessa forma, você pode implantar versões diferentes do módulo para diferentes ramificações de manutenção, se necessário, mantendo o núcleo inalterado.

O redesenho provavelmente precisará ser um projeto separado, tentando fazê-lo em um alvo em movimento não funcionará.

Quanto ao histórico do código fonte, minha opinião: esqueça o novo código. Mas mantenha a história em algum lugar para poder verificá-la, se necessário. Aposto que você não precisará muito disso desde o início.

Você provavelmente precisará obter a adesão da gerência para este projeto. Talvez você possa argumentar com tempo de desenvolvimento mais rápido, menos erros, manutenção mais fácil e menos caos geral. Algo parecido com "Habilite proativamente a viabilidade de manutenção e futuro de nossos ativos críticos de software" :)

É assim que eu começaria a enfrentar o problema pelo menos.


2

Comece adicionando comentários a ele. Com referência a onde as funções são chamadas e se você pode mover as coisas. Isso pode colocar as coisas em movimento. Você realmente precisa avaliar o quão frágil o código o baseia. Em seguida, junte bits comuns de funcionalidade. Pequenas mudanças de cada vez.



2

Algo que eu acho útil fazer (e estou fazendo agora, embora não na escala que você enfrenta), é extrair métodos como classes (refatoração de objeto de método). Os métodos que diferem nas diferentes versões se tornarão classes diferentes que podem ser injetadas em uma base comum para fornecer o comportamento diferente que você precisa.


2

Achei esta frase a parte mais interessante do seu post:

> O arquivo é usado e alterado ativamente em várias (> 10) versões de manutenção do nosso produto e, portanto, é realmente difícil refatorá-lo

Primeiro, eu recomendaria que você usasse um sistema de controle de origem para desenvolver essas versões de manutenção com mais de 10 anos que suportam ramificação.

Segundo, eu criaria dez ramificações (uma para cada uma das suas versões de manutenção).

Já posso sentir você se encolhendo! Mas o controle de origem não está funcionando para a sua situação devido à falta de recursos ou não está sendo usado corretamente.

Agora, na filial em que você trabalha - refatore-o como achar melhor, seguro com o conhecimento de que você não perturbará os outros nove ramos do seu produto.

Eu ficaria um pouco preocupado que você tenha muito em sua função main ().

Em qualquer projeto que eu escreva, eu usaria main () apenas para executar a inicialização de objetos principais - como um objeto de simulação ou aplicativo - nessas classes é onde o trabalho real deve continuar.

Eu também inicializaria um objeto de log de aplicativo principal para uso global em todo o programa.

Finalmente, também adiciono o código de detecção de vazamento nos blocos de pré-processador, para garantir que ele seja ativado apenas nas compilações DEBUG. Isso é tudo que eu acrescentaria ao main (). Main () deve ser curto!

Você diz que

> O arquivo contém basicamente a "classe principal" (expedição e coordenação principal do trabalho interno) do nosso programa

Parece que essas duas tarefas podem ser divididas em dois objetos separados - um coordenador e um despachante.

Quando você os divide, você pode atrapalhar o seu "fluxo de trabalho do SCC", mas parece que aderir estritamente ao seu fluxo de trabalho do SCC está causando problemas de manutenção do software. Abandone-o agora e não olhe para trás, porque assim que você o consertar, você começará a dormir tranquilamente.

Se você não for capaz de tomar a decisão, lute com unhas e dentes com seu gerente - a sua aplicação precisa ser refatorada - e muito pelo que parece! Não aceite não como resposta!


Pelo que entendi, o problema é o seguinte: se você morder a bala e refatorar, não poderá mais carregar patches entre as versões. O SCC pode estar perfeitamente configurado.
peterchen

@ Peterchen - exatamente o problema. Os SCCs são mesclados no nível do arquivo. (Mesclagem de três vias) Se você mover o código entre arquivos, terá que começar a mexer manualmente os blocos de código modificado de um arquivo para outro. (O recurso alguém GIT pessoa mencionados em outro comentário é apenas bom para a história, não para a fusão, tanto quanto eu posso dizer)
Martin Ba

2

Como você descreveu, o principal problema é diferenciar pré-divisão versus pós-divisão, mesclando correções de bugs etc. Ferramenta para contorná-la. Não demorará muito tempo para codificar um script em Perl, Ruby etc. para eliminar a maior parte do ruído da pré-divisão diferente contra uma concatenação da pós-divisão. Faça o que for mais fácil em termos de manipulação de ruído:

  • remova certas linhas antes / durante a concatenação (por exemplo, inclua guardas)
  • remova outras coisas da saída diff, se necessário

Você pode fazer isso sempre que houver um check-in, a concatenação é executada e você tem algo preparado para diferir das versões de arquivo único.


2
  1. Nunca toque neste arquivo e no código novamente!
  2. Tratar é como algo que você está preso. Comece a escrever adaptadores para a funcionalidade codificada lá.
  3. Escreva um novo código em unidades diferentes e fale apenas com adaptadores que encapsulam a funcionalidade do monstro.
  4. ... se apenas uma das opções acima não for possível, saia do emprego e obtenha um novo.

2
+/- 0 - sério, onde vocês moram que recomendariam sair de um emprego com base em detalhes técnicos como este?
Martin Ba

1

"O arquivo contém basicamente a" classe principal "(expedição e coordenação principal do trabalho interno) do nosso programa, portanto, toda vez que um recurso é adicionado, ele também afeta esse arquivo e toda vez que cresce."

Se esse grande SWITCH (que eu acho que existe) se tornar o principal problema de manutenção, você poderá refatorá-lo para usar o dicionário e o padrão de comando e remover toda a lógica do switch do código existente para o carregador, que preenche esse mapa, ou seja:

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();

2
Não, na verdade não existe um grande interruptor. A sentença é apenas o mais próximo que posso chegar para descrever essa bagunça :)
Martin Ba

1

Eu acho que a maneira mais fácil de acompanhar o histórico da fonte ao dividir um arquivo seria algo como isto:

  1. Faça cópias do código-fonte original, usando os comandos de cópia que preservam o histórico que seu sistema SCM fornece. Você provavelmente precisará enviar a essa altura, mas ainda não há necessidade de informar o sistema de compilação sobre os novos arquivos, portanto tudo deve estar bem.
  2. Exclua o código dessas cópias. Isso não deve quebrar a história das linhas que você mantém.

"usando os comandos de cópia que preservam a história que seu sistema SCM fornece" ... coisa ruim que não fornece nenhum
Martin Ba

Que pena. Só isso parece uma boa razão para mudar para algo mais moderno. :-)
Christopher Creutzig

1

Eu acho que o que eu faria nessa situação é um pouco complicado e:

  1. Descobrir como eu queria dividir o arquivo (com base na versão atual de desenvolvimento)
  2. Coloque um bloqueio administrativo no arquivo ("Ninguém toca em mainmodule.cpp após as 17:00 de sexta-feira !!!"
  3. Passe o fim de semana prolongado aplicando essa alteração nas> 10 versões de manutenção (da mais antiga para a mais recente), incluindo a versão atual.
  4. Exclua o mainmodule.cpp de todas as versões suportadas do software. É uma nova era - não há mais mainmodule.cpp.
  5. Convença o gerenciamento de que você não deve oferecer suporte a mais de uma versão de manutenção do software (pelo menos sem um grande contrato de suporte $$$). Se cada um de seus clientes tiver sua própria versão exclusiva .... yeeeeeshhhh. Eu estaria adicionando diretivas do compilador em vez de tentar manter mais de 10 garfos.

O rastreamento de alterações antigas no arquivo é simplesmente resolvido pelo seu primeiro comentário no check-in, dizendo algo como "split from mainmodule.cpp". Se você precisar voltar a algo recente, a maioria das pessoas lembrará da mudança; se daqui a dois anos, o comentário dirá a eles onde procurar. Obviamente, qual será o valor de voltar mais de dois anos para ver quem mudou o código e por quê?

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.