¿Hay alguna posibilidad de que hayas malinterpretado a tu maestro?
Decir que una función debe contener una sola línea es realmente ridículo. Sin embargo, la pauta general que la mayoría de los buenos programadores intentan seguir es que una función debe hacer una cosa. Ahora, el concepto de “una cosa” no está muy bien definido, y para aplicarlo correctamente debe comprender por qué esa directriz es importante.
Razón 1: legibilidad
La razón principal, al menos en lo que a mí respecta, es que permite a las personas comprender más o menos lo que hace la función sin tener que leerla. Imagina este código (Java, pero espero que alguien pueda analizar esto):
void main (String [] args) {
Datos de datos = nuevos datos ();
processArguments (argumentos);
if (dumpRequested) {
dumpData (datos);
} más {
fetchData (datos);
}
int respuesta = computeAnswer (datos);
System.out.println (“La respuesta es” + respuesta);
}
Algo aquí es extraño: ¿por qué fetchData
solo se llama si dumpRequested
es falso? ¿No deberíamos buscar siempre los datos? Bueno, resulta (en nuestro código hipotético) que dumpData
también dumpData
los datos antes de descargarlos.
¿Esperabas eso cuando leías el código? Por supuesto no. Este es el tipo de sorpresa desagradable que termina causando un dolor enorme y volviendo loca a las personas como yo.
¿Como paso? Bueno, tal vez comenzó lo suficientemente inocente. Por ejemplo, el código podría haberse visto así inicialmente:
void main (String [] args) {
Datos de datos = nuevos datos ();
Opciones opciones = processArguments (args);
if (options.dumpRequested) {
dumpData (datos);
} más {
int respuesta = computeAnswer (datos);
System.out.println (“La respuesta es” + respuesta);
}
}
Quizás en ese momento los datos se leyeron de algún archivo local, o incluso se inicializaron en la memoria o algo así, por lo que no había necesidad de buscarlos; y solo volcamos los datos o calculamos la respuesta, pero nunca ambos. Aquí, la función dumpData
solo hace “una cosa”: realiza la solicitud del usuario para volcar los datos.
Entonces alguien tuvo la idea de que es mejor obtener los datos del servidor remoto, porque SOA es donde está hoy en día. Entonces hicimos un cambio:
void main (String [] args) {
Datos de datos = nuevos datos ();
fetchData (datos);
Opciones opciones = processArguments (args);
if (options.dumpRequested) {
dumpData (datos);
} más {
int respuesta = computeAnswer (datos);
System.out.println (“La respuesta es” + respuesta);
}
}
Pero luego nos dimos cuenta de que hay un montón de programas que llaman dumpData
, y es estúpido que todos tengan que llamar a fetchData
primero, ¿verdad? Así que trasladamos ese fetchData
a dumpData
y agregamos una llamada separada antes de processData
. En este punto, el propósito de dumpData
no ha cambiado realmente, excepto que tiene que hacer un poco más para hacerlo.
Pero luego decidimos que eliminar los datos debería ser solo una acción adicional, además de calcular la respuesta. Entonces el código se convirtió en:
void main (String [] args) {
Datos de datos = nuevos datos ();
Opciones opciones = processArguments (args);
if (options.dumpRequested) {
dumpData (datos);
}
fetchData (datos);
int respuesta = computeAnswer (datos);
System.out.println (“La respuesta es” + respuesta);
}
Esto se está volviendo peligroso ahora, porque la naturaleza de la “única cosa” que dumpData
hace ha cambiado un poco. Esto debería haber llevado a un replanteamiento, pero no lo hizo. Y finalmente, el golpe mortal: el código anterior es claramente ineficiente, ya que recupera los datos dos veces, por lo que solo hicimos la solución conveniente y “simple” de llamar a fetchData
solo cuando dumpData
no ha sido llamado.
Ahora, ¿cuál es el propósito de dumpData
? Ya no es solo volcar los datos. En este código, también sirve para obtener los datos en el caso donde se solicitó el volcado. Ya no es una función de un solo propósito.
Puede mejorar un poco las cosas simplemente cambiándole el nombre a fetchAndDumpData
, lo que hace que el código anterior sea mucho más legible. Pero eso no ayuda con las otras razones de la regla de “una cosa”, que discutiré a continuación.
Razón 2: contratos
Su función, como cualquier otra unidad de código (clase, módulo, aplicación) debe tener un contrato bien definido y exigible. El contrato consiste en lo que su función espera de sus llamantes y lo que les promete a cambio.
Por ejemplo, la firma de un método Java es parte de su contrato: si me llamas con un int
y una String
, devolveré un boolean
. El compilador lo aplica: si un cliente intenta pasar los parámetros incorrectos, no compilará; Si el método intenta devolver el tipo incorrecto, no se compilará.
La semántica del método, incluidos los argumentos y el valor de retorno, debe formar parte del contrato, que es transmitido por el Javadoc. Esta parte de su contrato se aplica mediante pruebas unitarias.
Cuando agrega funcionalidad a un método, tiene dos opciones: o lo hace parte del contrato o no (duh). Si lo hace, significa que a partir de ahora debe mantener esa funcionalidad, porque otros confiarán en ella. También necesita encontrar una manera de hacer cumplir eso, por ejemplo, escribir buenas pruebas unitarias para ello.
Si decide que esto es demasiado complicado, o que no quiere estar obligado para siempre por este contrato, entonces realmente desea que sus clientes no confíen en él, lo que idealmente significa que al menos deberían fingir que no saben al respecto Sin embargo, eso podría hacer que hagan algo que tenga consecuencias negativas, como obtener los datos dos veces en nuestro ejemplo. Si evitan eso, confiando en su nueva funcionalidad, ahora tiene una dependencia no contractual, que no se aplica. Si cambia su código mañana, los romperá, y no hay protección contra eso. Esto sucede sorprendentemente a menudo en bases de código grandes, diversas y de larga vida, es decir, más del 90% del código de la industria.
Al adherirse a la directriz de “una cosa”, hace que sea más fácil mantener su contrato bien definido y exigible. Si necesita agregar más funcionalidad, divídala en otros métodos / clases / etc., y defina sus contratos como “una cosa”.
Razón 3: Acoplamiento
Finalmente, llegamos a uno de mis favoritos personales como desarrollador: el acoplamiento. Además de ser un programa de la BBC bastante divertido, el acoplamiento es una medida de cuánta interdependencia hay entre diferentes unidades de código. En general, desea que el acoplamiento sea mínimo, porque cuanto más alto (o más apretado ) sea, más difícil será cambiar una de las unidades sin tener que cambiar, y reconstruir, volver a probar y volver a implementar las otras. Dado que el cambio es la única constante en la ingeniería de software del mundo real, esto es extremadamente importante.
Regrese a nuestro ejemplo. Digamos que el método fetchData
ahora requiere un parámetro, por ejemplo, la URL de la que se obtienen los datos, y que esto se pasa como un argumento de línea de comando. ¿Como hacemos que esto funcione? Probablemente necesitemos hacer algo como esto:
void main (String [] args) {
Datos de datos = nuevos datos ();
Opciones opciones = processArguments (args);
if (options.dumpRequested) {
fetchAndDumpData (options.dataUrl, data);
}
fetchData (options.dataUrl, data);
int respuesta = computeAnswer (datos);
System.out.println (“La respuesta es” + respuesta);
}
Tenga en cuenta que tenemos que pasar la URL para fetchAndDumpData
, a pesar de que es completamente innecesario para el “objetivo principal” de ese método, que es volcar los datos. Recuerde que la recuperación fue realmente solo una optimización. Cuantas más “cosas” diferentes haga su función, más va a suceder.
Si la función solo hace “una cosa”, entonces, por definición, solo necesita saber las cosas que se requieren para hacer esa única cosa. Puedes cambiar el resto del mundo a su alrededor y no le importaría. Eso es acoplamiento flojo, y eso es algo bueno.
Entonces, ¿qué haría aquí? Hay muchas maneras diferentes de volver a “una cosa” y reducir el acoplamiento. Por ejemplo, puede decir que el trabajo de la persona que llama es buscar los datos y hacer que esa parte del contrato para dumpData
; o puede proporcionar dumpData
con una dumpData
bien definida de “proveedor de datos” (que tal vez enfrente un caché en la implementación final); o puede refactorizar dumpData
en la clase Data
; Todos estos (y estoy seguro de que hay otros) son razonables, y elegiría uno en función de los detalles.
Conclusión:
Siempre piense en lo que su función está tratando de lograr; ¿Cuál es su propósito ? Como mencioné anteriormente, esto se aplica a casi cualquier unidad de código: una función / método, una clase, un módulo, un servicio, una aplicación, una página web, incluso una sola línea de código o un bucle. Cuanto más enfocado y específico sea ese propósito, más fácil será comprender , mantener y cambiar , lo cual es el sello distintivo de una buena ingeniería de software.