Como minha equipe pode evitar erros frequentes após a refatoração?


20

Para lhe dar um pouco de conhecimento: eu trabalho para uma empresa com aproximadamente doze desenvolvedores do Ruby on Rails (+/- estagiários). Trabalho remoto é comum. Nosso produto é composto de duas partes: um núcleo bastante gordo e fino para grandes projetos de clientes construídos sobre ele. Projetos de clientes geralmente expandem o núcleo. A substituição dos principais recursos não ocorre. Devo acrescentar que o núcleo tem algumas partes ruins que precisam urgentemente de refatorações. Existem especificações, mas principalmente para os projetos dos clientes. A pior parte do núcleo não foi testada (não como deveria ser ...).

Os desenvolvedores são divididos em duas equipes, trabalhando com um ou dois pedidos para cada sprint. Normalmente, um projeto de cliente está estritamente associado a uma das equipes e OPs.

Agora, nosso problema: com bastante frequência, quebramos as coisas um do outro. Alguém da equipe A expande ou refatora o recurso principal Y, causando erros inesperados em um dos projetos de clientes da equipe B. Principalmente, as mudanças não são anunciadas pelas equipes, portanto os bugs atingem quase sempre inesperados. A equipe B, incluindo o OP, considerou o recurso Y estável e não o testou antes da liberação, sem saber das alterações.

Como se livrar desses problemas? Que tipo de 'técnica de anúncio' você pode me recomendar?


34
A resposta óbvia é TDD .
Mouviciel 12/06

1
Como vem você afirma que "Substituições dos principais recursos não acontece", e, em seguida, o seu problema é que ele não acontecer? Você diferencia em sua equipe entre "recursos principais" e "recursos principais", e como você faz isso? Apenas tentando compreender a situação ...
logC

4
@mouvciel Isso e não usa digitação dinâmica , mas esse conselho em particular chega um pouco tarde demais neste caso.
D

3
Use uma linguagem fortemente tipada como OCaml.
Gaius

@logc Pode ser que eu não estava claro, desculpe. Não substituímos um recurso principal como a própria biblioteca de filtros, mas adicionamos novos filtros às classes que usamos em nossos projetos de clientes. Um cenário comum pode ser que as alterações na biblioteca de filtros destruam os filtros adicionados no projeto do cliente.
SDD64

Respostas:


24

Eu recomendaria ler Working Effective with Legacy Code, de Michael C. Feathers . Explica que você realmente precisa de testes automatizados, como pode adicioná-los facilmente, se ainda não os tiver, e que "código cheira" a refatorar de que maneira.

Além disso, outro problema central na sua situação parece ser a falta de comunicação entre as duas equipes. Qual o tamanho dessas equipes? Eles estão trabalhando em diferentes atrasos?

Quase sempre é uma prática ruim dividir equipes de acordo com sua arquitetura. Por exemplo, uma equipe principal e uma equipe não central. Em vez disso, eu criaria equipes no domínio funcional, mas com vários componentes.


Eu li em "The Mythical Man-Month" que a estrutura de código geralmente segue a estrutura de equipe / organização. Portanto, essa não é realmente uma "má prática", mas apenas o modo como as coisas costumam acontecer.
Marcel

Penso que em " Dinâmica do desenvolvimento de software ", o gerente por trás do Visual C ++ recomenda ter vividamente equipes de recursos; Eu não li "The Mythical Man-Month", @Marcel, mas AFAIK ele lista as más práticas na indústria ...
logC

Marcel, é verdade que é assim que as coisas geralmente acontecem, mas mais e mais equipes fazem diferente, por exemplo, equipes de destaque. Ter equipes baseadas em componentes resulta em falta de comunicação ao trabalhar em recursos entre componentes. Além disso, quase sempre resultará em discussões arquitetônicas não baseadas no objetivo de uma boa arquitetura, mas em pessoas que tentam passar responsabilidades para outras equipes / componentes. Portanto, você obterá a situação descrita pelo autor desta pergunta. Consulte também mountaingoatsoftware.com/blog/the-benefits-of-feature-teams .
Tohnmeister

Bem, até onde eu entendi o OP, ele afirmou que as equipes não são divididas em uma equipe principal e não central. As equipes são divididas "por cliente", que é essencialmente "por domínio funcional". E isso faz parte do problema: como todas as equipes têm permissão para alterar o núcleo comum, as mudanças de uma equipe afetam a outra.
Doc Brown

