Adicionando complexidade para remover código duplicado


24

Eu tenho várias classes que todos herdam de uma classe base genérica. A classe base contém uma coleção de vários objetos do tipo T.

Cada classe filho precisa ser capaz de calcular valores interpolados a partir da coleção de objetos, mas como as classes filho usam tipos diferentes, o cálculo varia um pouquinho de classe para classe.

Até agora, copiei / colei meu código de classe para classe e fiz pequenas modificações em cada uma. Mas agora estou tentando remover o código duplicado e substituí-lo por um método de interpolação genérico na minha classe base. No entanto, isso está se mostrando muito difícil, e todas as soluções que pensei parecem muito complexas.

Estou começando a pensar que o princípio DRY não se aplica tanto a esse tipo de situação, mas parece blasfêmia. Quanta complexidade é grande ao tentar remover a duplicação de código?

EDITAR:

A melhor solução que posso encontrar é mais ou menos assim:

Classe base:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

Classe infantil A:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

Classe B da criança:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
Uma micro aplicação excessiva de conceitos como DRY e Code Reuse leva a pecados muito maiores.
Macaco

11
Você está recebendo boas respostas gerais. A edição para incluir uma função de exemplo pode nos ajudar a determinar se você está levando muito longe nessa instância específica.
Karl Bielefeldt

Isso não é realmente uma resposta, é mais uma observação: se você não pode explicar facilmente o que uma classe base fatorada faz , talvez seja melhor não ter uma. Outra maneira de analisar é (presumo que você esteja familiarizado com o SOLID?) 'Algum consumidor provável dessa funcionalidade exige substituição de Liskov'? Se não houver um caso de negócios provável para um consumidor generalizado da funcionalidade de interpolação, uma classe base não terá valor.
Tom W

11
A primeira coisa é coletar o trigêmeo X, Y, Z em um tipo de Posição e adicionar interpolação a esse tipo como membro ou talvez um método estático: Posição interpolar (Posição outro, razão).
Kevin cline

Respostas:


30

De certa forma, você respondeu sua própria pergunta com essa observação no último parágrafo:

Estou começando a pensar que o princípio DRY não se aplica tanto a esse tipo de situação, mas parece blasfêmia .

Sempre que você achar que alguma prática não é realmente prática para resolver seu problema, não tente usá-la religiosamente (a palavra blasfêmia é uma espécie de aviso para isso). A maioria das práticas têm suas whens e porquês e até mesmo se eles cobrem 99% de todos os casos possíveis, ainda há que 1%, onde você pode precisar de uma abordagem diferente.

Especificamente, com relação ao DRY , também descobri que, às vezes, é realmente melhor ter várias partes do código duplicado, mas simples, do que uma monstruosidade gigante que faz você se sentir doente quando olha para ele.

Dito isto, a existência desses casos extremos não deve ser usada como desculpa para a codificação superficial de copiar e colar ou completa falta de módulos reutilizáveis. Simplesmente, se você não tem idéia de como escrever um código genérico e legível para algum problema em algum idioma, provavelmente é menos ruim ter alguma redundância. Pense em quem precisa manter o código. Eles viveriam mais facilmente com redundância ou ofuscação?

Um conselho mais específico sobre o seu exemplo específico. Você disse que esses cálculos eram semelhantes, mas um pouco diferentes . Convém tentar dividir sua fórmula de cálculo em sub-fórmulas menores e fazer com que todos os seus cálculos ligeiramente diferentes chamem essas funções auxiliares para fazer os sub-cálculos. Você evitaria a situação em que todo cálculo depende de algum código super generalizado e ainda teria algum nível de reutilização.


10
Outro ponto sobre similar, mas um pouco diferente, é que, embora pareçam semelhantes em código, não significa que tenham que ser parecidos em "negócios". Depende do que é claro, mas às vezes é uma boa idéia manter as coisas separadas, porque, embora pareçam iguais, podem ser baseadas em diferentes decisões / requisitos de negócios. Portanto, convém considerá-los como cálculos muito diferentes, mesmo que eles codifiquem de maneira semelhante. (Não é uma regra ou qualquer coisa, mas apenas algo para manter em mente quando decidir se as coisas devem ser combinados ou reformulado :)
Svish

@Svish Ponto interessante. Nunca pensei nisso dessa maneira.
Phil

17

Se as classes filho usam tipos diferentes, o cálculo varia um pouco de classe para classe.

A primeira e mais importante é: primeiro identifique claramente qual parte está mudando e qual parte NÃO está mudando entre as classes. Depois de identificar isso, seu problema está resolvido.

Faça isso como o primeiro exercício antes de iniciar o re-fatoração. Tudo o resto se encaixará automaticamente depois disso.


2
Bem colocado. Isso pode ser apenas um problema de a função repetida ser muito grande.
Karl Bielefeldt

8

Acredito que quase toda repetição de mais de algumas linhas de código possa ser fatorada de uma maneira ou de outra, e quase sempre deveria ser.

No entanto, essa refatoração é mais fácil em alguns idiomas do que em outros. É bastante fácil em linguagens como LISP, Ruby, Python, Groovy, Javascript, Lua, etc. Geralmente não é muito difícil em C ++ usando modelos. Mais doloroso em C, onde a única ferramenta pode ser macros de pré-processador. Frequentemente doloroso em Java e às vezes simplesmente impossível, por exemplo, tentando escrever código genérico para lidar com vários tipos numéricos embutidos.

