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
, E
e 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 SN
está 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?
fonte
Respostas:
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
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
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.
fonte
Você está realmente fazendo a coisa certa. Eu digo isso porque:
fonte
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.
fonte
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.
fonte
O primeiro passo, não importa para onde isso vá, deve ser dividir o método aparentemente grande
A::ExecJob
em pedaços menores.Portanto, em vez de
você recebe
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.
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
.fonte
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
fonte
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
A
paraF
nã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()
aS5()
sua classe base. Posteriormente, as funções "ExecJob" devem ficar assim: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.
fonte