@DocBrown Você está certo. Cada equipe pode mudar o núcleo. Obviamente, essas mudanças devem ser benéficas para cada projeto. No entanto, eles trabalham em diferentes atrasos. Temos um para cada cliente e um para o núcleo.
SDD64

41

A pior parte do núcleo não foi testada (como deveria ser ...).

Este é o problema. A refatoração eficiente depende muito do conjunto de testes automatizados. Se você não os tiver, os problemas que você está descrevendo começam a aparecer. Isso é especialmente importante se você usar uma linguagem dinâmica como Ruby, onde não há compilador para detectar erros básicos relacionados à passagem de parâmetros para métodos.


10
Isso e refatoração em etapas do bebê e cometer com muita freqüência.
precisa

1
Provavelmente, existem montes de conselhos que poderiam adicionar conselhos aqui, mas tudo se resume a esse ponto. Qualquer que seja a piada dos OPs "como deveria ser", mostrando que eles sabem que é um problema em si, o impacto dos testes com script na refatoração é imenso: se uma aprovação se tornou uma falha, a refatoração não funcionou. Se todos os passes permanecerem aprovados, a refatoração pode ter funcionado (mover falhas nos passes seria obviamente uma vantagem, mas manter todos os passes como passes é mais importante do que um ganho satisfatório; uma mudança que interrompe um teste e corrige cinco pode ser uma melhoria, mas não um refactoring)
Jon Hanna

Eu dei um "+1", mas acho que "testes automatizados" não são a única abordagem para resolver isso. Um controle de qualidade manual melhor, mas sistemático, talvez por uma equipe separada de controle de qualidade, também possa resolver os problemas de qualidade (e provavelmente faz sentido ter testes - automáticos e manuais).
Doc Brown

Um bom ponto, mas se o núcleo e os projetos do cliente são módulos separados (e além disso em uma linguagem dinâmica como Ruby), o núcleo pode alterar um teste e sua implementação associada e interromper um módulo dependente sem falhar em seus próprios testes.
logc 12/06

Como outros comentaram. TDD. Você provavelmente já reconhece que deve ter testes de unidade para o máximo de código possível. Ao escrever testes de unidade apenas por uma questão de desperdício de recursos, quando você começa a refatorar qualquer componente, deve começar com uma extensa escrita de teste antes de tocar no código principal.
Jb510

5

As respostas anteriores que apontam para melhores testes de unidade são boas, mas acho que pode haver questões mais fundamentais a serem abordadas. Você precisa de interfaces claras para acessar o código principal a partir do código dos projetos do cliente. Dessa forma, se você refatorar o código principal sem alterar o comportamento conforme observado nas interfaces , o código da outra equipe não será quebrado. Isso tornará muito mais fácil saber o que pode ser refatorado "com segurança" e o que precisa de uma reformulação, possivelmente quebrando a interface.


Spot on. Testes mais automatizados não trarão nada além de benefícios e vale a pena fazer totalmente, mas não resolverão o problema principal aqui, que é uma falha na comunicação das alterações principais. A dissociação envolvendo as interfaces em torno de recursos importantes será uma grande melhoria.
Bob Tway

5

Outras respostas destacaram pontos importantes (mais testes de unidade, equipes de recursos, interfaces limpas aos componentes principais), mas há um ponto que acho que falta, que é o controle de versão.

Se você congelar o comportamento do seu núcleo executando uma liberação 1 e a colocar em um sistema de gerenciamento de artefatos privado 2 , qualquer projeto do cliente poderá declarar sua dependência da versão principal X e não será interrompido na próxima liberação X + 1 .

A "política de anúncio" se reduz a um arquivo ALTERAÇÕES a cada versão ou a uma reunião de equipe para anunciar todos os recursos de cada nova versão principal.

Além disso, acho que você precisa definir melhor o que é "núcleo" e qual subconjunto é "chave". Você parece (corretamente) evitar fazer muitas alterações nos "componentes principais", mas permite alterações frequentes no "núcleo". Para confiar em algo, você precisa mantê-lo estável; se algo não estiver estável, não o chame de núcleo. Talvez eu possa sugerir chamá-lo de componentes "auxiliares"?

