Agregar complejidad para eliminar el código duplicado


24

Tengo varias clases que todas heredan de una clase base genérica. La clase base contiene una colección de varios objetos de tipo T.

Cada clase secundaria necesita poder calcular valores interpolados de la colección de objetos, pero dado que las clases secundarias usan diferentes tipos, el cálculo varía un poco de una clase a otra.

Hasta ahora he copiado / pegado mi código de una clase a otra y he realizado modificaciones menores en cada uno. Pero ahora estoy tratando de eliminar el código duplicado y reemplazarlo con un método de interpolación genérico en mi clase base. Sin embargo, eso está resultando ser muy difícil, y todas las soluciones que he pensado parecen demasiado complejas.

Estoy empezando a pensar que el principio DRY no se aplica tanto en este tipo de situación, pero eso suena como una blasfemia. ¿Cuánta complejidad es demasiado cuando se trata de eliminar la duplicación de código?

EDITAR:

La mejor solución que se me ocurre es algo como esto:

Clase 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);

Niño clase 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);
}

Niño clase B:

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);
}

99
Una aplicación excesivamente micro de conceptos como DRY y Code Reuse conduce a pecados mucho mayores.
Affe

1
Estás obteniendo algunas buenas respuestas generales. La edición para incluir una función de ejemplo podría ayudarnos a determinar si lo está llevando demasiado lejos en esta instancia en particular.
Karl Bielefeldt

Esto no es realmente una respuesta, más de una observación: Si no se puede explicar fácilmente lo que es una clase base factorizado de salida lo hace , lo mejor sería no tener uno. Otra forma de verlo es (supongo que está familiarizado con SOLID?) "¿Algún consumidor probable de esta funcionalidad requiere la sustitución de Liskov"? Si no existe un caso de negocios probable para un consumidor generalizado de funcionalidad de interpolación, una clase base no tiene valor.
Tom W

1
Lo primero es recopilar el triplete X, Y, Z en un tipo de posición y agregar interpolación a ese tipo como miembro o tal vez como un método estático: interpolar posición (otra posición, relación).
Kevin Cline

Respuestas:


30

En cierto modo, respondiste tu propia pregunta con ese comentario en el último párrafo:

Estoy empezando a pensar que el principio DRY no se aplica tanto en este tipo de situación, pero eso suena como una blasfemia .

Siempre que encuentre alguna práctica que no sea realmente práctica para resolver su problema, no intente usar esa práctica religiosamente (la palabra blasfemia es una especie de advertencia para esto). La mayoría de las prácticas tienen sus whens y porqués e incluso si cubren el 99% de todos los casos posibles, todavía hay que 1%, donde es posible que tenga un enfoque diferente.

Específicamente, con respecto a DRY , también descubrí que a veces es mejor incluso tener varias piezas de código duplicado pero simple que una monstruosidad gigante que te hace sentir enfermo cuando lo miras.

Dicho esto, la existencia de estos casos extremos no debe usarse como una excusa para la codificación descuidada de copiar y pegar o la falta total de módulos reutilizables. Simplemente, si no tiene idea de cómo escribir un código genérico y legible para algún problema en algún idioma, entonces probablemente sea menos malo tener algo de redundancia. Piensa en quien tiene que mantener el código. ¿Vivirían más fácilmente con redundancia u ofuscación?

Un consejo más específico sobre su ejemplo particular. Dijiste que estos cálculos eran similares pero ligeramente diferentes . Es posible que desee dividir su fórmula de cálculo en subformulas más pequeñas y luego hacer que todos sus cálculos ligeramente diferentes llamen a estas funciones auxiliares para hacer los subcálculos. Evitaría la situación en la que cada cálculo depende de un código demasiado generalizado y aún tendría algún nivel de reutilización.


10
Otro punto sobre similar pero ligeramente diferente es que, aunque se vean similares en el código, no significa que tengan que ser similares en "negocios". Depende de lo que sea, por supuesto, pero a veces es una buena idea mantener las cosas separadas porque, a pesar de que se ven iguales, pueden basarse en diferentes decisiones / requisitos comerciales. Por lo tanto, es posible que desee verlos como cálculos muy diferentes a pesar de que su codificación puede ser similar. (No es una regla ni nada, sino algo a tener en cuenta al decidir si las cosas se deben combinar o refactorizar :)
Svish

@Svish Punto interesante. Nunca lo pensé de esa manera.
Phil

17

Las clases secundarias usan diferentes tipos, el cálculo varía un poco de una clase a otra.

Lo primero y más importante es: Primero, identifique claramente qué parte está cambiando y qué parte NO está cambiando entre las clases. Una vez que haya identificado eso, su problema está resuelto.

Haga eso como el primer ejercicio antes de comenzar la refactorización. Todo lo demás se colocará automáticamente después de eso.


2
Así poner. Esto puede ser solo un problema de que la función repetida sea demasiado grande.
Karl Bielefeldt

8

Creo que casi todas las repeticiones de más de un par de líneas de código pueden factorizarse de una forma u otra, y casi siempre deberían serlo.

Sin embargo, esta refactorización es más fácil en algunos idiomas que en otros. Es bastante fácil en lenguajes como LISP, Ruby, Python, Groovy, Javascript, Lua, etc. Por lo general, no es demasiado difícil en C ++ usando plantillas. Más doloroso en C, donde la única herramienta puede ser macros de preprocesador. A menudo es doloroso en Java, y a veces simplemente imposible, por ejemplo, tratar de escribir código genérico para manejar múltiples tipos numéricos incorporados.