Em linguagens mais expressivas, não há dúvida: refatorar qualquer coisa além de algumas linhas de código. Com linguagens menos expressivas, é necessário equilibrar a dor da refatoração com o comprimento e a estabilidade do código repetido. Se o código repetido é longo ou pode sofrer alterações com frequência, tenho tendência a refatorar mesmo que o código resultante seja um pouco difícil de ler.

Aceitarei código repetido apenas se for curto, estável e a refatoração for muito feia. Basicamente, eu fatoro quase toda a duplicação, a menos que esteja escrevendo Java.

É impossível fazer uma recomendação específica para o seu caso, porque você não publicou o código ou até indicou qual idioma está usando.


Lua não é um acrônimo.
DeadMG 3/12/12

@DeadMG: anotado; fique à vontade para editar. Por isso lhe demos toda essa reputação.
precisa

5

Quando você diz que a classe base precisa executar um algoritmo, mas o algoritmo varia para cada subclasse, isso soa como um candidato perfeito para o Modelo Padrão .

Com isso, a classe base executa o algoritmo e, quando se trata da variação de cada subclasse, adia para um método abstrato que é de responsabilidade da subclasse implementar. Pense na maneira como a página ASP.NET é diferente do seu código para implementar Page_Load, por exemplo.


4

Parece que o seu "método de interpolação genérico" em cada classe está fazendo muito e deve ser transformado em métodos menores.

Dependendo da complexidade do seu cálculo, por que cada "parte" do cálculo não pode ser um método virtual como este

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

E substitua as peças individuais quando precisar fazer essa "pequena variação" na lógica do cálculo.

(Este é um exemplo muito pequeno e inútil de como você pode adicionar muitas funcionalidades ao substituir métodos pequenos)


3

Na minha opinião, você está certo, em certo sentido, que o DRY pode ser levado longe demais. Se duas partes semelhantes do código provavelmente evoluirem em direções muito diferentes, você poderá causar problemas tentando não se repetir inicialmente.

No entanto, você também tem toda a razão de desconfiar de tais pensamentos blasfemos. Tente muito pensar em suas opções antes de decidir deixá-lo em paz.

Seria, por exemplo, melhor colocar esse código repetido em uma classe / método utilitário, em vez de na classe base? Consulte Preferir composição sobre herança .


2

O DRY é uma diretriz a seguir, não uma regra inquebrável. Em algum momento, você precisa decidir que não vale a pena ter níveis X de herança e modelos Y em todas as classes que você está usando apenas para dizer que não há repetição neste código. Algumas boas perguntas a serem feitas seriam: levarei mais tempo para extrair esses métodos semelhantes e implementá-los como um só; depois, pesquisarei por todos eles caso surja a necessidade de mudar ou seja o seu potencial para que uma mudança ocorra desfazer meu trabalho extraindo esses métodos em primeiro lugar? Estou no ponto em que a abstração adicional está começando a entender onde ou o que esse código faz é um desafio?

Se você pode responder sim a uma dessas perguntas, é bastante provável que deixe código potencialmente duplicado


0

Você precisa se perguntar: "por que devo refatorá-la"? No seu caso, quando você tiver um código "semelhante, mas diferente", se fizer uma alteração em um algoritmo, precisará garantir que também reflita essa alteração nos outros pontos. Normalmente, isso é uma receita para o desastre; invariavelmente, alguém perderá um ponto e introduzirá outro bug.

Nesse caso, refatorar os algoritmos em um gigante tornará muito complicado, dificultando a manutenção futura. Portanto, se você não pode fatorar as coisas comuns de maneira sensata, uma simples:

// this code is similar to class x function b

Comentário será suficiente. Problema resolvido.


0

Ao decidir se é melhor ter um método maior ou dois métodos menores com funcionalidade sobreposta, a primeira pergunta de US $ 50.000 é se a parte sobreposta do comportamento pode mudar e se alguma alteração deve ser aplicada igualmente aos métodos menores. Se a resposta para a primeira pergunta for sim, mas a resposta para a segunda pergunta for não, os métodos deverão permanecer separados . Se a resposta para ambas as perguntas for afirmativa, algo deve ser feito para garantir que todas as versões do código permaneçam sincronizadas; em muitos casos, a maneira mais fácil de fazer isso é ter apenas uma versão.

Existem alguns lugares em que a Microsoft parece ter ido contra os princípios DRY. Por exemplo, a Microsoft desencorajou explicitamente que os métodos aceitassem um parâmetro que indica se uma falha deve gerar uma exceção. Embora seja verdade que um parâmetro "falhas lançam exceções" é feio na API de "uso geral" de um método, esses parâmetros podem ser muito úteis nos casos em que um método Try / Do precisa ser composto por outros métodos Try / Do. Se um método externo deve lançar uma exceção quando ocorre uma falha, qualquer chamada de método interno que falhe deve lançar uma exceção que o método externo pode permitir que se propague. Se o método externo não deve lançar uma exceção, o método interno também não é. Se um parâmetro for usado para distinguir entre tentar / fazer, então o método externo pode passá-lo para o método interno. Caso contrário, será necessário que o método externo chame os métodos "try" quando ele deveria se comportar como "try" e os métodos "do" quando deveria se comportar como "do".

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.