EDIT : Se você seguir as convenções no sistema de versão semântica , qualquer alteração incompatível na API do núcleo deverá ser marcada por uma alteração de versão principal . Ou seja, quando você altera o comportamento do núcleo existente anteriormente ou remove algo, não apenas adiciona algo novo. Com essa convenção, os desenvolvedores sabem que atualizar da versão '1.1' para '1.2' é seguro, mas passar de '1.X' para '2.0' é arriscado e deve ser cuidadosamente revisado.

1: Eu acho que isso é chamado de gema, no mundo do Ruby
2: O equivalente ao Nexus em Java ou PyPI em Python


O "controle de versão" é importante, de fato, mas quando se tenta resolver o problema descrito congelando o núcleo antes de um lançamento, você acaba facilmente com a necessidade de ramificação e fusão sofisticadas. O raciocínio é que, durante uma fase de "compilação de versão" da equipe A, A pode precisar alterar o núcleo (pelo menos para correção de bugs), mas não aceitará alterações no núcleo de outras equipes - portanto, você acaba com um ramo da o núcleo por equipe, a ser mesclado "mais tarde", que é uma forma de dívida técnica. Às vezes, tudo bem, mas muitas vezes adia o problema descrito para um momento posterior.
Doc Brown

@ DocBrown: Eu concordo com você, mas escrevi sob o pressuposto de que todos os desenvolvedores são cooperativos e adultos. Isso não quer dizer que eu não tenha visto o que você descreve . Mas uma parte essencial para tornar um sistema confiável é, assim, buscar a estabilidade. Além disso, se a equipe A precisar alterar X no núcleo e a equipe B precisar alterar X no núcleo, talvez X não pertença ao núcleo; Eu acho que esse é o meu outro ponto. :)
logc 12/06

@DocBrown Sim, aprendemos a usar uma ramificação do núcleo para cada projeto do cliente. Isso causou alguns outros problemas. Por exemplo, não gostamos de 'tocar' os sistemas dos clientes já implantados. Como resultado, eles podem encontrar vários saltos de versão secundários do núcleo usado após cada implantação.
SDD64

@ SDD64: é exatamente o que estou dizendo - não integrar mudanças imediatamente em um núcleo comum também não é solução a longo prazo. O que você precisa é de uma melhor estratégia de teste para o seu núcleo - com testes automáticos e manuais também.
Doc Brown

1
Para constar, não estou defendendo um núcleo separado para cada equipe, nem negando a necessidade de testes - mas um teste central e sua implementação podem mudar ao mesmo tempo, como eu comentei antes . Somente um núcleo congelado, marcado por uma string de liberação ou uma tag de confirmação, pode ser utilizado por um projeto que se baseia nele (excluindo correções de bugs e desde que a política de versão seja sólida).
logc

3

Como outras pessoas disseram, um bom conjunto de testes de unidade não resolve o seu problema: você terá problemas ao mesclar as alterações, mesmo que cada conjunto de testes da equipe seja aprovado.

Mesmo para TDD. Não vejo como isso pode resolver isso.

Sua solução não é técnica. Você precisa definir claramente os limites do "núcleo" e atribuir um papel de "cão de guarda" a alguém, seja ele o desenvolvedor ou arquiteto principal. Quaisquer alterações no núcleo devem passar por esse cão de guarda. Ele é responsável por garantir que todos os resultados de todas as equipes sejam mesclados sem muitos danos colaterais.


Tivemos um "cão de guarda", já que ele escreveu a maior parte do núcleo. Infelizmente, ele também foi responsável pela maioria das partes não testadas. Ele foi representado por YAGNI e foi substituído há meio ano por dois outros caras. Ainda lutamos para refatorar essas 'partes escuras'.
SDD64

2
A idéia é ter um conjunto de testes de unidade para o núcleo , que faz parte do núcleo , com contribuições de todas as equipes, não conjuntos de testes separados para cada equipe.
Doc Brown

2
@ SDD64: você parece confundir "Você ainda não precisa" (o que é uma coisa muito boa) com "Você não precisa limpar seu código (ainda)" - que é um hábito extremamente ruim , e IMHO muito pelo contrário.
Doc Brown

A solução watchdog é realmente, muito abaixo do ideal, IMHO. É como criar um ponto único de falha em seu sistema e, além disso, um processo muito lento, porque envolve uma pessoa e política. Caso contrário, é claro que o TDD pode ajudar com esse problema: cada teste principal é um exemplo para os desenvolvedores de projetos do cliente como o núcleo atual deve ser usado. Mas acho que você deu sua resposta de boa fé ...
logC

