Pergunta

Tenho acostumado a fazer algumas refatorações introduzindo erros de compilação.Por exemplo, se eu quiser remover um campo da minha classe e torná-lo um parâmetro para alguns métodos, normalmente removo o campo primeiro, o que causa um erro de compilação para a classe.Então eu introduziria o parâmetro em meus métodos, o que interromperia os chamadores.E assim por diante.Isso geralmente me dava uma sensação de segurança.Na verdade, ainda não li nenhum livro sobre refatoração, mas costumava pensar que essa é uma maneira relativamente segura de fazer isso.Mas eu me pergunto: é realmente seguro?Ou é uma maneira ruim de fazer as coisas?

Foi útil?

Solução

Esta é uma técnica comum e útil para idiomas estaticamente compilados. A versão geral do que você está fazendo pode ser declarada da seguinte maneira:

Quando você está fazendo uma alteração em um módulo que pode invalidar alguns usos nos clientes desse módulo, faça a alteração inicial de forma a causar um erro de tempo de compilação.

Há uma variedade de corolários:

  • Se o significado de um método, função ou procedimento mudar, e o tipo também não mudar, altere o nome. (Quando você terá examinado e corrigido todos os usos, provavelmente mudará o nome de volta.)

  • Se você adicionar um novo caso a um tipo de dados ou um novo literal a uma enumeração, altere os nomes de todos os construtores de dados existentes ou literais de enumeração. (Ou, se você tiver a sorte de ter um compilador que verifique se a análise de casos é exaustiva, existem maneiras mais fáceis.)

  • Se você está trabalhando em um idioma com sobrecarga, não Basta alterar uma variante ou adicionar uma nova variante. Você corre o risco de ter a sobrecarga resolver silenciosamente de uma maneira diferente. Se você usar sobrecarga, é muito difícil fazer com que o compilador funcione para você da maneira que você espera. A única maneira que conheço para lidar com a sobrecarga é raciocinar globalmente sobre todos os usos. Se o seu IDE não o ajudará, você deve mudar os nomes de tudo variantes sobrecarregadas. Desagradável.

O que você realmente está fazendo é usar o compilador para ajudá -lo a examinar todos os lugares do código que você pode precisar alterar.

Outras dicas

Eu nunca confio na compilação simples ao refatorar, o código pode compilar, mas os bugs podem ter sido introduzidos.

Eu acho que apenas escrever alguns testes de unidade para os métodos ou classes que você deseja refatorar será o melhor e, ao executar o teste após a refatoração, você terá certeza de que nenhum erro foi introduzido.

Não estou dizendo que vá para o desenvolvimento orientado a testar, apenas escreva os testes de unidade para obter a confiança necessária que você precisa para refatorar.

Não vejo nenhum problema com isso. É seguro e, desde que você não esteja cometendo mudanças antes de compilar, isso não tem efeito a longo prazo. Além disso, o Resharper e o VS possuem ferramentas que facilitam o processo um pouco para você.

Você usa um processo semelhante na outra direção no TDD - você escreve código que pode não ter os métodos definidos, o que faz com que ele não compile, então você escreve código suficiente para compilar (depois passa os testes e assim por diante ...)

Quando você está pronto para ler livros sobre o assunto, eu recomendo a de Michael Feather "Trabalhando efetivamente com o código legado". (Adicionado por não-autor: também o livro clássico de Fowler "Reestruturação" - e a Reestruturação O site pode ser útil.)

Ele fala sobre identificar as características do código que você está trabalhando antes de fazer uma alteração e fazer o que ele chama de riscos de refatoração. Isso é refletir para encontrar características do código e depois jogar fora os resultados.

O que você está fazendo é usar o compilador como um teste automático. Ele testará que seu código compila, mas não se o comportamento tiver mudado devido à sua refatoração ou se houve algum efeito colateral.

Considere isto

class myClass {
     void megaMethod() 
     {
         int x,y,z;
         //lots of lines of code
         z = mysideEffect(x)+y;
         //lots more lines of code 
         a = b + c;
     }
}

você pode refatorar a adição

class myClass {
     void megaMethod() 
     {
         int a,b,c,x,y,z;
         //lots of lines of code
         z = addition(x,y);
         //lots more lines of code
         a = addition(b,c);  
     }

     int addition(int a, b)
     {
          return mysideaffect(a)+b;
     }
}

E isso funcionaria, mas o segundo additon estaria errado ao invocar o método. Outros testes seriam necessários além de apenas compilação.

É fácil pensar em exemplos em que a refatoração por erros do compilador falha silenciosamente e produz resultados não intencionais.
Alguns casos que vêm à mente: (vou assumir que estamos falando sobre C ++)

  • Alterando argumentos para uma função em que outras sobrecargas existem com parâmetros padrão. Após a refatoração, a melhor combinação para os argumentos pode não ser o que você espera.
  • Classes que lançaram operadores ou construtores de argumento único não explícitos. Mudar, adicionar, remover ou alterar argumentos para qualquer um deles pode alterar as melhores correspondências que são chamadas, dependendo da constelação envolvida.
  • Alterar uma função virtual e sem alterar a classe base (ou alterar a classe base de maneira diferente) resultará em chamadas direcionadas para a classe base.

Contar com os erros do compilador deve ser usado apenas se você tiver certeza de que o compilador captura todas as alterações que precisam ser feitas. Eu quase sempre tendem a suspeitas sobre isso.

Gostaria apenas de acrescentar a toda a sabedoria por aqui que há mais um caso em que isso pode não ser seguro.Reflexão.Isso afeta ambientes como .NET e Java (e outros também, é claro).Seu código seria compilado, mas ainda haveria erros de tempo de execução quando o Reflection tentasse acessar variáveis ​​inexistentes.Por exemplo, isso pode ser bastante comum se você usar um ORM como o Hibernate e esquecer de atualizar seus arquivos XML de mapeamento.

