Como evito refatorações em cascata?

52

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_castatravé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 gque opera sem um contexto, e algumas delas precisam ser feitas como parte do agora diferido f. Mas nem todos os métodos fchamam necessidade ou querem esse contexto - alguns deles precisam de um contexto distinto que obtêm por meios separados. Então, para tudo o que facaba 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 fpara o novo fe 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.

DeadMG
fonte
67
Quando você diz que "refatorou o projeto para adicionar um recurso", o que você quer dizer exatamente? A refatoração não altera o comportamento dos programas, por definição, o que torna essa declaração confusa.
Jules
5
@Jules: Estritamente falando, o recurso era permitir que outros desenvolvedores adicionassem um tipo específico de extensão; portanto, o recurso era o refator, o que tornava a estrutura da classe mais aberta.
DeadMG
5
Eu pensei que isso é discutido em todos os livros e artigos que falam sobre refatoração? O controle da fonte vem para resgatar; se você achar que para executar a etapa A, é necessário executar a etapa B primeiro, depois descarte A e faça B primeiro.
rwong
4
@DeadMG: este é o livro que eu originalmente queria citar no meu primeiro comentário: "O jogo" pick-up sticks "é uma boa metáfora para o Método Mikado. Você elimina a" dívida técnica "- os problemas herdados incorporados em quase todos os softwares sistema - seguindo um conjunto de regras fáceis de implementar. Você extrai cuidadosamente cada dependência entrelaçada até expor o problema central, sem recolher o projeto ".
Rwong
2
Você pode esclarecer sobre qual linguagem de programação estamos falando? Depois de ler todos os seus comentários, chego à conclusão de que você está fazendo isso manualmente, em vez de usar um IDE para ajudá-lo. Assim, gostaria de saber se posso lhe dar alguns conselhos práticos.
thepacker

Respostas:

69

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.

Doc Brown
fonte
53

É apenas um sintoma das minhas aulas anteriores que dependem muito um do outro?

Certo. Uma mudança causando uma infinidade de outras mudanças é praticamente a definição de acoplamento.

Como evito refatores em cascata?

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.

Telastyn
fonte
33
+1 Quanto mais uma refatoração for necessária, mais amplamente a refatoração chegará. É a própria natureza da coisa.
Paul Draper
4
Se você está realmente refatorando , porém, outro código não deve se preocupar com as mudanças imediatamente. (É claro que, eventualmente, você deseja limpar as outras partes ... mas isso não deve ser necessário imediatamente.) Uma alteração que "cascata" pelo restante do aplicativo é maior que a refatoração - nesse ponto, é basicamente um redesenho ou reescrita.
cHao 12/01
+1 Um adaptador é exatamente o caminho para isolar o código que você deseja alterar primeiro.
#
17

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.

Jules
fonte
4
Não vejo como essa situação possa surgir. Para qualquer refatoração razoável da interface de um método, deve haver um novo conjunto de parâmetros facilmente determinado que possa ser transmitido, que resultará no comportamento da chamada igual ao antes da alteração.
Jules
3
Nunca estive em uma situação em que quisesse realizar tal refatoração, mas devo dizer que me parece bastante incomum. Você está dizendo que removeu a funcionalidade da interface? Se sim, para onde foi? Em outra interface? Ou em outro lugar?
Jules
5
A maneira de fazer isso é remover todos os usos do recurso a ser removido antes da refatoração para removê-lo, e não depois. Isso permite que você mantenha a construção do código enquanto trabalha nele.
Jules
11
@DeadMG: isso soa estranho: você está removendo um recurso que não é mais necessário, como você diz. Mas, por outro lado, você escreve "o projeto se torna completamente não funcional" - isso realmente parece que o recurso é absolutamente necessário. Por favor, esclareça.
Doc Brown
26
@DeadMG Nesses casos, você normalmente desenvolver o novo recurso, adicionar testes para garantir que ele funciona, código existente transição para usar a nova interface, e , em seguida, remover o recurso de idade (agora) supérfluo. Dessa forma, não deve haver um ponto em que as coisas quebrem.
sapi
12

