tipos - sentencias repetitivas en java
Gestionar código y documentación altamente repetitivos en Java (9)
El código altamente repetitivo generalmente es algo malo, y hay patrones de diseño que pueden ayudar a minimizar esto. Sin embargo, a veces es simplemente inevitable debido a las limitaciones del lenguaje en sí. Tome el siguiente ejemplo de java.util.Arrays
:
/**
* Assigns the specified long value to each element of the specified
* range of the specified array of longs. The range to be filled
* extends from index <tt>fromIndex</tt>, inclusive, to index
* <tt>toIndex</tt>, exclusive. (If <tt>fromIndex==toIndex</tt>, the
* range to be filled is empty.)
*
* @param a the array to be filled
* @param fromIndex the index of the first element (inclusive) to be
* filled with the specified value
* @param toIndex the index of the last element (exclusive) to be
* filled with the specified value
* @param val the value to be stored in all elements of the array
* @throws IllegalArgumentException if <tt>fromIndex > toIndex</tt>
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex < 0</tt> or
* <tt>toIndex > a.length</tt>
*/
public static void fill(long[] a, int fromIndex, int toIndex, long val) {
rangeCheck(a.length, fromIndex, toIndex);
for (int i=fromIndex; i<toIndex; i++)
a[i] = val;
}
El fragmento anterior aparece en el código fuente 8 veces, con muy poca variación en la documentación / firma del método pero exactamente el mismo cuerpo del método , uno para cada tipo de matriz raíz int[]
, short[]
, char[]
, byte[]
, boolean[]
, double[]
, float[]
y Object[]
.
Creo que a menos que uno recurra a la reflexión (que es un tema completamente diferente en sí mismo), esta repetición es inevitable. Entiendo que como una clase de utilidad, una concentración tan alta de código Java repetitivo es muy atípica, pero incluso con la mejor práctica, la repetición sí sucede . Refactorizar no siempre funciona porque no siempre es posible (el caso obvio es cuando la repetición está en la documentación).
Obviamente, mantener este código fuente es una pesadilla. Un pequeño error en la documentación, o un error menor en la implementación, se multiplica por la cantidad de repeticiones que se realizaron. De hecho, el mejor ejemplo involucra a esta clase exacta:
El error es sorprendentemente sutil, ocurriendo en lo que muchos pensaron que era solo un algoritmo simple y directo.
// int mid =(low + high) / 2; // the bug
int mid = (low + high) >>> 1; // the fix
¡La línea de arriba aparece 11 veces en el código fuente !
Entonces mis preguntas son:
- ¿Cómo se manejan en la práctica estos tipos de código / documentación repetitiva de Java? ¿Cómo se desarrollan, mantienen y prueban?
- ¿Empiezas con "el original" y lo haces lo más maduro posible, y luego lo copias y lo pegas según sea necesario y esperas que no hayas cometido un error?
- Y si cometió un error en el original, simplemente corríjalo en todas partes, a menos que se sienta cómodo eliminando las copias y repitiendo todo el proceso de replicación.
- ¿Y aplicas el mismo proceso para el código de prueba también?
- ¿Se beneficiaría Java de algún tipo de preprocesamiento de código fuente de uso limitado para este tipo de cosas?
- ¿Tal vez Sun tiene su propio preprocesador para ayudar a escribir, mantener, documentar y probar este tipo de código de biblioteca repetitivo?
Un comentario solicitó otro ejemplo, así que saqué este de Google Collections: com.google.common.base.Predicates líneas 276-310 ( AndPredicate
) frente a las líneas 312-346 ( OrPredicate
).
La fuente de estas dos clases es idéntica, a excepción de:
-
AndPredicate
vsOrPredicate
(cada uno aparece 5 veces en su clase) -
"And("
vsOr("
(en los métodostoString()
respectivos) -
#or
vs#or
(en el@see
comentarios de Javadoc) -
true
vsfalse
(inapply
!
puede reescribirse fuera de la expresión) -
-1 /* all bits on */
vs0 /* all bits off */
enhashCode()
-
&=
vs|=
enhashCode()
Dado dos fragmentos de código que se dice que son similares, la mayoría de los lenguajes tienen instalaciones limitadas para construir abstracciones que unifican los fragmentos de código en un monolito. Para resumir cuando su idioma no puede hacerlo, debe salir del idioma: - {
El mecanismo más general de "abstracción" es un macroprocesador completo que puede aplicar cálculos arbitrarios al "cuerpo macro" al instanciarlo (piense en el sistema Post o de reescritura de cadenas , que es capaz de Turing). M4 y GPM son ejemplos por excelencia. El preprocesador C no es uno de estos.
Si tiene un macroprocesador de este tipo, puede construir una "abstracción" como macro y ejecutar el macroprocesador en su texto fuente "abstraído" para producir el código fuente real que compila y ejecuta.
También puede usar versiones más limitadas de las ideas, a menudo llamadas "generadores de código". Estos generalmente no son capaces de Turing, pero en muchos casos funcionan lo suficientemente bien. Depende de cuán sofisticado debe ser su "macro instanciación". (La razón por la que la gente está enamorada del mecanismo de plantilla de C ++ es que a pesar de su fealdad, es capaz de Turing y la gente puede hacer tareas de generación de código realmente feas pero asombrosas). Otra respuesta aquí menciona Trove, que aparentemente está en la categoría más limitada pero aún muy útil.
Los macroprocesadores realmente generales (como M4) manipulan solo texto; eso los hace poderosos pero no manejan bien la estructura del lenguaje de programación, y es realmente incómodo escribir un generador en un procesador mcaro que no solo produzca código, sino que también optimice el resultado generado. La mayoría de los generadores de código que encuentro son "conecte esta cadena en esta plantilla de cadena" y por lo tanto no puede hacer ninguna optimización de un resultado generado. Si desea generar código arbitrario y alto rendimiento para arrancar, necesita algo que sea capaz de Turing, pero que comprenda la estructura del código generado para poder manipularlo fácilmente (por ejemplo, optimizarlo).
Tal herramienta se llama Sistema de Transformación de Programas . Tal herramienta analiza el texto de origen como lo hace un compilador, y luego lleva a cabo análisis / transformaciones para lograr el efecto deseado. Si puede poner marcadores en el texto fuente de su programa (por ejemplo, comentarios estructurados o anotaciones en los idiomas que los tienen), dirigiendo la herramienta de transformación del programa qué hacer, entonces puede usarla para llevar a cabo dicha instanciación de abstracción, generación de código y / o optimización del código (La sugerencia de un afiche de engancharse al compilador de Java es una variación de esta idea). El uso de un sistema general de transformación de puprose (como DMS Software Reengineering Tookit significa que puede hacer esto para prácticamente cualquier idioma.
De Wikipedia Do not Repeat Yourself (DRY) o Duplication is Evil (DIE)
En algunos contextos, el esfuerzo requerido para hacer cumplir la filosofía DRY puede ser mayor que el esfuerzo por mantener copias separadas de los datos. En algunos otros contextos, la información duplicada es inmutable o se mantiene bajo un control lo suficientemente estricto como para que DRY no sea necesario.
Probablemente no haya respuesta o técnica para evitar problemas como ese.
Entiendo que Sun debe documentar así el código de la biblioteca Java SE y quizás otros escritores de bibliotecas de terceros también lo hagan.
Sin embargo, creo que es una pérdida total copiar y pegar documentación en un archivo como este en código que solo se usa en casa. Sé que mucha gente estará en desacuerdo porque hará que sus JavaDocs internos se vean menos limpios. Sin embargo, la compensación es que hace que su código sea más limpio, lo que, en mi opinión, es más importante.
Incluso los lenguajes de pantalones elegantes como Haskell tienen código repetitivo ( ver mi publicación en Haskell y serialización )
Parece que hay tres opciones para este problema:
- Usa la reflexión y pierde el rendimiento
- Utilice preprocesamiento como Template Haskell o Caml4p equivalente para su idioma y viva con maldad
- O mis macros de uso personal favoritas si su idioma lo admite (esquema y lisp)
Considero que las macros son diferentes a las del preprocesamiento porque las macros suelen estar en el mismo idioma que el objetivo, ya que el preprocesamiento es un lenguaje diferente.
Creo que las macros de Lisp / Scheme resolverían muchos de estos problemas.
Los tipos primitivos de Java te atornillan, especialmente cuando se trata de arreglos. Si estás preguntando específicamente sobre el código que involucra tipos primitivos, entonces diría que simplemente trato de evitarlos. El método Object [] es suficiente si usa los tipos encuadrados.
En general, necesitas muchas pruebas unitarias y realmente no hay nada más que hacer, aparte de recurrir a la reflexión. Como dijiste, es otro tema por completo, pero no tengas miedo a la reflexión. Primero, escribe el código DRYest, luego perfilalo y determina si el rendimiento del reflejo es lo suficientemente malo como para justificar la escritura y mantener el código adicional.
Mucho de este tipo de repetición ahora se puede evitar gracias a los genéricos. Son un regalo del cielo cuando se escribe el mismo código donde solo cambian los tipos.
Lamentablemente, creo que las matrices genéricas aún no son muy compatibles. Por ahora, al menos, use contenedores que le permitan aprovechar los genéricos. El polimorfismo es también una herramienta útil para reducir este tipo de duplicación de código.
Para responder a su pregunta sobre cómo manejar el código que absolutamente debe duplicarse ... Etiquete cada instancia con comentarios de fácil búsqueda. Hay algunos preprocesadores Java que agregan macros de estilo C. Creo que recuerdo netbeans tener uno.
Para las personas que absolutamente necesitan rendimiento, boxeo y desempaquetado y colecciones genéricas y otras cosas que no son grandes.
El mismo problema ocurre en la computación de rendimiento donde se necesita el mismo complejo para trabajar tanto en flotación como en doble (por ejemplo, algunos de los métodos que se muestran en Goldberd: " Lo que cada científico debería saber sobre los números de coma flotante " ).
Hay una razón por la cual TIntIntHashMap
Trove ejecuta círculos alrededor de HashMap<Integer,Integer>
de Java HashMap<Integer,Integer>
cuando trabaja con una cantidad similar de datos.
¿Cómo se escribe el código fuente de la colección Trove?
Mediante el uso de la instrumentación de código fuente, por supuesto :)
Hay varias bibliotecas Java para un mayor rendimiento (mucho más alto que las Java predeterminadas) que usan generadores de código para crear el código fuente repetido.
Todos sabemos que la "instrumentación de código fuente" es mala y que la generación de código es una porquería, pero así es como lo hacen las personas que realmente saben lo que hacen (es decir, el tipo de personas que escriben cosas como Trove) :)
Por lo que vale, generamos un código fuente que contiene grandes advertencias como:
/*
* This .java source file has been auto-generated from the template xxxxx
*
* DO NOT MODIFY THIS FILE FOR IT SHALL GET OVERWRITTEN
*
*/
Puede usar un generador de código para construir variaciones del código usando una plantilla. En ese caso, la fuente java es un producto del generador y el código real es la plantilla.
Si absolutamente debe duplicar el código, siga los excelentes ejemplos que ha dado y agrupe todo ese código en un solo lugar donde sea fácil de encontrar y corregir cuando tenga que realizar un cambio. Documente la duplicación y, lo que es más importante, el motivo de la duplicación para que todos los que vienen detrás de usted conozcan ambos.