@DocBrown: Ok, talvez nossos entendimentos sejam diferentes. Os principais recursos, escritos por ele, são excessivamente complicados para satisfazer até as mais estranhas possibilidades. A maioria deles, nunca encontramos. A complexidade nos atrasa para refatorá-los, por outro lado.
SDD64

2

Como uma correção a longo prazo, você também precisa de uma comunicação melhor e mais oportuna entre as equipes. Cada uma das equipes que alguma vez utilizará, por exemplo, o recurso principal Y, precisa estar envolvida na criação dos casos de teste planejados para o recurso. Esse planejamento, por si só, destacará os diferentes casos de uso inerentes ao recurso Y entre as duas equipes. Depois que o recurso deve funcionar, e os casos de teste são implementados e acordados, é necessária uma alteração adicional no seu esquema de implementação. A equipe que está liberando o recurso é necessária para executar o testcase, não a equipe que está prestes a usá-lo. A tarefa, se houver, que deve causar colisões, é a adição de um novo caso de teste de qualquer uma das equipes. Quando um membro da equipe pensa em um novo aspecto do recurso que não foi testado, eles devem ter a liberdade de adicionar uma caixa de teste que eles verificaram passando em sua própria caixa de proteção. Dessa maneira, as únicas colisões que ocorrerão serão no nível de intenção e devem ser definidas antes que o recurso refatorado seja liberado na natureza.


2

Embora todo sistema precise de conjuntos de testes eficazes (o que significa, entre outras coisas, automação) e, embora esses testes, se usados ​​com eficácia, detectem esses conflitos mais cedo do que são agora, isso não soluciona os problemas subjacentes.

A questão revela pelo menos dois problemas subjacentes: a prática de modificar o "núcleo" para satisfazer os requisitos de clientes individuais e a falha das equipes em se comunicar e coordenar sua intenção de fazer alterações. Nenhuma dessas causas é raiz e você precisará entender por que isso está sendo feito antes que você possa corrigi-lo.

Uma das primeiras coisas a serem determinadas é se os desenvolvedores e os gerentes percebem que há um problema aqui. Se pelo menos alguns o fazem, então você precisa descobrir por que eles acham que não podem fazer nada a respeito ou optam por não fazer. Para aqueles que não o fazem, tente aumentar a capacidade deles de prever como suas ações atuais podem criar problemas futuros ou substituí-los por pessoas que possam. Até que você tenha uma força de trabalho ciente de como as coisas estão dando errado, é improvável que você consiga resolver o problema (e talvez nem mesmo assim, pelo menos a curto prazo).

Pode ser difícil analisar o problema em termos abstratos, pelo menos inicialmente, portanto, concentre-se em um incidente específico que resultou em um problema e tente determinar como isso aconteceu. Como é provável que as pessoas envolvidas sejam defensivas, você precisará estar atento a justificativas egoístas e post-hoc para descobrir o que realmente está acontecendo.

Hesito em mencionar uma possibilidade, porque é muito improvável: os requisitos dos clientes são tão díspares que não há uma comunalidade suficiente para justificar o código principal compartilhado. Nesse caso, você realmente tem vários produtos separados e deve gerenciá-los como tal, e não criar um acoplamento artificial entre eles.


Antes de migrarmos nosso produto do Java para o RoR, fizemos o que você sugeriu. Nós tínhamos um núcleo Java para todos os clientes, mas seus requisitos o quebraram um dia e tivemos que dividi-lo. Durante essa situação, enfrentamos problemas como: 'Cara, o cliente Y tem um recurso básico tão bom. Pena que não podemos portá-lo para o cliente Z, porque seu núcleo é incompatível '. Com o Rails, queremos estritamente uma política de 'um núcleo para todos'. Se for necessário, ainda oferecemos alterações drásticas, mas essas desassociam o cliente de outras atualizações.
SDD64

Apenas ligar para TDD não parece suficiente para mim. Portanto, além da divisão da sugestão principal, eu gosto mais da sua resposta. Infelizmente, o núcleo não está perfeitamente testado, mas isso não resolveria todos os nossos problemas. Adicionar novos recursos básicos para um cliente pode parecer perfeitamente adequado e até dar uma construção ecológica para eles, porque apenas as especificações principais são compartilhadas entre os clientes. Não se percebe o que acontece com todo cliente possível. Então, eu gosto da sua sugestão para descobrir os problemas e conversar sobre o que os causou.
SDD64

