Tratar de no saber los nombres de los parámetros de una función cuando la está llamando

13

Aquí hay un problema de programación / lenguaje en el que me gustaría escuchar sus opiniones.

Hemos desarrollado convenciones que la mayoría de los programadores (deberían) seguir que no forman parte de la sintaxis de los idiomas pero sirven para hacer que el código sea más legible. Estos son, por supuesto, siempre una cuestión de debate, pero hay al menos algunos conceptos básicos que la mayoría de los programadores encuentran agradables. Nombrar sus variables de manera apropiada, nombrarlas en general, hacer que sus líneas no sean escandalosamente largas, evitando funciones largas, encapsulaciones, esas cosas.

Sin embargo, hay un problema que todavía tengo que encontrar a alguien que comenta y ese podría ser el más grande de todos. Es el problema de que los argumentos sean anónimos cuando llamas a una función.

Las funciones se derivan de las matemáticas, donde f (x) tiene un significado claro porque una función tiene una definición mucho más rigurosa que la que normalmente tiene en la programación. Las funciones puras en matemáticas pueden hacer mucho menos de lo que pueden en la programación y son una herramienta mucho más elegante, generalmente solo toman un argumento (que generalmente es un número) y siempre devuelven un valor (también generalmente un número). Si una función toma múltiples argumentos, casi siempre son solo dimensiones adicionales del dominio de la función. En otras palabras, un argumento no es más importante que los otros. Están explícitamente ordenados, claro, pero aparte de eso, no tienen ordenamiento semántico.

Sin embargo, en la programación, tenemos más funciones que definen la libertad, y en este caso, diría que no es algo bueno. Una situación común, tienes una función definida como esta

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Mirando la definición, si la función está escrita correctamente, está perfectamente claro qué es qué. Al llamar a la función, es posible que en su IDE / editor esté activada alguna magia de finalización de código inteligente que le indicará cuál será el siguiente argumento. Pero espera. Si necesito eso cuando en realidad estoy escribiendo la llamada, ¿no hay algo que nos falte aquí? La persona que lee el código no tiene el beneficio de un IDE y, a menos que salte a la definición, no tiene idea de cuál de los dos rectángulos pasados como argumentos se usa para qué.

El problema va incluso más allá de eso. Si nuestros argumentos provienen de alguna variable local, puede haber situaciones en las que ni siquiera sabemos cuál es el segundo argumento, ya que solo vemos el nombre de la variable. Tomemos, por ejemplo, esta línea de código

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Esto se reduce a varias extensiones en diferentes idiomas, pero incluso en idiomas estrictamente tipificados e incluso si nombra sus variables con sensatez, ni siquiera menciona el tipo de variable cuando se lo pasa a la función.

Como suele ocurrir con la programación, hay muchas soluciones potenciales para este problema. Muchos ya están implementados en lenguajes populares. Parámetros nombrados en C # por ejemplo. Sin embargo, todo lo que sé tiene importantes inconvenientes. Nombrar cada parámetro en cada llamada de función no puede llevar a un código legible. Casi parece que tal vez estamos superando las posibilidades que nos brinda la programación de texto simple. Hemos pasado del texto JUST en casi todas las áreas, pero aún así codificamos lo mismo. ¿Se necesita más información para mostrar en el código? Añadir más texto. De todos modos, esto se está volviendo un poco tangencial, así que me detendré aquí.

Una de las respuestas que obtuve al segundo fragmento de código es que probablemente primero desempaquetarás la matriz de algunas variables con nombre y luego las usarás, pero el nombre de la variable puede significar muchas cosas y la forma en que se llama no necesariamente te dice la forma Se supone que debe interpretarse en el contexto de la función llamada. En el ámbito local, puede tener dos rectángulos llamados leftRectangle y rightRectangle porque eso es lo que representan semánticamente, pero no es necesario que se extiendan a lo que representan cuando se les asigna una función.

De hecho, si sus variables se nombran en el contexto de la función llamada, se está introduciendo menos información de la que podría potencialmente con esa llamada de función y en algún nivel si eso lleva al código al código peor. Si tiene un procedimiento que resulta en un rectángulo que almacena en rectForClipping y luego otro procedimiento que proporciona rectForDrawing, entonces la llamada real a DrawRectangleClipped es simplemente una ceremonia. Una línea que no significa nada nuevo y está ahí solo para que la computadora sepa qué es exactamente lo que desea, incluso aunque ya lo haya explicado con su nombre. Esto no es algo bueno.

Realmente me encantaría escuchar nuevas perspectivas sobre esto. Estoy seguro de que no soy el primero en considerar esto como un problema, entonces, ¿cómo se resuelve?

    
pregunta Darwin 21.05.2014 - 18:04

5 respuestas

9

Estoy de acuerdo en que la forma en que se usan las funciones a menudo puede ser una parte confusa de escribir código, y especialmente de leer código.

La respuesta a este problema depende en parte del idioma. Como mencionaste, C # ha nombrado parámetros. La solución de Objective-C a este problema involucra nombres de métodos más descriptivos. Por ejemplo, stringByReplacingOccurrencesOfString:withString: es un método con parámetros claros.

En Groovy, algunas funciones toman mapas, lo que permite una sintaxis como la siguiente:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

