¿Es una mala práctica de programación pasar parámetros como Objetos? [duplicar]

96

Entonces, tenemos a un tipo al que le gusta escribir métodos que toman objetos como parámetros, para que puedan ser 'muy flexibles'. Luego, internamente, o bien realiza el casting directo, la reflexión o la sobrecarga de métodos para manejar los diferentes tipos.

Siento que esta es una mala práctica, pero no puedo explicar exactamente por qué, excepto que hace que sea más difícil de leer. ¿Existen otras razones más concretas por las que esto sería una mala práctica? ¿Cuáles serían esos?

Entonces, algunas personas han pedido un ejemplo. Tiene una interfaz definida, algo como:

public void construct(Object input, Object output);

Y él planea usar estos colocando varios en una lista, de modo que genere bits y los agregue al objeto de salida, de este modo:

for (ConstructingThing thing : constructingThings)
    thing.construct(input, output);
return output;

Luego, en la cosa que implementa construct , hay una cosa de reflexión desvencijada que encuentra el método correcto que coincide con la entrada / salida y la llama, pasando la entrada / salida.

Sigo diciéndole que funciona, entiendo que funciona, pero también lo hace poner todo en una sola clase. Estamos hablando de reutilización y mantenibilidad, y creo que en realidad se está limitando a sí mismo y tiene menos reutilización de lo que piensa. La capacidad de mantenimiento, aunque probablemente sea alta para él en este momento, probablemente será muy baja para él y para cualquier otra persona en el futuro.

    
pregunta Risser 20.09.2013 - 19:57

18 respuestas

102

Desde un punto de vista práctico veo estos problemas:

  • Una gran cantidad de posibles errores de tipo de ejecución, a menos que muchas de las comprobaciones dinámicas de tipo que podrían evitarse con el comprobador de tipo sólido incluido en Java.
  • Un montón de lanzamientos innecesarios
  • Dificultad para entender lo que hace un método por su firma

Desde un punto de vista teórico veo estos problemas:

  • A falta de contratos de la interfaz de una clase. Si todos los parámetros son de tipo Object , entonces no está declarando nada informativo a las clases del cliente.
  • Falta de posibilidades de sobrecarga
  • Lo incorrecto de anular. Puede anular un método y cambiar sus tipos de parámetros, rompiendo así todo lo relacionado con la herencia.
respondido por el Jack 20.09.2013 - 21:22
106

El método viola el principio de sustitución de Liskov .

Si un método acepta un parámetro de tipo Object , debo esperar que cualquier objeto que pase al método funcione.

Lo que parece es que solo se permiten ciertas subclases de Object . La única forma de saber qué tipos están permitidos es conocer los detalles del método. En otras palabras, el método es menos flexible que lo anunciado.

    
respondido por el Aaron Kurtzhals 14.05.2013 - 20:14
53

Consideraría las siguientes implicaciones directas:

Capacidad de lectura

Supongo que trabajar con él no es exactamente la experiencia más placentera del mundo. Nunca se sabe cuál será el tipo de letra al final ni qué sucederá con los parámetros. Dudo que aprecies perder tu tiempo con eso.

Tipo de firma

En Java todo es un Objeto. Entonces, ¿qué hace ese método al final?

Speed

La reflexión, los moldes, las comprobaciones de tipos, etc. contribuyen a un rendimiento más lento. ¿Por qué? Porque en su opinión hace que las cosas sean flexibles.

Contra la naturaleza

Java ha sido fuertemente tipado desde su nacimiento. Dígale que comience a escribir JavaScript o Python o Ruby si quiere un dialecto de tipo dinámico. Su fondo probablemente se basa fuertemente en uno de ellos.

Flexibilidad

Para usar el mismo término que el colega en cuestión, Java ya ha descubierto la flexibilidad hace mucho tiempo. No tiene sentido mirarlo con las gafas de caballo puestas por un lenguaje de tipo dinámico.