Pode ser um pouco mais seguro pesquisar em todos os arquivos de código o nome específico da variável/método.É claro que isso pode trazer muitos falsos positivos, por isso não é uma solução universal;e você também pode usar concatenação de strings em sua reflexão, o que também tornaria isso inútil.Mas está pelo menos um pequeno passo mais perto da segurança.

Não acho que exista um método 100% infalível, além de passar por todo o código manualmente.

É uma maneira bastante comum, como encontra todas as referências a essa coisa específica. No entanto, os IDEs modernos, como o Visual Studio, encontraram todas as referências, o que torna isso desnecessário.

No entanto, existem algumas desvantagens nessa abordagem. Para grandes projetos, pode levar muito tempo para compilar o aplicativo. Além disso, não faça isso por um longo tempo (quero dizer, coloque as coisas de volta trabalhando o mais rápido possível) e não faça isso por mais de uma coisa de cada vez, pois você pode esquecer da maneira correta de consertar as coisas a primeira vez.

É uma maneira e não pode haver afirmação definitiva sobre se é seguro ou inseguro sem saber como é o código que você está refatorando e as escolhas que você faz.

Se estiver funcionando para você, não há motivo para mudar apenas para trocar o saquê, mas quando você tem tempo, lendo, os recursos aqui podem fornecer novas idéias que você pode querer explorar com o tempo.

http://www.refactoring.com/

Outra opção que você pode considerar, se estiver usando uma das linguagens dotnet, é sinalizar o método "antigo" com o atributo Obsolete, que apresentará todos os avisos do compilador, mas ainda deixará o código chamável caso haja código além de você controle (se você estiver escrevendo uma API ou se não usar a opção strict em VB.Net, por exemplo).Você pode refatorar alegremente, fazendo com que a versão obsoleta chame a nova;como um exemplo:

    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    public int Login()
    {
        /* do stuff */
    }

Torna-se:

    [ObsoleteAttribute()]
    public string Username
    {
        get
        {
            return this.userField;
        }
        set
        {
            this.userField = value;
        }
    }

    [ObsoleteAttribute("Replaced by Login(username, password)")]
    public int Login()
    {
        Login(Username, Pasword);
    }

    public int Login(string username, string password)
    {
        /* do stuff */
    }

É assim que costumo fazer, de qualquer maneira...

Essa é uma abordagem comum, mas os resultados podem variar dependendo se o seu idioma é estático ou dinâmico.

Em uma linguagem tiped estaticamente, essa abordagem faz algum sentido, pois quaisquer discrepâncias que você apresenta serão capturadas no horário de compilação. No entanto, os idiomas dinâmicos geralmente encontram apenas esses problemas em tempo de execução. Essas questões não seriam capturadas pelo compilador, mas pelo seu conjunto de testes; Supondo que você escreveu um.

Tenho a impressão de que você está trabalhando com um idioma estático como C# ou Java, então continue com essa abordagem até encontrar algum tipo de problema importante que diz que deve fazer o contrário.

Faço refatores usuais, mas ainda faço refatores, introduzindo erros do compilador. Eu os faço geralmente quando as alterações não são tão simples e quando essa refatoração não é uma refatoração real (estou mudando de funcionalidade). Esses erros do compilador estão me dando pontos que eu preciso dar uma olhada e fazer uma alteração mais complicada do que o nome ou a alteração do parâmetro.

Isso soa semelhante a um método absolutamente padrão usado no desenvolvimento orientado a testes: escreva o teste referente a uma classe inexistente, de modo que a primeira etapa para fazer o passe de teste é adicionar a classe, os métodos e assim por diante. Ver Livro de Beck Para exemplos exaustivos de Java.

Seu método para refatorar soa perigoso porque você não tem nenhum teste de segurança (ou pelo menos não menciona que tem algum). Você pode criar um código de compilação que realmente não faz o que deseja ou quebra outras partes do seu aplicativo.

Sugiro que você adicione uma regra simples à sua prática: faça alterações não compilantes Somente no código de teste de unidade. Dessa forma, você certamente fará pelo menos um teste local para cada modificação e está registrando a intenção da modificação no seu teste antes de fazê -lo.

A propósito, o Eclipse facilita o método "falha, stub, gravação", absurdamente fácil em Java: cada objeto inexistente é marcado para você, e o Ctrl-1 mais uma opção de menu diz ao Eclipse para escrever um stub (compilável) para você! Eu estaria interessado em saber se outros idiomas e IDEs fornecem suporte semelhante.

É "seguro", no sentido de que, em uma linguagem verificada por tempo suficiente, obriga você a atualizar todas as referências ao vivo à coisa que mudou.

Ainda pode dar errado se você tiver código compilado condicionalmente, por exemplo, se você usou o pré-processador C/C ++. Portanto, certifique -se de reconstruir todas as configurações possíveis e em todas as plataformas, se aplicável.

Não remove a necessidade de testar suas alterações. Se você adicionou um parâmetro a uma função, o compilador não pode dizer se o valor que você forneceu quando atualizou cada site de chamada para essa função, era o valor certo. Se você removeu um parâmetro, ainda pode cometer erros, por exemplo, alterar:

void foo(int a, int b);

para

void foo(int a);

Em seguida, altere a chamada de:

foo(1,2);

para:

foo(2);

Isso compila bem, mas está errado.

Pessoalmente, eu uso falhas de compilação (e link) como uma maneira de pesquisar o código para referências ao vivo à função que estou alterando. Mas você tem que ter em mente que este é apenas um dispositivo de economia de trabalho. Não garante que o código resultante esteja correto.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top