¿Por qué no es más común codificar los nombres de argumentos en nombres de funciones? [cerrado]

47

En Clean Code el autor da un ejemplo de

assertExpectedEqualsActual(expected, actual)

vs

assertEquals(expected, actual)

el primero afirmó ser más claro porque elimina la necesidad de recordar dónde van los argumentos y el posible mal uso que se deriva de eso. Sin embargo, nunca he visto un ejemplo del esquema de denominación anterior en ningún código y veo este último todo el tiempo. ¿Por qué los programadores no adoptan el primero si es, como afirma el autor, más claro que el segundo?

    
pregunta EternalStudent 30.06.2018 - 14:39

8 respuestas

64

Porque es más para escribir y más para leer

La razón más simple es que a las personas les gusta escribir menos, y codificar esa información significa escribir más. Al leerlo, cada vez que tengo que leerlo todo, incluso si estoy familiarizado con el orden de los argumentos. Incluso si no está familiarizado con el orden de los argumentos ...

Muchos desarrolladores usan IDE

Los IDE

a menudo proporcionan un mecanismo para ver la documentación de un método determinado al pasar el mouse o mediante un método abreviado de teclado. Debido a esto, los nombres de los parámetros están siempre disponibles.

La codificación de los argumentos introduce duplicación y acoplamiento

Los nombres de los parámetros ya deberían documentar lo que son. Al escribir los nombres en el nombre del método, también estamos duplicando esa información en la firma del método. También creamos un acoplamiento entre el nombre del método y los parámetros. Supongamos que expected y actual son confusos para nuestros usuarios. Pasar de assertEquals(expected, actual) a assertEquals(planned, real) no requiere cambiar el código del cliente usando la función. Pasar de assertExpectedEqualsActual(expected, actual) a assertPlannedEqualsReal(planned, real) significa un cambio importante en la API. O no cambiamos el nombre del método, que rápidamente se vuelve confuso.

Utilice tipos en lugar de argumentos ambiguos

El problema real es que tenemos argumentos ambiguos que se cambian fácilmente porque son del mismo tipo. En su lugar, podemos usar nuestro sistema de tipos y nuestro compilador para imponer el orden correcto:

class Expected<T> {
    private T value;
    Expected(T value) { this.value = value; }
    static Expected<T> is(T value) { return new Expected<T>(value); }
}

class Actual<T> {
    private T value;
    Actual(T value) { this.value = value; }
    static Actual<T> is(T value) { return new Actual<T>(value); }
}

static assertEquals(Expected<T> expected, Actual<T> actual) { /* ... */ }

// How it is used
assertEquals(Expected.is(10), Actual.is(x));

Esto se puede hacer cumplir a nivel de compilador y garantiza que no se pueden obtener al revés. Al aproximarse desde un ángulo diferente, esto es esencialmente lo que hace la biblioteca Hamcrest para las pruebas.

    
respondido por el cbojar 30.06.2018 - 16:34
20

Usted pregunta acerca de un debate de larga data en la programación. ¿Cuánta verbosidad es buena? Como respuesta general, los desarrolladores han descubierto que la verbosidad adicional que da nombre a los argumentos no vale la pena.

La verbosidad no siempre significa más claridad. Considera

copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)

versus

copy(output, source)

Ambos contienen el mismo error, pero ¿realmente hicimos que sea más fácil encontrar ese error? Como regla general, lo más fácil de depurar es cuando todo está totalmente terso, excepto las pocas cosas que tienen el error, y esas son lo suficientemente detalladas como para decirle qué fue lo que no funcionó.

Hay una larga historia de agregar verbosidad. Por ejemplo, existe la " notación húngara " que generalmente nos da nombres maravillosos como lpszName . Eso generalmente ha caído en el camino en la población general de programadores. Sin embargo, agregar caracteres a los nombres de variables miembro (como mName o m_Name o name_ ) continúa teniendo popularidad en algunos círculos. Otros dejaron eso por completo. Resulta que trabajo en una base de código de simulación física cuyos documentos de estilo de codificación requieren que cualquier función que devuelva un vector deba especificar el marco del vector en la llamada de función ( getPositionECEF ).

Es posible que te interesen algunos de los idiomas que popularizó Apple. Objective-C incluye los nombres de los argumentos como parte de la firma de la función (la función [atm withdrawFundsFrom: account usingPin: userProvidedPin] está escrita en la documentación como withdrawFundsFrom:usingPin: . Ese es el nombre de la función). Swift tomó un conjunto similar de decisiones y le pidió que pusiera los nombres de los argumentos en las llamadas de función ( greet(person: "Bob", day: "Tuesday") ).

    
respondido por el Cort Ammon 01.07.2018 - 03:21
8

