¿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 afirma ser más claro porque elimina la necesidad de recordar a 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 nomenclatura anterior en ningún código y veo el último todo el tiempo. ¿Por qué los codificadores no adoptan el primero si es, como afirma el autor, más claro que el segundo?

Estudiante eterno
fuente
99
Creo que esta es una gran pregunta para una discusión. Pero no es algo que pueda responderse con una respuesta objetiva. Entonces esta pregunta podría cerrarse como basada en la opinión.
Eufórico el
54
Muchas personas argumentarían en contra del primer esquema de nombres porque es excesivamente detallado , mucho más allá del punto en que ayudaría a la claridad. Especialmente para assertEquals(), ese método se usa cientos de veces en una base de código, por lo que se puede esperar que los lectores se familiaricen con la convención una vez. Diferentes marcos tienen diferentes convenciones (p. Ej. (actual, expected) or an agnostic (Izquierda, derecha) `), pero en mi experiencia eso es como mucho una fuente menor de confusión.
amon
55
Debido a que la ganancia es tan pequeña, en comparación con sus beneficios, cualquier persona sensata probablemente se iría. Si desea un enfoque más fluido , debe intentar assert(a).toEqual(b)(incluso si es IMO, todavía es innecesariamente detallado) donde puede encadenar algunas afirmaciones relacionadas.
Adriano Repetti
18
¿Cómo sabemos que los valores reales y esperados son valores? Seguramente debería ser assertExpectedValueEqualsActualValue? Pero espera, ¿cómo podemos recordar si se utiliza ==o .equalso Object.equals? Debe ser assertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter?
user253751
66
Dado que, para este método en particular, el orden de los dos argumentos no importa, parece un mal ejemplo para elegir exponer los beneficios de este esquema de nombres.
Steven Rands

Respuestas:

66

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 IDEs

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

Codificar 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. Decir expectedy actualson 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.

Use tipos en lugar de argumentos ambiguos

El verdadero problema es que tenemos argumentos ambiguos que se cambian fácilmente porque son del mismo tipo. En cambio, 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 en el nivel del compilador y garantiza que no se puedan hacer retroceder. Al acercarse desde un ángulo diferente, esto es esencialmente lo que hace la biblioteca Hamcrest para las pruebas.

cbojar
fuente
55
Bueno, si usa un IDE, tiene los nombres de los parámetros en la ayuda del globo; si no usa uno, recordar el nombre de la función es equivalente a recordar los argumentos, por lo que no se gana nada de ninguna manera.
Peter - Restablece a Mónica el
29
Si se opone a assertExpectedEqualsActual"porque es más para escribir y más para leer", ¿cómo puede abogar assertEquals(Expected.is(10), Actual.is(x))?
ruakh
99
@ruakh no es comparable. assertExpectedEqualsActualaún requiere que el programador se preocupe por especificar los argumentos en el orden correcto. La assertEquals(Expected<T> expected, Actual<T> actual)firma usa el compilador para imponer el uso correcto, que es un enfoque completamente diferente. Puede optimizar este enfoque por brevedad, por ejemplo expect(10).equalsActual(x), pero esa no era la cuestión ...
Holger
66
Además, en este caso particular (==), el orden de los argumentos es realmente irrelevante para el valor final. El orden solo es importante por un efecto secundario (informar el fallo). Cuando ordenar es importante, puede tener (marginalmente) más sentido. Por ejemplo strcpy (dest, src).
Kristian H
1
No puedo estar más de acuerdo, especialmente con la parte sobre duplicación y acoplamiento ... Si cada vez que un parámetro de función cambia su nombre, el nombre de la función también tendría que cambiar, tendría que ir a rastrear todos los usos de esa función y cámbielos también ... Eso supondría un montón de cambios importantes para mí, mi equipo y todos los demás que usan nuestro código como dependencia ...
mrsmn
20

Preguntas sobre 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 al nombrar los argumentos no vale la pena.

La verbosidad no siempre significa más claridad. Considerar

copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)

versus

copy(output, source)

Ambos contienen el mismo error, pero ¿realmente lo hicimos más fácil de encontrar? Como regla general, lo más fácil de depurar es cuando todo es lo más breve posible, excepto las pocas cosas que tienen el error, y son lo suficientemente detalladas como para decirle qué salió mal.

Hay una larga historia de agregar verbosidad. Por ejemplo, existe la " notación húngara " generalmente impopular que nos dio nombres maravillosos como lpszName. Eso generalmente se ha quedado en el camino en la población programadora general. Sin embargo, agregar caracteres a los nombres de las variables miembro (como mNameo m_Nameo name_) continúa teniendo popularidad en algunos círculos. Otros lo dejaron caer por completo. 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 a la función ( getPositionECEF).

Es posible que le interesen algunos de los idiomas que Apple popularizó. 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]se escribe en la documentación como withdrawFundsFrom:usingPin:. Ese es el nombre de la función). Swift tomó un conjunto similar de decisiones, que requieren que coloque los nombres de los argumentos en las llamadas a funciones ( greet(person: "Bob", day: "Tuesday")).

Cort Ammon
fuente
13
Dejando a un lado todos los demás puntos, sería mucho más fácil de leer si copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)se escribieran copy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle). ¿Ves lo fácil que fue? Esto se debe a que es demasiado fácil pasar por alto pequeños cambios a la mitad de esa ensalada de palabras rotas y enormes, y lleva más tiempo descubrir dónde están los límites de las palabras. Rompiendo confusos.
tchrist 01 de
1
La sintaxis obj-C withdrawFundsFrom: account usingPin: userProvidedPinse toma prestada de SmallTalk.
joH1
14
@tchrist tenga cuidado al estar seguro de que tiene razón en temas relacionados con guerras santas. El otro lado no siempre está mal.
Cort Ammon
3
@tchrist Addingunderscoresnakesthingseasiertoreadnotharderasyouseeestá manipulando el argumento. La respuesta aquí utilizó mayúsculas, que estás omitiendo. AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere. En segundo lugar, 9 de cada 10 veces, un nombre nunca debería crecer más allá [verb][adjective][noun](donde cada bloque es opcional), un formato que es fácil de leer usando mayúsculas simples:ReadSimpleName
Flater
55
@tchrist: la ciencia de su estudio ( enlace de texto completo gratuito ) simplemente muestra que los programadores capacitados para usar el estilo de subrayado son más rápidos al leer el estilo de subrayado que el caso de camello. Los datos también muestran que la diferencia es menor para las asignaturas con más experiencia (y la mayoría de las asignaturas que son estudiantes sugieren que incluso las que probablemente no tenían experiencia especial). Esto no significa que los programadores que han pasado más tiempo usando camel case también den el mismo resultado.
Julio
8

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

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

actual.Should().Be(expected);

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

JacquesB
fuente
1
Soy anal y sigo el orden recomendado, pero me parece que si espero que el resultado fun(x)sea ​​5, ¿qué podría salir mal si invierte el orden assert(fun(x), 5)? ¿Cómo te mordió?
emory
3
@emory Sé que jUnit (al menos) crea un mensaje de error exhaustivo a partir de los valores de expectedy actual, por lo que invertirlos puede dar como resultado un mensaje no preciso. Pero estoy de acuerdo que suena más natural, aunque :)
joH1
@ joH1 me parece débil. el código que falla fallará y el código de aprobación pasará si lo haces assert(expected, observed)o no assert(observed, expected). Un mejor ejemplo sería algo así locateLatitudeLongitude: si invierte las coordenadas, se dañará seriamente.
emory
1
@emory Las personas que no se preocupan por los mensajes de error sensibles en las pruebas unitarias son las razones por las que tengo que tratar con "Assert.IsTrue falló" en algunas bases de código antiguas. Lo cual es tremendamente divertido de depurar. Pero sí, en este caso, el problema podría no ser tan esencial (excepto si hacemos comparaciones confusas donde el orden de los argumentos generalmente importa). Las afirmaciones fluidas son de hecho una excelente manera de evitar este problema y también hacen que el código sea más expresivo (y proporcionan un mensaje de error mucho mejor para arrancar).
Voo
@emory: Invertir el argumento hará que los mensajes de error sean engañosos y lo enviará por el camino incorrecto al depurar.
JacquesB
5

Está tratando de dirigir su camino entre Scylla y Charybdis hacia la claridad, tratando de evitar la verbosidad inútil (también conocida como divagación sin sentido), así como la brevedad excesiva (también conocida como la terquedad críptica).

Por lo tanto, tenemos que mirar 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 arity y name?
    No, entonces el nombre en sí es lo suficientemente claro.
  2. ¿Son los tipos de alguna importancia?
    No, así que ignorémoslos. ¿Ya hiciste eso? Bueno.
  3. ¿Es simétrico en sus argumentos?
    Casi, en caso de error, el mensaje coloca cada representación de argumentos en su propio lugar.

Entonces, veamos si esa pequeña diferencia tiene alguna importancia y no está cubierta por las convenciones fuertes existentes.

¿Se incomoda a la audiencia prevista 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 incluso eso falta y no es obvio por el mensaje cuál 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 codificarlo en el nombre de la función tenga una utilidad negativa.

Deduplicador
fuente
bueno, el orden podría importar con jUnit, que crea un mensaje de error específico a partir de los valores de expectedy actual(al menos con Strings)
joH1
Creo que cubrí esa parte ...
Deduplicador
usted ha mencionado, pero tener en cuenta: assertEquals("foo", "doo")da el mensaje de error es ComparisonFailure: expected:<[f]oo> but was:<[d]oo>... El intercambio de los valores sería invertir el sentido del mensaje, que los sonidos más contra simétrica para mí. De todos modos, como dijiste, un desarrollador tiene otros indicadores para resolver el error, pero puede ser engañoso en mi humilde opinión y tomar un poco más de tiempo de depuración.
joH1
La idea de que existe una "convención" para las órdenes de argumento es divertida, considerando que ambos campos (dest, src vs. src, dest) han discutido sobre esto durante al menos tanto tiempo como la sintaxis de AT&T vs. Intel ha existido. Y los mensajes de error inútiles en las pruebas unitarias son una plaga que debe erradicarse y no aplicarse. Eso es casi tan malo como "Assert.IsTrue falló" ("oye, tienes que ejecutar la prueba de la unidad de todos modos para depurarlo, así que simplemente ejecútalo de nuevo y pon un punto de interrupción allí", "oye, tienes que mirar el código de todos modos, entonces solo verifique si el orden es correcto ").
Voo
@Voo: El punto es que el "daño" por equivocarse es minúsculo (la lógica no depende de ello, y la utilidad de mensajes no se ve afectada en ningún grado significativo), y al escribir el IDE le mostrará el nombre de los parámetros y escribe de todos modos.
Deduplicador
3

A menudo no agrega ninguna claridad lógica.

Compare "Agregar" con "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. Debería decir lo que hace. Decirle, 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.

Martin Maat
fuente
44
Addsugiere una operación conmutativa. El OP se ocupa de situaciones en las que importa el orden.
Rosie F
En Swift, por ejemplo, llamaría a add (5, to: x) o add (5, plus: 7, to: x) o add (5, plus: 7, dando: x) si define la función add () en consecuencia.
gnasher729
La tercera sobrecarga debería llamarse "Sum"
StingyJack
@StringyJack Hmm .. Sum no es una instrucción, es un sustantivo que lo hace menos adecuado para un nombre de método. Pero si te sientes así y si quieres ser purista al respecto, la versión de dos argumentos también debería llamarse Sum. Si tuviera un método Add, debería tener un argumento que se agrega a la instancia del objeto en sí (que debería ser un tipo numérico o vectorial). Las 2 o más variedades de argumentos (cualquiera que sea su nombre) serían estáticas. Entonces las 3 o más versiones de argumento serían redundantes y habríamos implementado un operador más: - |
Martin Maat
1
@ Martin ¿Espera qué? sumes un verbo perfectamente cromulento . Es particularmente común en la frase "resumir".
Voo
2

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

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

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

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

Compare assertExpectedEqualsActual(foo, bar)con algunas alternativas (de esta página y de otras partes), 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 dependen de esta estructura, por lo que no puede estar fuera de sincronización con lo que está haciendo la implementación (como puede hacerlo un nombre o comentario).

Cuando encuentro o veo un problema como este, antes de gritarle frustrado a mi computadora, 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 que la máquina avance y haga lo incorrecto. Si lo assertExpectedEqualsActualusáramos, podría hacernos menos propensos a cometer un error, pero no da más información a la máquina (no puede entender inglés y la elección del nombre no debería afectar la semántica).

Lo que hace que los enfoques "estructurados" sean más preferibles, como argumentos de palabras clave, campos etiquetados, tipos distintos, etc., es que la información adicional también es legible por máquina , por lo que podemos hacer que la máquina detecte usos incorrectos y nos ayude a hacer las cosas bien. El assertEqualcaso no es tan malo, ya que el único problema serían los mensajes inexactos. Podría ser un ejemplo más siniestro String replace(String old, String new, String content), que es fácil de confundir y 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 haría que los errores desencadenaran un error de inmediato (incluso sin tipos).

Tenga en cuenta que incluso con los tipos, es posible que no estemos "diciéndole a la máquina lo que queremos". Por ejemplo, el antipatrón llamado "programación tipada en cadena" trata todos los datos como cadenas, lo que facilita mezclar argumentos (como este caso), olvidar realizar algún paso (p. Ej., Escapar), romper accidentalmente invariantes (p. Ej. haciendo JSON no analizable), 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 al intentar usarlos en otra no está claro qué representan realmente, si los tenemos mezclados, etc. Compare esto con, por ejemplo, enumeraciones distintas que tienen nombres descriptivos (por ejemplo, en LOGGING_DISABLEDlugar de false) y que provocan un mensaje de error si los mezclamos.

Warbo
fuente
1

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

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

Si lee el código, debería 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 así copyFromTheFirstLocationToTheSecondLocation(placeA, placeB).

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

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

disco
fuente
0

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

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

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

Sin embargo, una vez escrita, 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 utilizarán variables nombradas.

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

La brevedad de esto ganará a la mayoría de los programadores y personalmente me resulta 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 debes recordar el nombre de la función en primer lugar. Esto requiere buscar referencias de funciones u obtener ayuda de un IDE que probablemente proporcionará información de pedido de parámetros de todos modos.

Josh Taylor
fuente
99
Entonces, si puedo jugar al abogado del diablo por un minuto: solo es intuitivo cuando se conoce el nombre completo de la función. Si sabe que hay una función de copia y no recuerda si es copyFromSourceToDestinationo copyToDestinationFromSource, sus opciones son encontrarla por prueba y error o leer el material de referencia. Los IDE que pueden completar nombres parciales son solo una versión automatizada de este último.
Blrfl
@Blrfl El punto de llamarlo copyFromSourceToDestinationes que si crees que es copyToDestinationFromSource, el compilador encontrará tu error, pero si fue llamado copy, no lo hará. Obtener los parámetros de una rutina de copia al revés es fácil, ya que strcpy, strcat, etc. sientan un precedente. ¿Y es el conciso más fácil de leer? ¿MergeLists (listA, listB, listC) crea listA de listB & listC, o lee listA & listB y escribe listC?
Rosie F
44
@RosieF Si no estuviera seguro de lo que significaban los argumentos, estaría leyendo la documentación antes de escribir el código. Además, incluso con los nombres de funciones más detallados, todavía hay espacio para la interpretación sobre cuál es el orden en realidad. Alguien que eche un vistazo al código no podrá intuir que ha establecido la convención de que lo que está en el nombre de la función refleja el orden de los argumentos. Todavía tendrían que saberlo con anticipación o leer los documentos.
Blrfl
OTOH, destinationDir.copy (sourceDir); // "... tiene más sentido"
Kristian H
1
@ KristianH ¿En qué dirección dir1.copy(dir2)funciona? Ni idea. ¿Qué hay de dir1.copyTo(dir2)?
maaartinus