1

Todos sabemos que os testes de unidade são o caminho a percorrer. Mas também sabemos que é realista ajustá-los de maneira realista a um núcleo.

Uma técnica específica que pode ser útil para você ao estender a funcionalidade é tentar temporariamente e localmente verificar se a funcionalidade existente não foi alterada. Isso pode ser feito assim:

Pseudo-código original:

def someFunction
   do original stuff
   return result
end

Código de teste temporário no local:

def someFunctionNew
   new do stuff
   return result
end

def someFunctionOld
   do original stuff
   return result
end

def someFunction
   oldResult = someFunctionOld
   newResult = someFunctionNew
   check oldResult = newResult
   return newResult
end

Execute esta versão através dos testes em nível de sistema existentes. Se estiver tudo bem, você sabe que não quebrou as coisas e pode remover o código antigo. Observe que, ao verificar a correspondência de resultados antigos e novos, você também pode adicionar código para analisar diferenças para capturar casos que você sabe que devem ser diferentes devido a uma alteração pretendida, como uma correção de bug.


1

"Principalmente, as mudanças não são anunciadas pelas equipes, então os bugs atingem quase sempre inesperados"

Problema de comunicação alguém? Que tal (além do que todo mundo já apontou, que você deve fazer testes rigorosos), para garantir uma comunicação adequada? Que as pessoas tenham consciência de que a interface para a qual estão escrevendo mudará no próximo lançamento e quais serão essas mudanças?
E conceda a eles acesso a pelo menos uma interface fictícia (com implementação vazia) o mais rápido possível durante o desenvolvimento para que eles possam começar a escrever seu próprio código.

Sem tudo isso, os testes de unidade não farão muito, exceto apontar durante os estágios finais que há algo fora de controle entre partes do sistema. Você quer saber disso, mas quer saber cedo, muito cedo, e fazer com que as equipes conversem entre si, coordenem esforços e tenham acesso frequente ao trabalho que a outra equipe está realizando (cometer regularmente, não uma tarefa maciça confirmar após várias semanas ou meses, 1-2 dias antes da entrega).
Seu bug NÃO está no código, certamente não no código da outra equipe que não sabia que você estava brincando com a interface contra a qual eles estão escrevendo. Seu bug está no seu processo de desenvolvimento, na falta de comunicação e colaboração entre as pessoas. Só porque você está sentado em salas diferentes não significa que você deve se isolar dos outros caras.


1

Principalmente, você tem um problema de comunicação (provavelmente também vinculado a um problema de formação de equipe ), então acho que uma solução para o seu caso deve ser focada em ... bem, comunicação, em vez de técnicas de desenvolvimento.

Eu tenho como certo que não é possível congelar ou bifurcar o módulo principal ao iniciar um projeto do cliente (caso contrário, basta integrar nos agendamentos da sua empresa alguns projetos não relacionados ao cliente que visam atualizar o módulo principal).

Portanto, ficamos com a questão de tentar melhorar a comunicação entre as equipes. Isso pode ser tratado de duas maneiras:

  • com seres humanos. Isso significa que sua empresa designa alguém como o arquiteto do módulo principal (ou qualquer linguagem que seja boa para a alta gerência) que será responsável pela qualidade e disponibilidade do código. Essa pessoa encarnará o núcleo. Assim, ela será compartilhada por todas as equipes e garantirá a sincronização adequada entre elas. Além disso, ela também deve atuar como revisora ​​de código comprometida com o módulo principal para manter sua coerência;
  • com ferramentas e fluxos de trabalho. Ao impor a Integração Contínua ao núcleo, você fará do próprio código do núcleo o meio de comunicação. Isso exigirá algum esforço primeiro (pela adição de conjuntos de testes automatizados), mas os relatórios noturnos de IC serão uma atualização geral do status do módulo principal.

Você pode encontrar mais informações sobre o IC como um processo de comunicação aqui .

Finalmente, você ainda tem um problema com a falta de trabalho em equipe no nível da empresa. Eu não sou um grande fã de eventos de formação de equipes, mas isso parece ser um caso em que eles seriam úteis. Você tem reuniões regulares para todo o desenvolvedor? Você pode convidar pessoas de outras equipes para as retrospectivas do seu projeto? Ou talvez tomar alguma cerveja na sexta à noite?

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.