Interfaces, polimorfismo, covarianza y amp; contra-varianza, genéricos, herencia, clases abstractas y sobrecarga de métodos son solo algunas de las cosas que contribuyen a la llamada "flexibilidad". La flexibilidad viene dada por el hecho de que es trivial especializar virtualmente cualquier cosa desde cualquier tipo dado a otro (el caso habitual significa especializarse en un tipo superior directo o inferior).

Documentación

Uno de los objetivos principales de la programación es escribir código reutilizable y estable. ¿Cómo se ve la documentación de tu ejemplo? ¿Cuál es el tiempo de aprendizaje para otro desarrollador antes de poder usar esas cosas? Le sugeriría a su chico que escriba un código de ensamblador auto-modificable y se responsabilice por ello.

    
respondido por el alex23 15.05.2013 - 14:51
41

Ya hay muchas respuestas buenas aquí. Quiero añadir un pensamiento secundario:

Lo más probable es que este patrón sea un " olor de código ".

En otras palabras, si necesita pasar un Objeto a su método para obtener la flexibilidad "definitiva", lo más probable es que tenga un método muy poco definido . El hecho de haber escrito los parámetros con fuerza indica un "contrato" con la persona que llama (necesito cosas que se parezcan a "esto"). El hecho de no poder especificar un contrato de este tipo probablemente signifique que su método hace demasiado o tiene un alcance poco definido.

    
respondido por el SamtheBrand 20.09.2013 - 21:33
22

Quita la comprobación de tipo que hace el compilador y puede dar lugar a más excepciones de tiempo de ejecución cuando el objeto se convierte al tipo concreto. Por lo tanto, el usuario final verá la excepción no controlada, a menos que todos los lanzamientos no válidos se capturen y manejen adecuadamente.

    
respondido por el edtheprogrammerguy 14.05.2013 - 18:55
16

Estoy de acuerdo con las otras respuestas en que esto es generalmente una mala práctica.

Hay un caso específico en el que encontrar que pasar un Objeto puede ser superior: cuando eventualmente estás tratando con Cadenas, por ejemplo si estás creando XML o JSON. En ese caso, en lugar de algo como:

public void addAttribute(String s) { 
  //... add s to the XML
}

Prefiero:

public void addAttribute(Object o) {
   String s = String.valueOf(o);  // perhaps check for null explicitly
   // now add s to the XML
}

Esto guarda interminables String.valueOf 's en el código de llamada. Además, funciona bien con vararg 's. Y, con el autoboxing, puedes pasar ints , floats , etc ... Ya que todos los Objetos tienen un toString() , esto, posiblemente, no viola a Liskov, etc. ...

    
respondido por el user949300 20.09.2013 - 21:47
5

si. Se supone que java es estrictamente typecast ed. Pero tener Object como parámetro virtualmente romperá esta regla. Además, hará que su programa sea más propenso a errores.

Y esa es una de las razones principales por las que se introdujo generics .

    
respondido por el Ankit 14.05.2013 - 18:57
5

Ser flexible y capaz de manejar múltiples tipos de datos es exactamente para lo que se crearon la sobrecarga de métodos y las interfaces. Java ya tiene las características que simplifican el manejo de estas situaciones al tiempo que garantizan la seguridad del tiempo de compilación a través de la verificación de tipos.

Además, si es imposible definir de manera restringida en qué datos opera el método, entonces ese método puede estar haciendo demasiado y debe descomponerse en métodos más pequeños.

Me gusta una regla general sobre la que he leído (en Código Completo, creo). La regla establece que debería poder decir qué hace una función o método con solo leer su nombre. Si es realmente difícil nombrar claramente una función \ método usando esa idea (o tiene que usar Y en el nombre), entonces la función / método realmente está haciendo demasiado.

    
respondido por el Peter Smith 14.05.2013 - 19:29
2

Primero, si pasa los parámetros como el objeto concreto, cualquier error de usar el objeto incorrecto (instancia de una clase no esperada) se detectará en tiempo de compilación, por lo que será más rápido de solucionar. En el otro caso, el error se mostrará en tiempo de ejecución y será más difícil de detectar y corregir.

En segundo lugar, el código de verificación de la clase real de la instancia, la reflexión, etc., será más lento que una llamada directa

    
respondido por el Evans 14.05.2013 - 18:57
2