En general, puede resolver este problema limitando el número de parámetros que pasa a una función. Creo que 2-3 es un buen límite. Si parece que una función necesita más parámetros, me hace repensar el diseño. Pero, esto puede ser más difícil de responder en general. A veces intentas hacer demasiado en una función. A veces tiene sentido considerar una clase para almacenar sus parámetros. Además, en la práctica, a menudo encuentro que las funciones que toman una gran cantidad de parámetros normalmente tienen muchas de ellas como opcionales.

Incluso en un lenguaje como Objective-C tiene sentido limitar el número de parámetros. Una razón es que muchos parámetros son opcionales. Para ver un ejemplo, consulte rangeOfString: y sus variaciones en NSString .

Un patrón que uso a menudo en Java es usar una clase de estilo fluido como parámetro. Por ejemplo:

something.draw(new Box().withHeight(5).withWidth(20))

Esto usa una clase como parámetro, y con una clase de estilo fluido, hace que el código sea fácil de leer.

El fragmento de código Java anterior también ayuda cuando el orden de los parámetros puede no ser tan obvio. Normalmente asumimos con las coordenadas que X viene antes de Y. Y normalmente veo la altura antes del ancho como una convención, pero aún no está muy claro ( something.draw(5, 20) ).

También he visto algunas funciones como drawWithHeightAndWidth(5, 20) pero incluso estas no pueden tomar demasiados parámetros, o comenzarías a perder la legibilidad.

    
respondido por el David V 21.05.2014 - 18:59
11

En su mayoría se resuelve con una buena denominación de funciones, parámetros y argumentos. Ya exploraste eso y encontraste que tenía deficiencias, sin embargo. La mayoría de esas deficiencias se mitigan manteniendo las funciones pequeñas, con un pequeño número de parámetros, tanto en el contexto de llamada como en el contexto de llamada. Su ejemplo particular es problemático porque la función a la que está llamando trata de hacer varias cosas a la vez: especifique un rectángulo base, especifique una región de recorte, dibuje y rellénela con un color específico.

Esto es como intentar escribir una oración usando solo los adjetivos. Ponga más verbos (llamadas a funciones) allí, cree un sujeto (objeto) para su oración, y es más fácil de leer:

rect.clip(clipRect).fill(color)

Incluso si clipRect y color tienen nombres terribles (y no deberían), aún puede distinguir sus tipos desde el contexto.

Su ejemplo de deserialización es problemático porque el contexto de la llamada está tratando de hacer demasiado a la vez: deserializar y dibujar algo. Debe asignar nombres que tengan sentido y separar claramente las dos responsabilidades. Como mínimo, algo como esto:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Muchos problemas de legibilidad son causados por tratar de ser demasiado concisos, omitiendo etapas intermedias que los humanos requieren para discernir el contexto y la semántica.

    
respondido por el Karl Bielefeldt 21.05.2014 - 18:57
3

En la práctica, se resuelve con un mejor diseño. Es excepcionalmente infrecuente que las funciones bien escritas tomen más de 2 entradas, y cuando esto ocurre, es poco común que esas muchas entradas no puedan agregarse en algún paquete cohesivo. Esto hace que sea bastante fácil dividir las funciones o agregar parámetros para que no esté haciendo que una función haga demasiado. Uno tiene dos entradas, se vuelve fácil de nombrar y mucho más claro sobre qué entrada es cuál.

Mi lenguaje de juguetes tenía el concepto de frases para lidiar con esto, y otros Los lenguajes de programación más centrados en el lenguaje natural han tenido otros enfoques para enfrentarlo, pero todos ellos tienden a tener otros inconvenientes. Además, incluso las frases son poco más que una buena sintaxis para hacer que las funciones tengan mejores nombres. Siempre será difícil hacer un buen nombre de función cuando se necesita un montón de entradas.

    
respondido por el Telastyn 21.05.2014 - 18:40
1

En Javascript (o ECMAScript ), por ejemplo, muchos programadores se acostumbraron a

  

pasar parámetros como un conjunto de propiedades de objeto con nombre en un solo objeto anónimo.

Y como práctica de programación, llegó de los programadores a sus bibliotecas y de allí a otros programadores que crecieron para que les guste y lo usen y escriban más bibliotecas, etc.

Example

En lugar de llamar

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

como este:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, que es un estilo válido y correcto, llamas a

function drawRectangleClipped (params)

como este:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, que es válido y correcto y agradable con respecto a su pregunta.

Por supuesto, tiene que haber condiciones adecuadas para esto - en Javascript esto es mucho más viable que en, por ejemplo, C. En javascript, esto incluso dio origen a la notación estructural ahora ampliamente utilizada que se hizo popular como una contraparte más ligera XML. Se llama JSON (es posible que ya hayas oído hablar de él).

    
respondido por el Pavel 22.05.2014 - 10:29
0

Debería usar el objetivo-C entonces, aquí hay una definición de función:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

Y aquí se usa:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Creo que Ruby tiene construcciones similares y puedes simularlas en otros idiomas con listas de valores-clave.

Para funciones complejas en Java, me gusta definir variables ficticias en la redacción de las funciones. Para su ejemplo de izquierda a derecha:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Parece más codificación, pero, por ejemplo, puedes usar leftRectangle y luego refactorizar el código más adelante con "Extraer la variable local" si crees que no será comprensible para un futuro mantenedor del código, lo cual podría o no serlo. .

    
respondido por el awsm 22.05.2014 - 00:47

Lea otras preguntas en las etiquetas