Como refatorar o código para algum código comum?

16

fundo

Estou trabalhando em um projeto C # em andamento. Eu não sou um programador de C #, principalmente um programador de C ++. Então, fui designado basicamente para tarefas fáceis e de refatoração.

O código está uma bagunça. É um projeto enorme. Como nosso cliente exigia lançamentos frequentes com novos recursos e correções de erros, todos os outros desenvolvedores foram forçados a adotar uma abordagem de força bruta durante a codificação. O código é altamente insustentável e todos os outros desenvolvedores concordam com ele.

Não estou aqui para debater se eles fizeram certo. Como estou refatorando, estou me perguntando se estou fazendo isso da maneira certa, pois meu código refatorado parece complexo! Aqui está minha tarefa como exemplo simples.

Problema

Há seis classes: A, B, C, D, Ee F. Todas as classes têm uma função ExecJob(). Todas as seis implementações são muito semelhantes. Basicamente, a princípio A::ExecJob()foi escrito. Em seguida, foi necessária uma versão ligeiramente diferente, que foi implementada na B::ExecJob()modificação de copiar e colar de A::ExecJob(). Quando outra versão ligeiramente diferente foi necessária, C::ExecJob()foi escrita e assim por diante. Todas as seis implementações têm algum código comum, depois algumas linhas de código diferentes, novamente algum código comum e assim por diante. Aqui está um exemplo simples das implementações:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

Onde SNestá um grupo exato das mesmas instruções.

Para torná-los comuns, criei outra classe e movi o código comum em uma função. Usando o parâmetro para controlar qual grupo de instruções deve ser executado:

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

Observe que, este exemplo emprega apenas três classes e instruções simplificadas demais. Na prática, a CommonTask()função parece muito complexa com todas essas verificações de parâmetros e há muito mais instruções. Além disso, no código real, existem várias CommonTask()funções de aparência.

Embora todas as implementações estejam compartilhando código comum e as ExecJob()funções pareçam mais bonitas, existem dois problemas que estão me incomodando:

  • Para qualquer alteração CommonTask(), todos os seis recursos (e podem ser mais no futuro) são necessários para serem testados.
  • CommonTask()já é complexo. Isso ficará mais complexo com o tempo.

Estou fazendo isso da maneira certa?

Donotalo
fonte
O livro Refatoração de Martin Fowler possui várias técnicas específicas para refatorar código que você pode achar úteis.
Allan

Respostas:

14

Sim, você está absolutamente no caminho certo!

Na minha experiência, notei que, quando as coisas são complicadas, as mudanças acontecem em pequenos passos. O que você fez é a etapa 1 do processo de evolução (ou processo de refatoração). Aqui estão as etapas 2 e 3:

Passo 2

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

Este é o 'Padrão de design do modelo' e está um passo à frente no processo de refatoração. Se a classe base mudar, as subclasses (A, B, C) não precisam ser afetadas. Você pode adicionar novas subclasses com relativa facilidade. No entanto, imediatamente da imagem acima, você pode ver que a abstração está quebrada. A necessidade de 'implementação vazia' é um bom indicador; isso mostra que há algo errado com sua abstração. Pode ter sido uma solução aceitável a curto prazo, mas parece haver uma solução melhor.

etapa 3

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

Esse é o 'Padrão de design da estratégia' e parece ser um bom ajuste para o seu caso. Existem estratégias diferentes para executar o trabalho e cada classe (A, B, C) o implementa de maneira diferente.

Estou certo de que há uma etapa 4 ou 5 nesse processo ou abordagens de refatoração muito melhores. No entanto, este permitirá eliminar o código duplicado e garantir que as alterações sejam localizadas.

Guven
fonte
O principal problema que vejo com a solução descrita na "Etapa 2" é que a implementação concreta do S5 existe duas vezes.
user281377
1
Sim, a duplicação de código não é eliminada! E esse é outro indicador da abstração que não está funcionando. Eu só queria colocar a etapa 2 lá fora, para mostrar como eu penso sobre o processo; uma abordagem passo a passo para encontrar algo melhor.
Guven
1
+1 Muito boa estratégia (e não estou falando sobre o padrão )!
Jordão
7

Você está realmente fazendo a coisa certa. Eu digo isso porque:

  1. Se você precisar alterar o código para uma funcionalidade de tarefa comum, não precisará alterá-lo nas 6 classes que conteriam o código se não o escrever em uma classe comum.
  2. O número de linhas de código será diminuído.
prema
fonte
3