La regla que sigo es que ... las definiciones de parámetros deben ser tan específicas como lo requiera el método, ni más ni menos.

Si todo lo que necesito es iterar un enumerable, el parámetro se define como un enumerable, no una matriz o una lista. Esto permite utilizar cualquier tipo de enumerable, evitando la conversión a un tipo específico.

Tampoco lo definiré como un objeto porque necesito un enumerable. Cualquier objeto que no sea un enumerable no funcionará en este método. Es confuso leer la firma de un método y no saber qué significa, y solo las pruebas en tiempo de ejecución pueden encontrar el error.

    
respondido por el drewburlingame 14.05.2013 - 20:38
2

Lo que hace es deshacerse de todas las ventajas de tener un sistema de tipo estático.

Simplemente: Es mucho menos claro lo que hacen estos métodos, tanto para un compilador como para las personas que leen el código.

Una de las razones más importantes para un sistema de tipo estático es tener garantías de compilación sobre lo que hace un programa . El compilador puede verificar que un fragmento de código se usa correctamente y, de no ser así, emitir un error de compilación. Los métodos como los que está escribiendo un colega son mucho más propensos a errores en tiempo de ejecución, porque faltan las verificaciones en tiempo de compilación. Esté preparado para errores de ejecución y para escribir muchas más pruebas.

Otro gran problema es que no está del todo claro qué hace un método. Leer este código, mantenerlo o usarlo es un gran dolor.

Java agregó genéricos para ofrecer mayores garantías de tiempo de compilación sobre el código, Scala va aún más lejos con su enfoque funcional y su sofisticado sistema de tipos con tipos covariantes y contra-variantes. Su colega va en contra de esta línea de desarrollo, desechando todas sus ventajas.

Si en su lugar usara métodos sobrecargados y correctamente escritos, él (y todos ustedes) obtendrían:

  • Verificación del tiempo del compilador para el uso correcto de esos métodos.
  • Indicación clara de qué combinaciones de argumentos se permiten y cuáles no.
  • La posibilidad de documentar de manera adecuada y por separado cada una de estas combinaciones.
  • Código de legibilidad y mantenimiento.
  • Le ayudaría a dividir sus métodos en partes más pequeñas, que son más fáciles de depurar, probar y mantener. (Consulte también ¿Cómo convencer a su compañero desarrollador para que escriba métodos cortos? .)
respondido por el Petr Pudlák 23.05.2017 - 14:40
2

En realidad, tuve que trabajar en un paquete como ese una vez, era un paquete GUI. El diseñador dijo que provenía de un entorno de Pascal y pensaba que no era más que una clase base en todas partes.

En 20 años, trabajar con este kit de herramientas fue una de las experiencias más molestas de mi carrera.

Hasta que tenga que trabajar con un código como ese, no se dará cuenta de cuánto confía en los tipos de datos en sus parámetros para guiarlo. Si dije que tenía un método "connect (Object dest)", ¿qué significa? Incluso si ves "conectar (sitio web de objetos)" no es de mucha ayuda, pero "conectar (sitio web de cadenas)" o "conectar (sitio web de URL)" ni siquiera tienes que pensar en ello.

Podría pensar que "conectar (sitio web de objetos)" es una GRAN idea porque puede usar una cadena o una URL, pero eso solo significa que tengo que adivinar qué casos el programador decidió apoyar y cuáles no. En el mejor de los casos, mueve la mayor parte de la parte de prueba y error del desarrollo del tiempo de codificación al tiempo de compilación, en el peor de los casos, hace que todo el proceso de desarrollo sea confuso y molesto.

Yo (y la mayoría de los programadores de Java, creo) constantemente desciframos las cadenas de llamadas API al examinar un grupo de parámetros a los constructores para ver qué constructor usar, luego cómo construir los objetos que necesita pasar hasta que llegue a los objetos que tener o poder hacer - es como armar un rompecabezas en lugar de buscar documentación incompleta en busca de ejemplos incompletos (mi experiencia pre-java con C).