El autor de "Clean Code" señala un problema legítimo, pero su solución sugerida es bastante poco elegante. Por lo general, hay mejores maneras de mejorar los nombres de los métodos poco claros.

Tiene razón en que assertEquals (de las bibliotecas de prueba de unidades de estilo xUnit) no deja claro qué argumento es el esperado y cuál es el real. Esto también me ha mordido! Muchas bibliotecas de pruebas unitarias han observado el problema y han introducido sintaxis alternativas, como:

actual.Should().Be(expected);

O similar. Lo que sin duda es mucho más claro que assertEquals pero también mucho mejor que assertExpectedEqualsActual . Y también es mucho más composable.

    
respondido por el JacquesB 30.06.2018 - 17:52
5

Estás tratando de guiar tu camino entre Escila y Caribdis hacia la claridad, tratando de evitar la verbosidad inútil (también conocida como divagación sin rumbo), así como la excesiva brevedad (también conocida como terseness críptica).

Por lo tanto, tenemos que ver la interfaz que desea evaluar, una forma de hacer afirmaciones de depuración de que dos objetos son iguales.

  1. ¿Hay alguna otra función que podría estar considerando la aridad y el nombre?
    No, por lo que el nombre en sí es lo suficientemente claro.
  2. ¿Son los tipos de algún significado?
    No, así que vamos a ignorarlos. ¿Ya hiciste eso? Bien.
  3. ¿Es simétrico en sus argumentos?
    Casi, en caso de error, el mensaje coloca cada representación de argumentos en su propio lugar.

Por lo tanto, veamos si esa pequeña diferencia es de alguna importancia y no está cubierta por las convenciones vigentes existentes.

¿Es la audiencia deseada un inconveniente si los argumentos se intercambian involuntariamente?
No, los desarrolladores también obtienen un seguimiento de la pila y tienen que examinar el código fuente de todos modos para corregir el error.
Incluso sin un seguimiento completo de la pila, la posición de las aserciones resuelve esa pregunta. Y si aún falta eso y no es obvio por el mensaje que es cuál, a lo sumo duplica las posibilidades.

¿El orden de los argumentos sigue la convención?
Parece ser el caso. Aunque en el mejor de los casos parece una convención débil.

Por lo tanto, la diferencia parece bastante insignificante, y el orden de los argumentos está cubierto por una convención lo suficientemente fuerte como para que cualquier esfuerzo por incluirla en el nombre de la función tenga una utilidad negativa.

    
respondido por el Deduplicator 01.07.2018 - 16:34
3

A menudo no agrega ninguna claridad lógica.

Compare "Agregar" a "AddFirstArgumentToSecondArgument".

Si necesita una sobrecarga que, por ejemplo, agrega tres valores. ¿Qué tendría más sentido?

¿Otro "Agregar" con tres argumentos?

o

"AddFirstAndSecondAndThirdArgument"?

El nombre del método debe transmitir su significado lógico. Debe decir lo que hace. Decir, en un nivel micro, qué pasos da no hace que sea más fácil para el lector. Los nombres de los argumentos proporcionarán detalles adicionales si es necesario. Si aún necesita más detalles, el código estará allí para usted.

    
respondido por el Martin Maat 30.06.2018 - 15:50
2

Me gustaría agregar algo más que se sugiera en otras respuestas, pero no creo que se haya mencionado explícitamente:

@puck dice "Todavía no hay garantía de que el primer parámetro mencionado en el nombre de la función sea realmente el primer parámetro".

@cbojar dice "Usa tipos en lugar de argumentos ambiguos"

El problema es que los lenguajes de programación no entienden los nombres: solo son tratados como opacos, símbolos atómicos. Por lo tanto, al igual que con los comentarios de código, no hay necesariamente una correlación entre el nombre de una función y la forma en que realmente funciona.

Compare assertExpectedEqualsActual(foo, bar) con algunas alternativas (de esta página y de otra parte), como:

# Putting the arguments in a labelled structure
assertEquals({expected: foo, actual: bar})

# Using a keyword arguments language feature
assertEquals(expected=foo, actual=bar)

# Giving the arguments different types, forcing us to wrap them
assertEquals(Expected(foo), Actual(bar))

# Breaking the symmetry and attaching the code to one of the arguments
bar.Should().Be(foo)

Todos estos tienen más estructura que el nombre detallado, lo que le da al lenguaje algo no opaco para mirar. La definición y el uso de la función también depende de esta estructura, por lo que no puede desincronizarse con lo que está haciendo la implementación (como puede hacer un nombre o comentario).

