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?
clean-code
Estudiante eterno
fuente
fuente
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.assert(a).toEqual(b)
(incluso si es IMO, todavía es innecesariamente detallado) donde puede encadenar algunas afirmaciones relacionadas.assertExpectedValueEqualsActualValue
? Pero espera, ¿cómo podemos recordar si se utiliza==
o.equals
oObject.equals
? Debe serassertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter
?Respuestas:
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
expected
yactual
son confusos para nuestros usuarios. Pasar deassertEquals(expected, actual)
aassertEquals(planned, real)
no requiere cambiar el código del cliente usando la función. Pasar deassertExpectedEqualsActual(expected, actual)
aassertPlannedEqualsReal(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:
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.
fuente
assertExpectedEqualsActual
"porque es más para escribir y más para leer", ¿cómo puede abogarassertEquals(Expected.is(10), Actual.is(x))
?assertExpectedEqualsActual
aún requiere que el programador se preocupe por especificar los argumentos en el orden correcto. LaassertEquals(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 ejemploexpect(10).equalsActual(x)
, pero esa no era la cuestión ...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 (comomName
om_Name
oname_
) 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 comowithdrawFundsFrom: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")
).fuente
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
se escribierancopy_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.withdrawFundsFrom: account usingPin: userProvidedPin
se toma prestada de SmallTalk.Addingunderscoresnakesthingseasiertoreadnotharderasyousee
está 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
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:O similar. Lo que sin duda es mucho más claro que,
assertEquals
pero también mucho mejor queassertExpectedEqualsActual
. Y también es mucho más composable.fuente
fun(x)
sea 5, ¿qué podría salir mal si invierte el ordenassert(fun(x), 5)
? ¿Cómo te mordió?expected
yactual
, por lo que invertirlos puede dar como resultado un mensaje no preciso. Pero estoy de acuerdo que suena más natural, aunque :)assert(expected, observed)
o noassert(observed, expected)
. Un mejor ejemplo sería algo asílocateLatitudeLongitude
: si invierte las coordenadas, se dañará seriamente.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.
No, entonces el nombre en sí es lo suficientemente claro.
No, así que ignorémoslos. ¿Ya hiciste eso? Bueno.
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.
fuente
expected
yactual
(al menos con Strings)assertEquals("foo", "doo")
da el mensaje de error esComparisonFailure: 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.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.
fuente
Add
sugiere una operación conmutativa. El OP se ocupa de situaciones en las que importa el orden.sum
es un verbo perfectamente cromulento . Es particularmente común en la frase "resumir".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: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 comoassertEqual(actual, expected)
, por lo que es fácil para nosotros mezclarlos y que la máquina avance y haga lo incorrecto. Si loassertExpectedEqualsActual
usá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
assertEqual
caso no es tan malo, ya que el único problema serían los mensajes inexactos. Podría ser un ejemplo más siniestroString replace(String old, String new, String content)
, que es fácil de confundir yString 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_DISABLED
lugar defalse
) y que provocan un mensaje de error si los mezclamos.fuente
¿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)
.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 ;-)
fuente
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.
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.
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.
fuente
copyFromSourceToDestination
ocopyToDestinationFromSource
, 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.copyFromSourceToDestination
es que si crees que escopyToDestinationFromSource
, el compilador encontrará tu error, pero si fue llamadocopy
, 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?dir1.copy(dir2)
funciona? Ni idea. ¿Qué hay dedir1.copyTo(dir2)
?