Aunque el código de construcción como un rompecabezas parece un problema, funciona increíblemente bien, todo el tiempo.

Sé que esto ha sido respondido, pero esta fue una experiencia lo suficientemente molesta que realmente me sentí motivado para asegurarme de que la menor cantidad posible de personas la repita.

    
respondido por el Bill K 15.05.2013 - 07:06
1

Para dar algo a un tipo de dar:

  1. Los usuarios utilizan una interfaz para codificar con una descripción semántica.
  2. Compila una interfaz desde la cual analizar para permitir que sus optimizadores aumenten el tiempo de ejecución del código o reduzcan el tamaño del código más fácilmente.
  3. Enlace inicial (estático) que detecta errores de código al principio del ciclo de desarrollo.

El uso de solo un objeto no transmite casi ninguna información, por lo que se eliminan todas las funciones que se colocaron en el lenguaje por los motivos mencionados anteriormente.

Además, el sistema de tipo fue diseñado para ser flexible con sus relaciones is-a que permiten todas estas condiciones. Así que úsalo a tu favor. No lo desperdicies haciendo un código que no se puede mantener.

Puede haber una razón particular para hacer algo como esto en un momento determinado, pero hacerlo de forma genérica es una práctica deficiente en todo momento. Si lo está haciendo, haga que otros revisen el problema y vean si se puede hacer de otra manera. En la mayoría de los casos (99,999%) puede ser.

En ese 0,001% de lo que no puede ser, al menos todos están a bordo y saben lo que está pasando (aunque probablemente solo necesiten nuevos ojos sobre el problema).

    
respondido por el Adrian 14.05.2013 - 20:31
1

Sí, esta es una mala práctica porque no garantiza que el método pueda hacer lo que necesita para cualquier entrada particular que se pase. El objetivo de hacer un método que sea ampliamente utilizable no es un objetivo malo, pero Hay mejores maneras de lograrlo.

Para este caso en particular, suena como una situación perfecta para el uso de interfaces. Una interfaz garantizaría que los objetos que se pasan al método proporcionen la información necesaria y permitan evitar la conversión. Hay algunas situaciones en las que esto no funcionará bien (como si tuviera que trabajar con clases base o selladas a las que no se puede agregar una implementación de interfaz), pero en general, un método solo debe tomar información que sea válida para ella como parámetros.

    
respondido por el AJ Henderson 14.05.2013 - 23:11
0

Grandes respuestas en profundidad hasta ahora, realmente tengo que decir que, incluso antes de entrar en la filosofía y metodología de alto nivel, son perezosas, breves y simples.

No están proporcionando buena información en su código para que otros puedan mantener y entender, y van en contra del diseño del lenguaje de programación en cuestión. Amo a PERL, amo a Objective-C. Sin embargo, no me esfuerzo por doblar Java, C # o C ++ en este sentido, hacia mis preferencias a costa de tener un código mantenible, legible y eficiente.

Sí, si necesitan cambiar cómo funciona una función, o cuáles son las entradas, guardan algunos cambios de línea aquí y algunos pasos de refactorización. Sin embargo, el costo para otros que tienen que mantener el código es mayor, sin mencionar los IDE más modernos, como eclipse y Xcode pueden manejar la refactorización automáticamente con un simple comando de clic derecho. Además, como han mencionado otros, el costo de procesamiento aumenta con cada lanzamiento, con cada tipo de objeto desconocido que se pasa, y me imagino que las mejoras básicas incorporadas que el tiempo de ejecución realizaría al saber las clases adecuadas también se eliminan por la ventana.

La sintaxis del azúcar siempre es mejor que la sal para el desarrollador, pero una dieta equilibrada de ambos garantiza la salud del proyecto y de otras personas involucradas (cliente, aplicación, otros desarrolladores, etc.)

    
respondido por el FaultyJuggler 14.05.2013 - 22:22
-2

No hay problema con esto. En los idiomas que no tienen variables escritas, esto es lo que sucede en cada función. Los parámetros formales son solo valores, y el tipo está en el objeto que se pasa, no en el parámetro formal.

La gente se hace cosas (TM) en Lisp, Python, Ruby, ...

