En aras del argumento, aquí hay una función de muestra que imprime el contenido de un archivo determinado línea por línea.
Versión 1:
void printFile(const string & filePath) {
fstream file(filePath, ios::in);
string line;
while (std::getline(file, line)) {
cout << line << endl;
}
}
Sé que se recomienda que las funciones hagan una cosa en un nivel de abstracción. Para mí, aunque el código anterior hace más o menos una cosa y es bastante atómico.
Algunos libros (como el Código Limpio de Robert C. Martin) parecen sugerir dividir el código anterior en funciones separadas.
Versión 2:
void printFile(const string & filePath) {
fstream file(filePath, ios::in);
printLines(file);
}
void printLines(fstream & file) {
string line;
while (std::getline(file, line)) {
printLine(line);
}
}
void printLine(const string & line) {
cout << line << endl;
}
Entiendo lo que quieren lograr (abrir archivo / leer líneas / imprimir línea), pero ¿no es un poco exagerado?
La versión original es simple y, en cierto sentido, ya hace una cosa: imprime un archivo.
La segunda versión conducirá a una gran cantidad de funciones realmente pequeñas que pueden ser mucho menos legibles que la primera versión.
¿No sería, en este caso, mejor tener el código en un lugar?
¿En qué punto el paradigma "Do One Thing" se vuelve dañino?
printFile
,printLines
y, por últimoprintLine
.Respuestas:
Por supuesto, esto solo plantea la pregunta "¿Qué es una cosa?" ¿Leer una línea es una cosa y escribir una línea es otra? ¿O copiar una línea de un flujo a otro se considera una cosa? O copiando un archivo?
No hay una respuesta dura y objetiva a eso. Tu decides. Tú puedes decidir. Tienes que decidir. El objetivo principal del paradigma "hacer una cosa" es probablemente producir código que sea lo más fácil de entender posible, para que pueda usarlo como una guía. Desafortunadamente, esto tampoco es medible objetivamente, por lo que debe confiar en su instinto y en el "WTF". contar en la revisión de código .
OMI, una función que consiste en una sola línea de código rara vez vale la pena. Tu
printLine()
no tiene ninguna ventaja sobre el uso destd::cout << line << '\n'
1 directamente. Si veoprintLine()
, tengo que asumir que hace lo que dice su nombre o buscarlo y comprobarlo. Si veostd::cout << line << '\n'
, sé de inmediato lo que hace, porque esta es la forma canónica de generar el contenido de una cadena como una líneastd::cout
.Sin embargo, otro objetivo importante del paradigma es permitir la reutilización del código, y esa es una medida mucho más objetiva. Por ejemplo, en su segunda versión
printLines()
podría escribirse fácilmente para que sea un algoritmo universalmente útil que copie líneas de una secuencia a otra:Tal algoritmo podría reutilizarse también en otros contextos.
Luego puede poner todo lo específico de este caso de uso en una función que llame a este algoritmo genérico:
1 Tenga en cuenta que usé en
'\n'
lugar destd::endl
.'\n'
debería ser la opción predeterminada para generar una nueva línea ,std::endl
es el caso extraño .fuente
do_x_and_y()
nombrando la función en sudo_everything()
lugar. Sí, ese es un ejemplo tonto, pero muestra que esta regla ni siquiera evita los ejemplos más extremos de mal diseño. En mi opinión, esta es una decisión instintiva tanto como la dictada por las convenciones. De lo contrario, si fuera objetivo, podría encontrar una métrica para ello, que no puede.printLine
etc. es válida, cada una de ellas es una sola abstracción, pero eso no significa que sea necesario.printFile
Ya es "una cosa". Aunque puede descomponer eso en tres abstracciones separadas de nivel inferior, no tiene que descomponerse en cada nivel posible de abstracción. Cada función debe hacer "una cosa", pero no todas las "una cosa" posibles deben ser una función. Mover demasiada complejidad al gráfico de llamadas puede ser un problema.Tener una función que haga solo "una cosa" es un medio para dos fines deseables, no un mandamiento de Dios:
Si su función solo hace "una cosa", lo ayudará a evitar la duplicación de código y la hinchazón de la API porque podrá componer funciones para hacer cosas más complejas en lugar de tener una explosión combinatoria de funciones de alto nivel y menos componibles .
Tener funciones solo hace "una cosa" puede hacer que el código sea más legible. Esto depende de si gana más claridad y facilidad de razonamiento al desacoplar las cosas de lo que pierde a la verbosidad, la indirección y la sobrecarga conceptual de las construcciones que le permiten desacoplar las cosas.
Por lo tanto, "una cosa" es inevitablemente subjetiva y depende de qué nivel de abstracción sea relevante para su programa. Si
printLines
se considera una operación única y fundamental y la única forma de imprimir líneas que le interesan o prevé que le interesen, entonces, para sus propósitos,printLines
solo hace una cosa. A menos que encuentre la segunda versión más legible (no) la primera versión está bien.Si comienza a necesitar más control sobre los niveles más bajos de abstracción y termina con una duplicación sutil y una explosión combinatoria (es decir,
printLines
para nombres de archivos y completamente separadosprintLines
parafstream
objetos, unaprintLines
para consola y unaprintLines
para archivos), entoncesprintLines
está haciendo más de una cosa a nivel de abstracción que te importa.fuente
A esta escala, no importa. La implementación de una sola función es perfectamente obvia y comprensible. Sin embargo, agregar un poco más de complejidad hace que sea muy atractivo dividir la iteración de la acción. Por ejemplo, suponga que necesita imprimir líneas desde un conjunto de archivos especificado por un patrón como "* .txt". Luego separaría la iteración de la acción:
Ahora la iteración del archivo se puede probar por separado.
Divido las funciones para simplificar las pruebas o para mejorar la legibilidad. Si la acción realizada en cada línea de datos fuera lo suficientemente compleja como para justificar un comentario, entonces ciertamente la dividiría en una función separada.
fuente
Extraiga métodos cuando sienta la necesidad de un comentario para explicar las cosas.
Escriba métodos que solo hagan lo que su nombre dice de una manera obvia, o cuente una historia llamando a métodos ingeniosamente nombrados.
fuente
Incluso en su caso simple, le faltan detalles de que el Principio de responsabilidad única lo ayudaría a administrar mejor. Por ejemplo, qué sucede cuando algo sale mal al abrir el archivo. Agregar en el manejo de excepciones para fortalecer los casos límite de acceso a archivos agregaría 7-10 líneas de código a su función.
Después de abrir el archivo, aún no estás seguro. Podría ser arrancado de usted (especialmente si se trata de un archivo en una red), podría quedarse sin memoria, nuevamente pueden ocurrir una serie de casos extremos contra los que desea fortalecer y aumentar su función monolítica.
La línea de impresión, la línea de impresión parece lo suficientemente inocuo. Pero a medida que se agreguen nuevas funciones a la impresora de archivos (análisis y formateo de texto, renderización en diferentes tipos de pantallas, etc.) crecerá y se lo agradecerá más tarde.
El objetivo de SRP es permitirle pensar en una sola tarea a la vez. Es como dividir un gran bloque de texto en múltiples párrafos para que el lector pueda comprender el punto que está tratando de transmitir. Lleva un poco más de tiempo escribir código que se adhiera a estos principios. Pero al hacerlo, hacemos que sea más fácil leer ese código. Piense en lo feliz que será su futuro yo cuando tenga que rastrear un error en el código y lo encuentre perfectamente particionado.
fuente
Personalmente, prefiero este último enfoque, ya que le ahorra trabajo en el futuro y obliga a la mentalidad de "cómo hacerlo de manera genérica". A pesar de eso, en su caso, la Versión 1 es mejor que la Versión 2, solo porque los problemas resueltos por la Versión 2 son demasiado triviales y específicos de fstream. Creo que debería hacerse de la siguiente manera (incluida la corrección de errores propuesta por Nawaz):
Funciones de utilidad genéricas:
Función específica del dominio:
Ahora
printLines
yprintLine
puede trabajar no solo confstream
, sino con cualquier flujo.fuente
printLine()
función no tiene valor. Mira mi respuesta .Cada paradigma , (no necesariamente el que usted citó) a seguir requiere cierta disciplina y, por lo tanto, reducir la "libertad de expresión" - resulta en una sobrecarga inicial (¡al menos solo porque tiene que aprenderlo!). En este sentido, cada paradigma puede volverse dañino cuando el costo de esa sobrecarga no está sobrecompensado por la ventaja que el paradigma está diseñado para mantener consigo mismo.
La verdadera respuesta a la pregunta, por lo tanto, requiere una buena capacidad para "prever" el futuro, como:
A
yB
A-
yB+
(es decir, algo que se parezca a A y B, pero un poco diferente)?A*
oA*-
?Si esa probabilidad es relativamente alta, será una buena posibilidad si, mientras pienso en A y B, también pienso en sus posibles variantes, para así aislar las partes comunes para poder reutilizarlas.
Si esa probabilidad es muy baja (cualquier variante alrededor no
A
es esencialmente nada más queA
sí misma), estudie cómo descomponer A más probablemente resulte en una pérdida de tiempo.Solo como ejemplo, déjame contarte esta historia real:
Durante mi vida pasada como un maestro, descubrí que -en la mayoría de proyectos- del estudiante la práctica totalidad de ellos ofrecen su propia función para calcular la longitud de una cadena C .
Después de investigar un poco, descubrí que, al ser un problema frecuente, todos los estudiantes tienen la idea de utilizar una función para eso. Después de decirles que hay una función de biblioteca para eso (
strlen
), muchos respondieron que, dado que el problema era tan simple y trivial, era más efectivo para ellos escribir su propia función (2 líneas de código) que buscar el manual de la biblioteca C (era 1984, olvidé la WEB y google!) en estricto orden alfabético para ver si había una función lista para eso.Este es un ejemplo donde también el paradigma "no reinventar la rueda" puede volverse dañino, ¡sin un catálogo de ruedas efectivo!
fuente
Su ejemplo está bien para ser utilizado en una herramienta desechable que se necesita ayer para realizar una tarea específica. O como una herramienta de administración controlada directamente por un administrador. Ahora hágalo robusto para que sea adecuado para sus clientes.
Agregue el manejo adecuado de errores / excepciones con mensajes significativos. Tal vez necesite verificación de parámetros, incluidas las decisiones que deben tomarse, por ejemplo, cómo manejar archivos no existentes. Agregue funcionalidad de registro, tal vez con diferentes niveles como información y depuración. Agregue comentarios para que los colegas de su equipo sepan lo que está sucediendo allí. Agregue todas las partes que generalmente se omiten por brevedad y se dejan como ejercicio para el lector al dar ejemplos de código. No olvides tus pruebas unitarias.
Su pequeña y agradable función lineal termina repentinamente en un desorden complejo que pide ser dividido en funciones separadas.
fuente
En mi opinión, se vuelve dañino cuando llega tan lejos que una función casi no hace nada más que delegar el trabajo a otra función, porque eso es una señal de que ya no es una abstracción de nada y la mentalidad que conduce a tales funciones siempre está en peligro de haciendo cosas peores ...
De la publicación original
Si es lo suficientemente pedante, es posible que note que printLine sigue haciendo dos cosas: escribir la línea en cout y agregar un carácter de "línea final". Algunas personas pueden querer manejar eso creando nuevas funciones:
¡Oh no, ahora hemos empeorado el problema! ¡Ahora es incluso OBVIO que printLine haga DOS cosas! 1! No hace mucha estupidez crear las "soluciones" más absurdas que uno pueda imaginar para deshacerse de ese problema inevitable de que imprimir una línea consiste en imprimir la línea en sí misma y agregar un carácter de fin de línea.
fuente
Respuesta corta ... depende.
Piense en esto: qué pasa si, en el futuro, no desea imprimir solo en la salida estándar, sino en un archivo.
Sé lo que es YAGNI, pero solo digo que podría haber casos en los que se sabe que algunas implementaciones son necesarias, pero pospuestas. Entonces, tal vez el arquitecto o lo que sea que sepa que la función necesita poder imprimir también en un archivo, pero no quiere hacer la implementación en este momento. Entonces él crea esta función adicional, por lo que, en el futuro, solo necesita cambiar la salida en un lugar. ¿Tiene sentido?
Sin embargo, si está seguro de que solo necesita salida en la consola, realmente no tiene mucho sentido. Escribir un "envoltorio"
cout <<
parece inútil.fuente
La razón por la que hay libros que dedican capítulos a las virtudes de "hacer una cosa" es que todavía hay desarrolladores que escriben funciones de 4 páginas y condicionan 6 niveles. Si su código es simple y claro, lo ha hecho bien.
fuente
Como han comentado otros carteles, hacer una cosa es cuestión de escala.
También sugeriría que la idea de One Thing es evitar que las personas codifiquen por efecto secundario. Esto se ejemplifica mediante un acoplamiento secuencial donde los métodos deben llamarse en un orden particular para obtener el resultado 'correcto'.
fuente