Eu tenho um projeto. Neste projeto, eu queria refatorá-lo para adicionar um recurso, e refatorei o projeto para adicionar o recurso.
O problema é que, quando terminei, era necessário fazer uma pequena alteração na interface para acomodá-la. Então eu fiz a mudança. E então a classe consumidora não pode ser implementada com sua interface atual em termos da nova, por isso também precisa de uma nova interface. Agora, três meses depois, eu tive que corrigir inúmeros problemas virtualmente não relacionados, e estou tentando resolver problemas que foram roteados para daqui a um ano ou simplesmente listados como não serão corrigidos devido à dificuldade antes que a coisa seja compilada novamente.
Como posso evitar esse tipo de refatoração em cascata no futuro? É apenas um sintoma das minhas aulas anteriores que dependem muito um do outro?
Edição resumida: nesse caso, o refator era o recurso, pois o refator aumentava a extensibilidade de um pedaço de código específico e diminuía algum acoplamento. Isso significava que os desenvolvedores externos poderiam fazer mais, que era o recurso que eu queria oferecer. Portanto, o refator original em si não deveria ter sido uma mudança funcional.
Edição maior que prometi há cinco dias:
Antes de iniciar esse refator, eu tinha um sistema em que tinha uma interface, mas na implementação, simplesmente dynamic_cast
através de todas as implementações possíveis que eu enviei. Obviamente, isso significava que você não poderia simplesmente herdar da interface, por um lado, e em segundo lugar, que seria impossível para qualquer pessoa sem acesso à implementação implementar essa interface. Então, decidi que queria corrigir esse problema e abrir a interface para consumo público, para que qualquer pessoa pudesse implementá-lo e que a implementação da interface fosse o contrato inteiro necessário - obviamente uma melhoria.
Quando eu estava encontrando e matando com fogo todos os lugares que tinha feito isso, encontrei um lugar que provou ser um problema específico. Dependia dos detalhes de implementação de todas as várias classes derivadas e da funcionalidade duplicada que já estava implementada, mas melhor em outro lugar. Poderia ter sido implementado em termos da interface pública e reutilizado a implementação existente dessa funcionalidade. Descobri que era necessário um determinado contexto para funcionar corretamente. Grosso modo, a implementação anterior de chamada parecia um pouco com
for(auto&& a : as) {
f(a);
}
No entanto, para obter esse contexto, eu precisava transformá-lo em algo mais como
std::vector<Context> contexts;
for(auto&& a : as)
contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
f(con);
Isso significa que, para todas as operações que costumavam fazer parte f
, algumas delas precisam fazer parte da nova função g
que opera sem um contexto, e algumas delas precisam ser feitas como parte do agora diferido f
. Mas nem todos os métodos f
chamam necessidade ou querem esse contexto - alguns deles precisam de um contexto distinto que obtêm por meios separados. Então, para tudo o que f
acaba chamando (que é, grosso modo, praticamente tudo ), eu tive que determinar o contexto necessário, se houver, de onde eles deveriam obtê-lo e como separá-los do antigo f
para o novo f
e o novo. g
.
E foi assim que acabei onde estou agora. A única razão pela qual eu continuei foi porque eu precisava dessa refatoração por outras razões de qualquer maneira.
Respostas:
Na última vez em que tentei iniciar uma refatoração com consequências imprevisíveis e não consegui estabilizar a compilação e / ou os testes após um dia , desisti e reverti a base de código ao ponto anterior à refatoração.
Então, comecei a analisar o que deu errado e desenvolvi um plano melhor de como fazer a refatoração em etapas menores. Portanto, meu conselho para evitar refatorações em cascata é apenas: saiba quando parar , não deixe que as coisas fiquem fora de seu controle!
Às vezes você tem que morder a bala e jogar fora um dia inteiro de trabalho - definitivamente mais fácil do que jogar fora três meses de trabalho. O dia que você perde não é completamente em vão, pelo menos você aprendeu como não abordar o problema. E, de acordo com a minha experiência, sempre há possibilidades de dar passos menores na refatoração.
Nota lateral : você parece estar em uma situação em que precisa decidir se está disposto a sacrificar três meses completos de trabalho e começar de novo com um novo (e esperançosamente mais bem-sucedido) plano de refatoração. Eu posso imaginar que não é uma decisão fácil, mas pergunte-se, qual é o risco de que você precisa por mais três meses, não apenas para estabilizar a compilação, mas também para corrigir todos os erros imprevistos que você provavelmente introduziu durante sua reescrita que fez nos últimos três meses ? Escrevi "reescrever", porque acho que foi isso que você realmente fez, não uma "refatoração". Não é improvável que você possa resolver seu problema atual mais rapidamente, retornando à última revisão em que seu projeto é compilado e iniciando com uma refatoração real (em vez de "reescrever") novamente.
fonte
Certo. Uma mudança causando uma infinidade de outras mudanças é praticamente a definição de acoplamento.
No pior tipo de base de código, uma única alteração continuará em cascata, fazendo com que você mude (quase) tudo. Parte de qualquer refator em que haja acoplamento generalizado é isolar a parte em que você está trabalhando. Você precisa refatorar não apenas onde seu novo recurso está tocando nesse código, mas onde todo o resto toca nesse código.
Normalmente, isso significa fazer alguns adaptadores para ajudar o código antigo a trabalhar com algo que se parece e age como o código antigo, mas usa a nova implementação / interface. Afinal, se tudo o que você faz é alterar a interface / implementação, mas deixar o acoplamento, você não está ganhando nada. É batom em um porco.
fonte
Parece que sua refatoração era muito ambiciosa. Uma refatoração deve ser aplicada em pequenas etapas, cada uma das quais pode ser concluída em (digamos) 30 minutos - ou, na pior das hipóteses, no máximo por dia - e deixa o projeto montável e todos os testes ainda estão passando.
Se você mantiver cada alteração individual mínima, realmente não deve ser possível para uma refatoração interromper sua construção por um longo tempo. O pior caso é provavelmente alterar os parâmetros para um método em uma interface amplamente usada, por exemplo, para adicionar um novo parâmetro. Mas as alterações consequentes disso são mecânicas: adicionando (e ignorando) o parâmetro em cada implementação e adicionando um valor padrão em cada chamada. Mesmo que haja centenas de referências, não deve demorar nem um dia para realizar essa refatoração.
fonte
Wishful Thinking Design
O objetivo é excelente design e implementação de OO para o novo recurso. Evitar a refatoração também é um objetivo.
Comece do zero e faça um design para o novo recurso que você deseja ter. Aproveite o tempo para fazê-lo bem.
Observe, no entanto, que a chave aqui é "adicionar um recurso". Coisas novas tendem a nos deixar ignorar amplamente a estrutura atual da base de código. Nosso design de pensamento positivo é independente. Mas precisamos de mais duas coisas:
Heurísticas, lições aprendidas, etc.
A refatoração foi tão simples quanto adicionar um parâmetro padrão a uma chamada de método existente; ou uma única chamada para um método de classe estática.
Os métodos de extensão nas classes existentes podem ajudar a manter a qualidade do novo design com um risco mínimo absoluto.
"Estrutura" é tudo. Estrutura é a realização do Princípio da Responsabilidade Única; design que facilita a funcionalidade. O código permanecerá curto e simples por toda a hierarquia de classes. O tempo para o novo design é compensado durante o teste, o retrabalho e a prevenção de hackers na selva de código herdada.
As aulas de pensamento positivo se concentram na tarefa em questão. Geralmente, esqueça de estender uma classe existente - você está apenas induzindo a cascata de refatores novamente e tendo que lidar com a sobrecarga da classe "mais pesada".
Limpe quaisquer restos dessa nova funcionalidade do código existente. Aqui, a funcionalidade de novos recursos completa e bem encapsulada é mais importante do que evitar a refatoração.
fonte
Do (maravilhoso) livro Trabalhando Efetivamente com o Código Legado, de Michael Feathers :
fonte
Parece que (especialmente a partir das discussões nos comentários) você se envolveu com regras auto-impostas, o que significa que essa mudança "menor" é a mesma quantidade de trabalho que uma reescrita completa do software.
A solução tem que ser "não faça isso, então" . É o que acontece em projetos reais. Muitas APIs antigas têm interfaces feias ou parâmetros abandonados (sempre nulos) como resultado, ou funções denominadas DoThisThing2 (), que faz o mesmo que DoThisThing () com uma lista de parâmetros totalmente diferente. Outros truques comuns incluem esconder informações em globals ou ponteiros marcados, a fim de contrabandear essas informações após uma grande parte da estrutura. (Por exemplo, eu tenho um projeto em que metade dos buffers de áudio contém apenas um valor mágico de 4 bytes, porque isso foi muito mais fácil do que alterar a maneira como uma biblioteca invocava seus codecs de áudio.)
É difícil dar conselhos específicos sem código específico.
fonte
Testes automatizados. Você não precisa ser um fanático por TDD, nem precisa de 100% de cobertura, mas testes automatizados são o que permite fazer alterações com confiança. Além disso, parece que você tem um design com acoplamento muito alto; você deve ler sobre os princípios do SOLID, formulados especificamente para abordar esse tipo de problema no design de software.
Eu também recomendaria esses livros.
fonte
Provavelmente sim. Embora você possa obter efeitos semelhantes com uma base de código bastante agradável e limpa quando os requisitos mudarem o suficiente
Além de parar para trabalhar no código legado, você não pode ter medo. Mas o que você pode usar é um método que evita o efeito de não ter uma base de código funcional por dias, semanas ou até meses.
Esse método é chamado "Método Mikado" e funciona assim:
anote o objetivo que você deseja alcançar em um pedaço de papel
faça a mudança mais simples que o leva nessa direção.
verifique se funciona usando o compilador e sua suíte de testes. Se continuar com a etapa 7. caso contrário, continue com a etapa 4.
em seu artigo, observe as coisas que precisam ser alteradas para que sua alteração atual funcione. Desenhe setas, da sua tarefa atual, para as novas.
Reverta suas alterações Este é o passo importante. É contra-intuitivo e machuca fisicamente no começo, mas desde que você tentou uma coisa simples, não é tão ruim assim.
escolha uma das tarefas que não possui erros de saída (sem dependências conhecidas) e retorne para 2.
confirmar a alteração, riscar a tarefa no papel, escolher uma tarefa que não tenha erros de saída (sem dependências conhecidas) e retorne para 2.
Dessa forma, você terá uma base de código funcional em intervalos curtos. Onde você também pode mesclar alterações do resto da equipe. E você tem uma representação visual do que sabe que ainda precisa fazer, isso ajuda a decidir se você deseja continuar com o endevour ou se deve interrompê-lo.
fonte
A refatoração é uma disciplina estruturada, diferente da limpeza do código que você achar melhor. Você precisa ter testes de unidade escritos antes de iniciar, e cada etapa deve consistir em uma transformação específica que você sabe que não deve fazer alterações na funcionalidade. Os testes de unidade devem passar após cada alteração.
Obviamente, durante o processo de refatoração, você descobrirá naturalmente as mudanças que devem ser aplicadas que podem causar falhas. Nesse caso, tente o seu melhor para implementar um calço de compatibilidade para a interface antiga que usa a nova estrutura. Em teoria, o sistema ainda deve funcionar como antes, e os testes de unidade devem passar. Você pode marcar o calço de compatibilidade como uma interface obsoleta e limpá-lo em um momento mais adequado.
fonte
Como disse o @Jules, refatorar e adicionar recursos são duas coisas muito diferentes.
... mas, de fato, às vezes você precisa alterar o funcionamento interno para adicionar suas coisas, mas prefiro chamar isso de modificação do que de refatoração.
É aí que as coisas ficam confusas. As interfaces são consideradas limites para isolar a implementação de como é usada. Assim que você tocar nas interfaces, tudo em ambos os lados (implementando ou usando) também precisará ser alterado. Isso pode se espalhar até onde você o experimentou.
O fato de uma interface exigir uma mudança parece bom ... o fato de ela se espalhar para outra implica em mudanças ainda maiores. Parece que alguma forma de entrada / dados requer que flua pela cadeia. É esse o caso?
Sua palestra é muito abstrata, por isso é difícil descobrir. Um exemplo seria muito útil. Normalmente, as interfaces devem ser bem estáveis e independentes uma da outra, possibilitando modificar parte do sistema sem prejudicar o resto ... graças às interfaces.
... na verdade, a melhor maneira de evitar modificações de código em cascata são precisamente boas interfaces. ;)
fonte
Eu acho que você geralmente não pode, a menos que esteja disposto a manter as coisas como estão. No entanto, quando situações como a sua, acho que o melhor é informar a equipe e informá-la por que deve haver alguma refatoração para continuar o desenvolvimento mais saudável. Eu não iria apenas consertar as coisas sozinho. Eu falava sobre isso durante as reuniões do Scrum (supondo que vocês as tenham) e a abordava sistematicamente com outros desenvolvedores.
fonte