Aunque se beneficia menos de la comprobación de tipos de tiempo de compilación, también sufre menos de la comprobación de tipos de tiempo de compilación.

La solución real es usar un lenguaje dinámico que se dirija a la máquina virtual Java, en lugar de escribir un código Java feo en el que cada tercer identificador es la palabra Object .

Tengo sentido tener una función completamente genérica llamada construct que toma objetos arbitrarios.

Creo que me gustaría colaborar con este programador. Se le quitaría algo de importancia al uso de un lenguaje de programación bletcheros.

Tenga en cuenta que este tipo de código aparece en C en la implementación del soporte en tiempo de ejecución para lenguajes dinámicos implementados en C. E.g. Es posible que vea algo como esto en un Lisp implementado en C:

/* Map each object in a list through a function, in order to produce
   a list of the return values. */

val mapcar(val function, val list)
{
   val iter = car(list);
   val out = nil;  /* nil is really a null pointer */

   for (iter = car(list); iter; iter = cdr(iter))
      push(funcall_1(function, car(iter)), out);  /* push is a C macro */

   return nreverse(out); /* destructively reverse list */
}

El tipo val es en realidad algo como:

typedef struct object *val;

y struct object es una estructura complicada que superpone varios tipos de información de tipo para varios tipos de objetos.

Desarrollé un pequeño lenguaje de programación muy agradable, cuyos elementos internos están hechos en este estilo, que deliberadamente mantuve lo más limpio posible: más que en el interior de algún otro lenguaje de implementación que tenga un enfoque similar.

En este estilo, las cosas asombrosas son posibles en una pequeña cantidad de código C.

    
respondido por el Kaz 14.05.2013 - 23:35
-3

Supongamos por un minuto que "Object guy" tiene motivos para utilizar un objeto genérico "suelto". No conocemos sus razones, pero me recuerda el valor de usar la herencia y los genéricos (plantillas). En la remota posibilidad de que Object-guy realmente esté tratando de tener sentido, este es el sentido que podría estar intentando hacer:

Siempre baso mis objetos en un solo objeto base. Este objeto base maneja tareas adicionales como manejo de excepciones, registro, creación de perfiles y recolección de basura.

Siempre baso los objetos matemáticos (aquellos que pueden admitir operadores matemáticos) en una sola clase base. Esto ayuda a proporcionar utilidades para manejar excepciones en el caso de que una clase derivada no maneje una determinada operación. Por ejemplo, al usar una serie de Taylor en caso de que no esté disponible una representación explícita para una función.

Por lo tanto, no sé. Las razones de los Object-guys son: tal vez solo porque le gustan las tipografías dinámicas que ofrecen otros idiomas. Pero quizás él está tratando de apreciar el valor de la herencia.

También puede querer explorar los genéricos (plantillas). Los usé en C ++ para definir imágenes de dos, tres y cuatro dimensiones, donde los píxeles pueden ser bytes, ints, flotadores, complejos o espectros. Tuve que hacer un poco de casting, pero la recompensa fue significativa en términos de separar la geometría de las imágenes del cálculo de píxeles.

    
respondido por el xxyzzy 14.05.2013 - 23:18
-6

No olvide que este es un buen patrón para lenguajes como JavaScript que no admite el paso de parámetros por nombre. Considera una función

function dialog( message, cancelButton, okText, notOkText ) { ... } 

que tiene que ser llamado como

dialog( "...", true, "...", "..." );

Cuando estudias el código del cliente, no es fácil recordar el significado de, por ejemplo, el tercer parámetro de la llamada. La legibilidad del código del cliente sufre. Por lo tanto, es mejor declararlo con un objeto param

function dialog( options ) { ... }

que te permite llamarlo con parámetros con nombre:

dialog({ 
  message : "...",
  cancelButton: true,
  okText: "...",
  notOkText: "..."
  })

Principio: deberíamos reducir el número de parámetros de una función (cero es excelente, uno es frecuente, dos y más deberían ser raros)!

    
respondido por el rplantiko 14.05.2013 - 22:23

Lea otras preguntas en las etiquetas