En lenguajes más expresivos, no hay duda: refactorizar algo más que un par de líneas de código. Con lenguajes menos expresivos, debe equilibrar el dolor de la refactorización con la longitud y la estabilidad del código repetido. Si el código repetido es largo o puede cambiar con frecuencia, tiendo a refactorizar incluso si el código resultante es algo difícil de leer.

Aceptaré código repetido solo si es corto, estable y la refactorización es demasiado fea. Básicamente, factorizo ​​casi toda la duplicación a menos que esté escribiendo Java.

Es imposible dar una recomendación específica para su caso porque no ha publicado el código, o incluso no ha indicado qué idioma está utilizando.


Lua no es un acrónimo.
DeadMG

@DeadMG: anotado; siéntase libre de editar. Por eso te dimos toda esa reputación.
Kevin Cline

5

Cuando dice que la clase base tiene que realizar un algoritmo, pero el algoritmo varía para cada subclase, esto suena como un candidato perfecto para The Pattern Pattern .

Con esto, la clase base realiza el algoritmo, y cuando se trata de la variación para cada subclase, difiere a un método abstracto que es responsabilidad de la subclase implementar. Piense en la forma en que la página ASP.NET difiere de su código para implementar Page_Load, por ejemplo.


4

Parece que su "método de interpolación genérico" en cada clase está haciendo demasiado y debería convertirse en métodos más pequeños.

Dependiendo de cuán complejo sea su cálculo, ¿por qué no podría cada "pieza" del cálculo ser un 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
}

Y anule las piezas individuales cuando necesite hacer esa "ligera variación" en la lógica del cálculo.

(Este es un ejemplo muy pequeño e inútil de cómo podría agregar mucha funcionalidad al anular pequeños métodos)


3

En mi opinión, tienes razón, en cierto sentido, que DRY puede llevarse demasiado lejos. Si es probable que dos códigos similares evolucionen en direcciones muy diferentes, puede causar problemas al intentar no repetirse inicialmente.

Sin embargo, también tiene razón al desconfiar de tales pensamientos blasfemos. Intenta pensar detenidamente en tus opciones antes de decidir dejarlo solo.

¿Sería, por ejemplo, mejor poner ese código repetido en una clase / método de utilidad, en lugar de en la clase base? Ver Preferir composición sobre herencia .


2

DRY es una guía a seguir, no una regla inquebrantable. En algún momento, debe decidir que no vale la pena tener X niveles de herencia y plantillas Y en cada clase que esté usando solo para decir que no hay repetición en este código. Un par de buenas preguntas para hacer sería si me tomaría más tiempo extraer estos métodos similares e implementarlos como uno solo, luego buscaría a través de todos ellos en caso de que surja la necesidad de cambiar o sea su potencial para que ocurra un cambio que podría deshacer mi trabajo extrayendo estos métodos en primer lugar? ¿Estoy en el punto donde la abstracción adicional está comenzando a hacer entender dónde o qué hace este código es un desafío?

Si puede responder afirmativamente a cualquiera de esas preguntas, entonces tiene un fuerte argumento para dejar código potencialmente duplicado


0

Tienes que hacerte la pregunta, "¿por qué debería refactorizarlo?" En su caso, cuando tiene un código "similar pero diferente" si realiza un cambio en un algoritmo, debe asegurarse de que también refleja ese cambio en los otros puntos. Esta es normalmente una receta para el desastre, invariablemente alguien más perderá un lugar e introducirá otro error.

En este caso, refactorizar los algoritmos en uno gigante lo hará demasiado complicado, lo que lo hará demasiado difícil para el mantenimiento futuro. Entonces, si no puede factorizar las cosas comunes con sensatez, un simple:

// this code is similar to class x function b

El comentario será suficiente. Problema resuelto.


0

Al decidir si es mejor tener un método más grande o dos métodos más pequeños con funcionalidad superpuesta, la primera pregunta de $ 50,000 es si la parte superpuesta del comportamiento podría cambiar, y si cualquier cambio debe aplicarse a los métodos más pequeños por igual. Si la respuesta a la primera pregunta es sí pero la respuesta a la segunda pregunta es no, entonces los métodos deben permanecer separados . Si la respuesta a ambas preguntas es sí, entonces se debe hacer algo para garantizar que todas las versiones del código permanezcan sincronizadas; En muchos casos, la forma más fácil de hacerlo es tener solo una versión.

Hay algunos lugares donde Microsoft parece haber ido en contra de los principios DRY. Por ejemplo, Microsoft ha desaconsejado explícitamente que los métodos acepten un parámetro que indique si una falla debería generar una excepción. Si bien es cierto que un parámetro de "fallas arrojan excepciones" es feo en la API de "uso general" de un método, dichos parámetros pueden ser muy útiles en los casos en que un método Try / Do necesita estar compuesto de otros métodos Try / Do. Si se supone que un método externo arroja una excepción cuando ocurre una falla, entonces cualquier llamada al método interno que falla debe arrojar una excepción que el método externo puede dejar propagar. Si no se supone que el método externo arroje una excepción, entonces el método interno tampoco lo es. Si se usa un parámetro para distinguir entre try / do, entonces el método externo puede pasarlo al método interno. De lo contrario, será necesario que el método externo llame a los métodos "try" cuando se supone que se comportará como "try", y a los métodos "do" cuando se suponga que se comporte como "do".

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.