Como posso evitar esse tipo de refatoramento em cascata no futuro?

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:

  • Refatore apenas o suficiente para fazer a costura necessária para injetar / implementar o código do novo recurso.
    • A resistência à refatoração não deve impulsionar o novo design.
  • Escreva uma classe voltada para o cliente com uma API que mantenha o novo recurso e os códigos existentes ignorantemente um do outro.
    • Ele translitera para obter objetos, dados e resultados. Princípio de conhecimento mínimo seja condenado. Não faremos nada pior do que o código existente já faz.

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.

radarbob
fonte
9

Do (maravilhoso) livro Trabalhando Efetivamente com o Código Legado, de Michael Feathers :

Quando você quebra dependências no código legado, muitas vezes é necessário suspender um pouco o senso estético. Algumas dependências quebram de maneira limpa; outros acabam parecendo menos do que ideais do ponto de vista do design. Eles são como os pontos de incisão na cirurgia: pode haver uma cicatriz no seu código após o trabalho, mas tudo abaixo dele pode melhorar.

Se mais tarde você puder cobrir o código em torno do ponto em que quebrou as dependências, também poderá curar essa cicatriz.

Kenny Evitt
fonte
6

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.

pjc50
fonte
3

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.

  • Trabalhando efetivamente com o código legado , penas
  • Refatoração , Fowler
  • Desenvolvimento de software orientado a objetos, guiado por testes , Freeman e Pryce
  • Código Limpo , Martin
asthasr
fonte
3
Sua pergunta é: "como evito essa [falha] no futuro?" A resposta é que, mesmo que você tenha "IC" e testes no momento, não os esteja aplicando corretamente. Não tive um erro de compilação que durou mais de dez minutos em anos, porque vejo a compilação como "o primeiro teste de unidade" e, quando está quebrado, eu o corrigo, porque preciso poder ver os testes passando como Estou trabalhando mais no código.
asthasr
6
Se estou refatorando uma interface muito usada, adiciono um calço. Esse calço lida com o padrão, para que as chamadas herdadas continuem funcionando. Eu trabalho na interface por trás do calço e, quando termino, começo a mudar de classe para usar a interface novamente em vez do calço.
asthasr
5
Continuar refatorando apesar da falha na compilação é semelhante ao acerto de contas . É uma técnica de navegação de último recurso . Na refatoração, é possível que uma direção de refatoração esteja totalmente errada, e você já viu o sinal revelador disso (no momento em que pára de compilar, ou seja, voando sem indicadores de velocidade no ar), mas decidiu continuar. Eventualmente, o avião cai do radar. Felizmente, não precisamos de blackbox ou investigadores para refatoração: sempre podemos "restaurar para o último estado conhecido".
Rwong
4
@DeadMG: você escreveu "No meu caso, as chamadas anteriores simplesmente não fazem mais sentido", mas na sua pergunta "uma pequena alteração na interface para acomodá-la". Honestamente, apenas uma dessas duas frases pode ser verdadeira. E, a partir da descrição do seu problema, parece bastante claro que sua alteração na interface definitivamente não foi menor . Você deve realmente pensar muito sobre como tornar sua alteração mais compatível com versões anteriores. Para minha experiência, isso sempre é possível, mas você deve elaborar um bom plano primeiro.
Doc Brown)
3
@DeadMG Nesse caso, acho que o que você está fazendo não pode ser chamado de refatoração, cujo ponto básico é aplicar as alterações de design como uma série de etapas muito simples.
Jules
3

É apenas um sintoma das minhas aulas anteriores que dependem muito um do outro?

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

Como posso evitar esse tipo de refatoração em cascata no futuro?

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:

  1. anote o objetivo que você deseja alcançar em um pedaço de papel

  2. faça a mudança mais simples que o leva nessa direção.

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

  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.

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

  6. escolha uma das tarefas que não possui erros de saída (sem dependências conhecidas) e retorne para 2.

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

Jens Schauder
fonte
2

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.

200_success
fonte
2

... Refatorei o projeto para adicionar o recurso.

Como disse o @Jules, refatorar e adicionar recursos são duas coisas muito diferentes.

  • Refatorar é mudar a estrutura do programa sem alterar seu comportamento.
  • Adicionar um recurso, por outro lado, está aumentando seu comportamento.

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

Eu precisava fazer uma pequena alteração na interface para acomodá-la

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

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.

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. ;)

dagnelies
fonte
-1

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.

Tarik
fonte
11
este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 9 respostas
mosquito
@gnat: Talvez não, mas simplificou as respostas.
Tarik