Você vê esse tipo de código compartilhando muito com o design orientado a eventos (especialmente o .NET). A maneira mais sustentável de manter é manter seu comportamento compartilhado nos menores pedaços possíveis.

Deixe o código de alto nível reutilizar vários métodos pequenos, deixe o código de alto nível fora da base compartilhada.

Você terá muitas placas de caldeira em suas implementações de folhas / concreto. Não entre em pânico, está tudo bem. Todo esse código é direto, fácil de entender. Você precisará reorganizá-lo ocasionalmente quando as coisas quebrarem, mas será fácil mudar.

Você verá muitos padrões no código de alto nível. Às vezes são reais, na maioria das vezes não são. As "configurações" dos cinco parâmetros lá em cima parecem semelhantes, mas não são. São três estratégias completamente diferentes.

Também quero observar que você pode fazer tudo isso com composição e nunca se preocupar com herança. Você terá menos acoplamento.

Tom Kerr
fonte
3

Se eu fosse você, provavelmente adicionarei mais uma etapa no início: um estudo baseado em UML.

Refatorar o código mesclando todas as partes comuns nem sempre é a melhor jogada, parece mais uma solução temporária do que uma boa abordagem.

Desenha um esquema UML, mantém as coisas simples, mas eficazes, lembre-se de alguns conceitos básicos sobre o seu projeto, como "o que deve fazer este software?" "qual é a melhor maneira de manter esse software abstrato, modular, extensível, etc etc?" "como posso implementar o encapsulamento da melhor maneira?"

Só estou dizendo o seguinte: não se importe com o código agora, você só precisa se preocupar com a lógica, quando tiver uma lógica clara em mente todo o resto pode se tornar uma tarefa muito fácil, no final, todo esse tipo dos problemas que você está enfrentando é meramente causado por uma lógica ruim.

Micro
fonte
Esse deve ser o primeiro passo, antes que qualquer refatoração seja feita. Até que o código seja compreendido o suficiente para ser mapeado (uml ou algum outro mapa da natureza), a refatoração será arquitetada no escuro.
Kzqai #
3

O primeiro passo, não importa para onde isso vá, deve ser dividir o método aparentemente grande A::ExecJobem pedaços menores.

Portanto, em vez de

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

você recebe

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

A partir daqui, existem muitos caminhos possíveis a seguir. Minha opinião: Torne A a classe base da hierarquia da sua classe e do ExecJob virtual e torna-se fácil criar B, C, ... sem copiar e colar demais - basta substituir o ExecJob (agora com cinco linhas) por um modificado versão.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

Mas por que tem tantas aulas? Talvez você possa substituí-los por uma única classe que possua um construtor, no qual sejam informadas quais ações são necessárias ExecJob.

user281377
fonte
2

Eu concordo com as outras respostas de que sua abordagem está no caminho certo, embora eu não ache que a herança seja a melhor maneira de implementar código comum - eu prefiro a composição. Nas Perguntas frequentes sobre C ++, que podem explicar muito melhor do que eu jamais pude: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html

Formiga
fonte
1

Primeiro, você deve se certificar de que a herança é realmente a ferramenta certa aqui para o trabalho - só porque você precisa de um lugar comum para as funções utilizadas por suas classes Apara Fnão significa que uma classe base comum é a coisa certa aqui - às vezes um ajudante separado classe faz o trabalho melhor. Pode ser, pode não ser. Isso depende de ter um relacionamento "é-a" entre A a F e sua classe base comum, impossível de dizer pelos nomes artificiais AF. Aqui você encontrar um post lidar com este tema.

Vamos supor que você decida que a classe base comum é a coisa certa no seu caso. A segunda coisa que eu faria é garantir que os fragmentos de código S1 a S5 sejam implementados em métodos separados para S1()a S5()sua classe base. Posteriormente, as funções "ExecJob" devem ficar assim:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

Como você vê agora, como S1 a S5 são apenas chamadas de método, nenhum código bloqueia mais, a duplicação de código foi quase completamente removida e você não precisa mais de nenhuma verificação de parâmetro, evitando o problema de aumentar a complexidade que você pode ter de outra forma.

Finalmente, mas apenas como uma terceira etapa (!), Você pode pensar em combinar todos esses métodos ExecJob em uma de sua classe base, onde a execução dessas partes pode ser controlada por parâmetros, da maneira que você sugeriu ou usando padrão de método de modelo. Você precisa decidir se vale a pena o esforço no seu caso, com base no código real.

Mas o IMHO, a técnica básica para dividir métodos grandes em métodos pequenos, é muito, muito mais importante para evitar a duplicação de código do que aplicar padrões.

Doc Brown
fonte