Cuando me enfrento o veo un problema como este, antes de gritar a mi computadora con frustración, primero me tomo un momento para preguntar si es "justo" culpar a la máquina. En otras palabras, ¿se le dio a la máquina suficiente información para distinguir lo que quería de lo que pedí?

Una llamada como assertEqual(expected, actual) tiene tanto sentido como assertEqual(actual, expected) , por lo que es fácil para nosotros mezclarlos y para que la máquina siga adelante y haga lo incorrecto. Si en su lugar usamos assertExpectedEqualsActual , podría hacer que us sea menos probable que cometamos un error, pero no proporciona más información a la máquina (no puede entender el inglés y la elección del nombre no debería afectar) semántica).

Lo que hace que los enfoques "estructurados" sean más preferibles, como los argumentos de palabras clave, los campos etiquetados, los tipos diferenciados, etc., es que la información adicional también es legible por máquina , por lo que podemos hacer que la máquina sea incorrecta Usos y ayudarnos a hacer las cosas bien. El caso assertEqual no es tan malo, ya que el único problema serían los mensajes inexactos. Un ejemplo más siniestro podría ser String replace(String old, String new, String content) , que es fácil de confundir con String replace(String content, String old, String new) que tiene un significado muy diferente. Un remedio simple sería tomar un par [old, new] , lo que cometería errores de manera inmediata (incluso sin tipos).

Tenga en cuenta que incluso con los tipos, podemos encontrarnos a nosotros mismos 'sin decirle a la máquina lo que queremos'. Por ejemplo, el anti-patrón llamado "programación tipificada rigurosamente" trata a todos los datos como cadenas, lo que hace que sea fácil mezclar argumentos (como este caso), olvidarse de realizar algún paso (por ejemplo, escapar), romper accidentalmente las invariantes (por ejemplo, haciendo JSON irreparable), etc.

Esto también está relacionado con la "ceguera booleana", donde calculamos un montón de booleanos (o números, etc.) en una parte del código, pero cuando intentamos usarlos en otra no está claro de qué se trata. representando si los hemos mezclado, etc. Compare esto con, por ejemplo, enumeraciones distintas que tienen nombres descriptivos (por ejemplo, LOGGING_DISABLED en lugar de false ) y que causan un mensaje de error si los confundimos.

    
respondido por el Warbo 02.07.2018 - 17:07
1
  

porque elimina la necesidad de recordar dónde van los argumentos

¿Realmente? Todavía no hay garantía de que el primer parámetro mencionado sea el primer argumento mencionado en el nombre de la función. Así que mejor búscalo (o deja que tu IDE haga eso) y quédate con nombres razonables que confía ciegamente en un nombre bastante tonto.

Si lees el código, deberías ver fácilmente qué sucede cuando los parámetros se nombran como deberían ser. copy(source, destination) es mucho más fácil de entender que algo como copyFromTheFirstLocationToTheSecondLocation(placeA, placeB) .

  

¿Por qué los programadores no adoptan el primero si es, como afirma el autor, más claro que el segundo?

Porque hay diferentes puntos de vista en diferentes estilos y puedes encontrar x autores de otros artículos que afirman lo contrario. Te volverías loco intentando seguir todo lo que alguien escribe en alguna parte ;-)

    
respondido por el puck 01.07.2018 - 15:16
0

Estoy de acuerdo en que la codificación de nombres de parámetros en nombres de funciones hace que la escritura y el uso de las funciones sean más intuitivos.

copyFromSourceToDestination( // "...ahh yes, the source directory goes first"

Es fácil olvidar el orden de los argumentos en las funciones y comandos de shell, y muchos programadores confían en las características de IDE o las referencias de funciones por esta razón. Tener los argumentos descritos en el nombre sería una solución elocuente a esta dependencia.

Sin embargo, una vez que se escribe, la descripción de los argumentos se vuelve redundante para el siguiente programador que tiene que leer la declaración, ya que en la mayoría de los casos se usarán variables con nombre.

copy(sourceDir, destinationDir); // "...makes sense"

La brusquedad de esto ganará a la mayoría de los programadores y personalmente me parece más fácil de leer.

EDITAR: Como señaló @Blrfl, los parámetros de codificación no son tan "intuitivos" después de todo, ya que necesita recordar el nombre de la función en primer lugar. Esto requiere buscar referencias de funciones o obtener ayuda de un IDE que, de todos modos, proporcionará información de pedido de parámetros.

    
respondido por el Josh Taylor 30.06.2018 - 16:42

Lea otras preguntas en las etiquetas