¿Cómo justifica que se escriba más código siguiendo prácticas de código limpio?


106

Nota para el moderador
Esta pregunta ya ha recibido diecisiete respuestas . Antes de publicar una nueva respuesta, lea las respuestas existentes y asegúrese de que su punto de vista no esté cubierto adecuadamente.

He seguido algunas de las prácticas recomendadas en el libro "Clean Code" de Robert Martin, especialmente las que se aplican al tipo de software con el que trabajo y las que tienen sentido para mí (no lo sigo como un dogma) .

Sin embargo, un efecto secundario que he notado es que el código "limpio" que escribo es más código que si no siguiera algunas prácticas. Las prácticas específicas que conducen a esto son:

  • Condicionales encapsulantes

Entonces en lugar de

if(contact.email != null && contact.emails.contains('@')

Podría escribir un pequeño método como este

private Boolean isEmailValid(String email){...}
  • Reemplazar un comentario en línea con otro método privado, de modo que el nombre del método se describa a sí mismo en lugar de tener un comentario en línea encima
  • Una clase solo debe tener una razón para cambiar

Y algunos otros. El punto es que lo que podría ser un método de 30 líneas, termina siendo una clase, debido a los pequeños métodos que reemplazan los comentarios y encapsulan los condicionales, etc. Cuando te das cuenta de que tienes tantos métodos, entonces "tiene sentido" poner toda la funcionalidad en una clase, cuando realmente debería haber sido un método.

Soy consciente de que cualquier práctica llevada al extremo puede ser dañina.

La pregunta concreta para la que busco una respuesta es:

¿Es este un subproducto aceptable de escribir código limpio? Si es así, ¿cuáles son algunos argumentos que puedo usar para justificar el hecho de que se han escrito más LOC?

La organización no está preocupada específicamente por más LOC, pero más LOC puede dar lugar a clases muy grandes (eso, de nuevo, podría ser reemplazado por un método largo sin un montón de funciones auxiliares use-once por razones de legibilidad).

Cuando ve una clase que es lo suficientemente grande, da la impresión de que la clase está lo suficientemente ocupada y que su responsabilidad ha sido concluida. Por lo tanto, podría terminar creando más clases para lograr otras funciones. El resultado es muchas clases, todas haciendo "una cosa" con la ayuda de muchos métodos de ayuda pequeños.

ESTA es la preocupación específica ... esas clases podrían ser una sola clase que aún logre "una cosa", sin la ayuda de muchos métodos pequeños. Podría ser una sola clase con quizás 3 o 4 métodos y algunos comentarios.


98
Si su organización usa solo LOC como una métrica para sus bases de código, entonces, para empezar, justificar el código limpio es inútil.
Kilian Foth

24
Si el mantenimiento es su objetivo, LOC no es la mejor métrica para juzgar, es uno de ellos, pero hay mucho más que considerar que simplemente mantenerlo corto.
Zibbobz

29
No es una respuesta, sino un punto a destacar: hay toda una subcomunidad sobre escribir código con la menor cantidad de líneas / símbolos posible. codegolf.stackexchange.com Se puede argumentar que la mayoría de las respuestas allí no son tan legibles como podrían ser.
Antiteos

14
Conozca las razones detrás de todas las mejores prácticas, no solo las reglas en sí mismas. Seguir reglas sin razones es culto a Cargo. Cada regla tiene una razón propia.
Gherman

99
Solo como un aparte, y usando su ejemplo, a veces empujar las cosas a los métodos lo hará pensar: "Tal vez hay una función de biblioteca que puede hacer esto". Por ejemplo, para validar una dirección de correo electrónico, puede crear un System.Net.Mail.MailAddress que lo validará por usted. Entonces puede (con suerte) confiar en el autor de esa biblioteca para hacerlo bien. Esto significa que su base de código tendrá más abstracciones y disminuirá de tamaño.
Gregory Currie

Respuestas:


130

... somos un equipo muy pequeño que admite una base de código relativamente grande e indocumentada (que heredamos), por lo que algunos desarrolladores / gerentes ven el valor de escribir menos código para hacer las cosas y tener menos código para mantener

Estas personas han identificado correctamente algo: quieren que el código sea más fácil de mantener. Sin embargo, donde se equivocaron es asumir que cuanto menos código haya, más fácil será mantenerlo.

Para que el código sea fácil de mantener, entonces debe ser fácil de cambiar. Con mucho, la forma más fácil de lograr un código fácil de cambiar es tener un conjunto completo de pruebas automáticas que fallarán si su cambio no funciona. Las pruebas son código, por lo que escribir esas pruebas aumentará su base de código. Y eso es algo bueno.

En segundo lugar, para determinar qué necesita cambiar, su código debe ser fácil de leer y de razonar. Es muy poco probable que el código muy breve, de tamaño reducido para mantener la cuenta regresiva de la línea, sea fácil de leer. Obviamente, hay que llegar a un compromiso ya que el código más largo tomará más tiempo para leer. Pero si es más rápido de entender, entonces vale la pena. Si no ofrece ese beneficio, entonces esa verbosidad deja de ser un beneficio. Pero si el código más largo mejora la legibilidad, nuevamente esto es algo bueno.


27
"Con mucho, la forma más fácil de lograr un código fácil de cambiar es tener un conjunto completo de pruebas automatizadas que fallarán si su cambio no funciona". Esto simplemente no es cierto. Las pruebas requieren un trabajo adicional para cada cambio de comportamiento porque las pruebas también necesitan un cambio , esto es por diseño y muchos dirían que hace que el cambio sea más seguro, pero también necesariamente hace que los cambios sean más difíciles.
Jack Aidley

63
Claro, pero el tiempo perdido en el mantenimiento de estas pruebas se ve reducido en el momento en que habría perdido el diagnóstico y la corrección de errores que las pruebas previenen.
MetaFight

29
@JackAidley, tener que cambiar las pruebas junto con el código puede dar la apariencia de más trabajo, pero solo si uno ignora los errores difíciles de encontrar que los cambios en el código no probado introducirán y que a menudo no se encontrarán hasta después del envío . Este último simplemente ofrece la ilusión de menos trabajo.
David Arno

31
@ JackAidley, estoy completamente en desacuerdo contigo. Las pruebas hacen que el código sea más fácil de cambiar. Sin embargo, reconozco que el código mal diseñado que está demasiado ajustado y, por lo tanto, muy unido a las pruebas puede ser difícil de cambiar, pero mi código bien estructurado y bien probado es simple de cambiar en mi experiencia.
David Arno

22
@JackAidley Puede refactorizar mucho sin cambiar ninguna API o interfaz. Significa que puede volverse loco mientras modifica el código sin tener que cambiar una sola línea en pruebas unitarias o funcionales. Es decir, si sus pruebas no prueban una implementación específica.
Eric Duminil

155

Sí, es un subproducto aceptable, y la justificación es que ahora está estructurado de manera que no tenga que leer la mayor parte del código la mayor parte del tiempo. En lugar de leer una función de 30 líneas cada vez que realiza un cambio, está leyendo una función de 5 líneas para obtener el flujo general, y tal vez un par de funciones auxiliares si su cambio toca esa área. Si se llama a su nueva clase "extra" EmailValidatory sabe que su problema no es con la validación de correo electrónico, puede omitir la lectura por completo.

También es más fácil reutilizar piezas más pequeñas, lo que tiende a reducir el recuento de líneas para su programa general. Se EmailValidatorpuede usar en todo el lugar. Algunas líneas de código que validan el correo electrónico pero se combinan con el código de acceso a la base de datos no se pueden reutilizar.

Y luego considere lo que debe hacerse si alguna vez es necesario cambiar las reglas de validación de correo electrónico, que preferiría: una ubicación conocida; o muchos lugares, posiblemente faltan algunos?


10
respuesta mucho mejor que la agotadora "prueba de unidad resuelve todos sus problemas"
Dirk Boer

13
Esta respuesta llega a un punto clave que el tío Bob y sus amigos siempre parecen pasar por alto: la refactorización en métodos pequeños solo ayuda si no tiene que leer todos los métodos pequeños para descubrir qué está haciendo su código. Es aconsejable crear una clase separada para validar las direcciones de correo electrónico. Tirar del código iterations < _maxIterationsa un método llamado ShouldContinueToIteratees estúpido .
BJ Myers

44
@DavidArno: "ser útil"! = "Resuelve todos tus problemas"
Christian Hackl

2
@DavidArno: Cuando uno se queja de que las personas implican que las pruebas unitarias "resuelven todos sus problemas", obviamente se refieren a personas que implican que las pruebas unitarias resuelven, o al menos contribuyen a la solución de, casi todos los problemas en la ingeniería de software. Creo que nadie acusa a nadie de sugerir pruebas unitarias como una forma de acabar con la guerra, la pobreza y la enfermedad. Otra forma de decirlo es que la sobrevaloración extrema de las pruebas unitarias en muchas respuestas, no solo a esta pregunta, sino también a la SE en general, está siendo criticada (legítimamente).
Christian Hackl

2
Hola @DavidArno, mi comentario fue claramente una hipérbole, no un hombre de paja;) Para mí es así: estoy preguntando cómo reparar mi automóvil y que vengan personas religiosas y me digan que debería vivir una vida menos pecaminosa. En teoría, algo que vale la pena discutir, pero en realidad no me está ayudando a mejorar en la reparación de automóviles.
Dirk Boer

34

A Bill Gates se le atribuyó la famosa frase: "Medir el progreso de la programación por líneas de código es como medir el progreso de la construcción de aeronaves por peso".

Estoy humildemente de acuerdo con este sentimiento. Esto no quiere decir que un programa deba esforzarse por obtener más o menos líneas de código, sino que, en última instancia, esto no es lo que cuenta para crear un programa que funcione y funcione. Es útil recordar que, en última instancia, la razón detrás de agregar líneas de código adicionales es que, en teoría, es más legible de esa manera.

Se pueden tener desacuerdos sobre si un cambio específico es más o menos legible, pero no creo que se equivoque al hacer un cambio en su programa porque cree que al hacerlo lo está haciendo más legible. Por ejemplo, hacer un isEmailValidpodría considerarse superfluo e innecesario, especialmente si la clase que lo define lo llama exactamente una vez. Sin embargo, preferiría ver isEmailValiduna condición en lugar de una serie de condiciones AND con las cuales debo determinar qué verifica cada condición individual y por qué se está verificando.

Cuando te metes en problemas es cuando creas un isEmailValidmétodo que tiene efectos secundarios o comprueba otras cosas además del correo electrónico, porque esto es peor que simplemente escribirlo todo. Es peor porque es engañoso y puedo perder un error debido a eso.

Aunque claramente no estás haciendo eso en este caso, entonces te animo a que continúes como lo estás haciendo. Siempre debe preguntarse si al hacer el cambio es más fácil de leer, y si ese es su caso, ¡hágalo!


1
Sin embargo, el peso de la aeronave es una métrica importante. Y durante el diseño, el peso esperado se controla de cerca. No como una señal de progreso, sino como una restricción. Las líneas de código de monitoreo sugieren que más es mejor, mientras que en el diseño de aeronaves, menos peso es mejor. Así que creo que el señor Gates podría haber elegido una mejor ilustración para su punto.
jos

21
@jos en el equipo particular con el que OP está trabajando, parece que menos LOC se considera 'mejor'. El punto que Bill Gates estaba haciendo es que el LOC no está relacionado con el progreso de ninguna manera significativa, al igual que en la construcción de aeronaves, el peso no está relacionado con el progreso de una manera significativa. Una aeronave en construcción puede tener el 95% de su peso final relativamente rápido, pero sería solo una carcasa vacía sin sistemas de control, no está completa en un 95%. Lo mismo en el software, si un programa tiene 100k líneas de código, eso no significa que cada 1000 líneas proporcionen el 1% de la funcionalidad.
Sr.Mindor

77
El monitoreo del progreso es un trabajo difícil, ¿no? Pobres gerentes.
jos

@jos: en el código también es mejor tener menos líneas para la misma funcionalidad, si todo lo demás es igual.
RemcoGerlich

@jos Leer atentamente. Gates no dice nada sobre si el peso es una medida importante para un avión en sí. Él dice que el peso es una medida terrible para el progreso de la construcción de un avión. Después de todo, según esa medida, tan pronto como arrojes todo el casco al suelo, básicamente has terminado, ya que presumiblemente representa el 9x% del peso de todo el avión.
Voo

23

así que algunos desarrolladores / gerentes ven el valor de escribir menos código para hacer las cosas y así tener menos código para mantener

Se trata de perder de vista el objetivo real.

Lo que importa es reducir las horas dedicadas al desarrollo . Eso se mide en tiempo (o esfuerzo equivalente), no en líneas de código.
Esto es como decir que los fabricantes de automóviles deberían construir sus automóviles con menos tornillos, porque lleva una cantidad de tiempo distinta de cero colocar cada tornillo. o no tiene Por encima de todo lo demás, un automóvil debe ser eficiente, seguro y fácil de mantener.

El resto de la respuesta son ejemplos de cómo un código limpio puede generar ganancias de tiempo.


Inicio sesión

Tome una aplicación (A) que no tiene registro. Ahora cree la aplicación B, que es la misma aplicación A pero con registro. B siempre tendrá más líneas de código y, por lo tanto, debe escribir más código.

Pero mucho tiempo se dedicará a investigar problemas y errores, y descubrir qué salió mal.

Para la aplicación A, los desarrolladores se quedarán atrapados leyendo el código y teniendo que reproducir continuamente el problema y recorrer el código para encontrar la fuente del problema. Esto significa que el desarrollador tiene que probar desde el principio de la ejecución hasta el final, en cada capa usada, y necesita observar cada pieza lógica utilizada.
Tal vez tenga suerte de encontrarlo de inmediato, pero tal vez la respuesta estará en el último lugar en el que piense en buscar.

Para la aplicación B, suponiendo un registro perfecto, un desarrollador observa los registros, puede identificar inmediatamente el componente defectuoso y ahora sabe dónde buscar.

Esto puede ser cuestión de minutos, horas o días guardados; dependiendo del tamaño y la complejidad de la base de código.


Regresiones

Tome la aplicación A, que no es SECA en absoluto.
Tome la aplicación B, que es SECA, pero terminó necesitando más líneas debido a las abstracciones adicionales.

Se presenta una solicitud de cambio, que requiere un cambio en la lógica.

Para la aplicación B, el desarrollador cambia la lógica (única, compartida) de acuerdo con la solicitud de cambio.

Para la aplicación A, el desarrollador tiene que cambiar todas las instancias de esta lógica donde recuerda que se está utilizando.

  • Si logra recordar todas las instancias, deberá implementar el mismo cambio varias veces.
  • Si no logra recordar todas las instancias, ahora está lidiando con una base de código inconsistente que se contradice. Si el desarrollador olvidó un fragmento de código poco utilizado, este error puede no ser evidente para los usuarios finales hasta bien avanzado el futuro. En ese momento, ¿los usuarios finales van a identificar cuál es la fuente del problema? Incluso si es así, el desarrollador puede no recordar qué implica el cambio y tendrá que descubrir cómo cambiar esta lógica olvidada. Tal vez el desarrollador ni siquiera trabaja en la compañía para entonces, y luego alguien más tiene que resolverlo todo desde cero.

Esto puede conducir a una enorme pérdida de tiempo. No solo en desarrollo, sino también en la caza y búsqueda del error. La aplicación puede comenzar a comportarse de manera errática de una manera que los desarrolladores no pueden comprender fácilmente. Y eso conducirá a largas sesiones de depuración.


Intercambiabilidad del desarrollador

El desarrollador A creó la aplicación A. El código no es limpio ni legible, pero funciona de maravilla y se ha estado ejecutando en producción. Como era de esperar, tampoco hay documentación.

El desarrollador A está ausente durante un mes debido a vacaciones. Se presenta una solicitud de cambio de emergencia. No puede esperar otras tres semanas para que Dev A regrese.

El desarrollador B tiene que ejecutar este cambio. Ahora necesita leer toda la base de código, comprender cómo funciona todo, por qué funciona y qué intenta lograr. Esto lleva años, pero digamos que puede hacerlo dentro de tres semanas.

Al mismo tiempo, la aplicación B (que creó el desarrollador B) tiene una emergencia. Dev B está ocupado, pero Dev C está disponible, aunque no conoce la base de código. qué hacemos?

  • Si mantenemos a B trabajando en A y ponemos a C a trabajar en B, entonces tenemos dos desarrolladores que no saben lo que están haciendo, y el trabajo se realiza de manera subóptima.
  • Si alejamos a B de A y hacemos que B haga B, y ahora ponemos C en A, entonces todo el trabajo del desarrollador B (o una parte significativa de él) puede terminar siendo descartado. Esto es potencialmente días / semanas de esfuerzo desperdiciado.

Dev A regresa de sus vacaciones y ve que B no entendió el código y, por lo tanto, lo implementó mal. No es culpa de B, porque usó todos los recursos disponibles, el código fuente simplemente no era legible adecuadamente. ¿A ahora tiene que pasar tiempo arreglando la legibilidad del código?


Todos estos problemas, y muchos más, terminan perdiendo el tiempo . Sí, a corto plazo, el código limpio requiere más esfuerzo ahora , pero terminará pagando dividendos en el futuro cuando se deban abordar errores / cambios inevitables.

La gerencia necesita comprender que una tarea corta ahora le ahorrará varias tareas largas en el futuro. No planificar es planificar el fracaso.

Si es así, ¿cuáles son algunos argumentos que puedo usar para justificar el hecho de que se han escrito más LOC?

Mi explicación general es preguntarle a la gerencia qué preferirían: una aplicación con una base de código 100KLOC que se pueda desarrollar en tres meses, o una base de código 50KLOC que se pueda desarrollar en seis meses.

Obviamente elegirán el tiempo de desarrollo más corto, porque la administración no se preocupa por KLOC . Los gerentes que se enfocan en KLOC están microgestionando mientras no están informados sobre lo que están tratando de manejar.


23

Creo que debe tener mucho cuidado al aplicar prácticas de "código limpio" en caso de que conduzcan a una mayor complejidad general. La refactorización prematura es la raíz de muchas cosas malas.

Extraer un condicional a una función conduce a un código más simple en el punto donde se extrajo el condicional , pero conduce a una mayor complejidad general porque ahora tiene una función que es visible desde más puntos en el programa. Agrega una ligera carga de complejidad a todas las demás funciones donde esta nueva función ahora es visible.

No estoy diciendo que no debas extraer el condicional, solo que debes considerarlo cuidadosamente si es necesario.

  • Si desea probar específicamente la lógica de validación de correo electrónico. Entonces necesita extraer esa lógica a una función separada, probablemente incluso clase.
  • Si se usa la misma lógica desde varios lugares en el código, entonces obviamente debe extraerla en una sola función. ¡No te repitas!
  • Si la lógica es obviamente una responsabilidad separada, por ejemplo, la validación del correo electrónico ocurre en medio de un algoritmo de clasificación. La validación del correo electrónico cambiará independientemente del algoritmo de clasificación, por lo que deberían estar en clases separadas.

En todo lo anterior, esta es una razón para la extracción más allá de ser simplemente "código limpio". Además, probablemente ni siquiera estaría en duda si fuera lo correcto.

Diría que, en caso de duda, elija siempre el código más simple y directo.


77
Tengo que estar de acuerdo, convertir cada condicional en un método de validación puede introducir más complejidad no deseada cuando se trata de mantenimiento y revisiones de código. Ahora tiene que cambiar de un lado a otro en el código solo para asegurarse de que sus métodos condicionales sean correctos. ¿Y qué sucede cuando tienes condiciones diferentes para el mismo valor? Ahora es posible que tenga una pesadilla de nombres con varios métodos pequeños que solo se llaman una vez y se ven casi iguales.
pboss3010

77
Fácilmente la mejor respuesta aquí. Especialmente la observación (en el tercer párrafo) de que la complejidad no es simplemente una propiedad de todo el código en su conjunto, sino algo que existe y difiere en múltiples niveles de abstracción simultáneamente.
Christian Hackl

2
Creo que una forma de decir esto es que, en general, la extracción de una condición debe hacerse solo si hay un nombre significativo y no ofuscado para esa condición. Esta es una condición necesaria pero no suficiente.
JimmyJames

Re "... porque ahora tiene una función que es visible desde más puntos en el programa" : en Pascal es posible tener funciones locales - "... Cada procedimiento o función puede tener sus propias declaraciones de etiquetas Goto, constantes , tipos, variables y otros procedimientos y funciones, ... "
Peter Mortensen

2
@PeterMortensen: También es posible en C # y JavaScript. ¡Y eso es genial! Pero el punto permanece, una función, incluso una función local, es visible en un alcance mayor que un fragmento de código en línea.
JacquesB

9

Señalaría que no hay nada inherentemente malo en esto:

if(contact.email != null && contact.email.contains('@')

Al menos suponiendo que se use esta vez.

Podría tener problemas con esto muy fácilmente:

private Boolean isEmailValid(String email){
   return email != null && email.contains('@');
}

Algunas cosas que miraría por:

  1. ¿Por qué es privado? Parece un trozo potencialmente útil. ¿Es lo suficientemente útil como para ser un método privado y no hay posibilidad de que se use más ampliamente?
  2. No nombraría personalmente el método IsValidEmail, posiblemente ContainsAtSign o LooksVaguelyLikeEmailAddress porque casi no realiza una validación real, lo que puede ser bueno, tal vez no lo que se ejecuta.
  3. ¿Se está usando más de una vez?

Si se usa una vez, es fácil de analizar y toma menos de una línea, dudaría la decisión. Probablemente no sea algo que llamaría si no fuera un problema particular de un equipo.

Por otro lado, he visto métodos que hacen algo como esto:

if (contact.email != null && contact.email.contains('@')) { ... }
else if (contact.email != null && contact.email.contains('@') && contact.email.contains("@mydomain.com")) { //headquarters email }
else if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") ) { //internal contract teams }

Ese ejemplo obviamente no es SECO.

O incluso esa última declaración puede dar otro ejemplo:

if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") )

El objetivo debe ser hacer que el código sea más legible:

if (LooksSortaLikeAnEmail(contact.Email)) { ... }
else if (LooksLikeFromHeadquarters(contact.Email)) { ... }
else if (LooksLikeInternalEmail(contact.Email)) { ... }

Otro escenario:

Es posible que tenga un método como:

public void SaveContact(Contact contact){
   if (contact.email != null && contact.email.contains('@'))
   {
       contacts.Add(contact);
       contacts.Save();
   }
}

Si esto se ajusta a la lógica de su negocio y no se reutiliza, no hay ningún problema aquí.

Pero cuando alguien pregunta "¿Por qué se guarda '@', porque eso no está bien!" y decide agregar una validación real de algún tipo, ¡luego extráigala!

Se alegrará de haberlo hecho cuando también necesite dar cuenta de la segunda cuenta de correo electrónico de los presidentes Pr3 $ sid3nt @ h0m3! @ Mydomain.com y decida simplemente hacer todo lo posible y tratar de apoyar RFC 2822.

Sobre legibilidad:

// If there is an email property and it contains an @ sign then process
if (contact.email != null && contact.email.contains('@'))

Si su código es así de claro, no necesita comentarios aquí. De hecho, no necesita comentarios para decir qué hace el código la mayor parte del tiempo, sino más bien por qué lo hace:

// The UI passes '@' by default, the DBA's made this column non-nullable but 
// marketing is currently more concerned with other fields and '@' default is OK
if (contact.email != null && contact.email.contains('@'))

Si los comentarios sobre una declaración if o dentro de un pequeño método son para mí, pedante. Incluso podría argumentar lo contrario de útil con buenos comentarios dentro de otro método porque ahora tendría que navegar a otro método para ver cómo y por qué hace lo que hace.

En resumen: no midas estas cosas; Concéntrese en los principios a partir de los cuales se construyó el texto (SECO, SÓLIDO, BESO).

// A valid class that does nothing
public class Nothing 
{

}

3
Whether the comments above an if statement or inside a tiny method is to me, pedantic.Este es un problema de "gota que colmó el vaso". Tienes razón en que esta cosa no es particularmente difícil de leer directamente. Pero si usted tiene un método grande (por ejemplo, una gran importación), que cuenta con decenas de estos pequeños evaluaciones, teniendo éstos encapsulado en nombre de los métodos de lectura mecánica ( IsUserActive, GetAverageIncome, MustBeDeleted, ...) se convertirá en una notable mejora en la lectura del código. El problema con el ejemplo es que solo observa una gota, no el paquete completo que rompe la espalda del camello.
Flater

@Flater y espero que este sea el espíritu que el lector toma de eso.
AthomSfere

1
Esta "encapsulación" es un antipatrón, y la respuesta realmente lo demuestra. Volvemos a leer el código para propósitos de depuración y para extender el código. En ambos casos, comprender lo que hace el código es fundamental. El inicio del bloque de código if (contact.email != null && contact.email.contains('@'))tiene errores. Si el if es falso, ninguna de las otras líneas if puede ser verdadera. Esto no es visible en absoluto en el LooksSortaLikeAnEmailbloque. Una función que contiene una sola línea de código no es mucho mejor que un comentario que explica cómo funciona la línea.
Quirk

1
En el mejor de los casos, otra capa de indirección oscurece la mecánica real y dificulta la depuración. En el peor de los casos, el nombre de la función se ha convertido en una mentira de la misma manera que los comentarios se convierten en mentiras: el contenido se actualiza pero el nombre no. Esto no es un ataque contra la encapsulación en general, pero este idioma particular es sintomático del gran problema moderno con la ingeniería de software "empresarial": capas y capas de abstracción y pegamento que ocultan la lógica relevante.
Quirk

@quirk ¿Creo que estás de acuerdo con mi punto general? Y con el pegamento, te estás enfrentando a un problema completamente diferente. De hecho, uso mapas de código cuando veo un nuevo código de equipo. Es aterrador lo que he visto hacer para algunos métodos grandes que llaman a una serie de métodos grandes incluso en el nivel de patrón mvc.
AthomSfere

6

Clean Code es un libro excelente y vale la pena leerlo, pero no es la autoridad final en tales asuntos.

Por lo general, es una buena idea dividir el código en funciones lógicas, pero pocos programadores lo hacen en la medida en que Martin lo hace: en algún momento obtienes rendimientos decrecientes al convertir todo en funciones y puede ser difícil seguirlo cuando todo el código está en minúsculo piezas.

Una opción cuando no vale la pena crear una función completamente nueva es simplemente usar una variable intermedia:

boolean isEmailValid = (contact.email != null && contact.emails.contains('@');

if (isEmailValid) {
...

Esto ayuda a mantener el código fácil de seguir sin tener que saltar mucho por el archivo.

Otro problema es que Clean Code se está volviendo bastante viejo como un libro ahora. Una gran cantidad de ingeniería de software se ha movido en la dirección de la programación funcional, mientras que Martin se esfuerza por agregar estado a las cosas y crear objetos. Sospecho que habría escrito un libro muy diferente si lo hubiera escrito hoy.


Algunos están preocupados por la línea de código adicional cerca de la condición (no lo estoy, en absoluto), pero tal vez aborden eso en su respuesta.
Peter Mortensen

5

Teniendo en cuenta el hecho de que la condición "es válida por correo electrónico" que tiene actualmente aceptaría la dirección de correo electrónico muy inválida " @", creo que tiene todos los motivos para resumir una clase EmailValidator. Aún mejor, use una buena biblioteca bien probada para validar las direcciones de correo electrónico.

Las líneas de código como métrica no tienen sentido. Las preguntas importantes en ingeniería de software no son:

  • ¿Tienes demasiado código?
  • ¿Tienes muy poco código?

Las preguntas importantes son:

  • ¿La aplicación en su conjunto está diseñada correctamente?
  • ¿El código está implementado correctamente?
  • ¿Se puede mantener el código?
  • ¿El código es comprobable?
  • ¿Se ha probado el código adecuadamente?

Nunca he pensado en LoC cuando escribo código para cualquier propósito que no sea Code Golf. Me he preguntado "¿Podría escribir esto de manera más sucinta?", Pero con fines de legibilidad, facilidad de mantenimiento y eficiencia, no simplemente longitud.

Claro, tal vez podría usar una larga cadena de operaciones booleanas en lugar de un método de utilidad, pero ¿debería?

Su pregunta en realidad me hace pensar en algunas cadenas largas de booleanos que he escrito y me doy cuenta de que probablemente debería haber escrito uno o más métodos de utilidad.


3

En un nivel, tienen razón: menos código es mejor. Otra respuesta citó Gate, prefiero:

"Si la depuración es el proceso de eliminar errores de software, entonces la programación debe ser el proceso de ponerlos". - Edsger Dijkstra

“Al depurar, los novatos insertan código correctivo; los expertos eliminan el código defectuoso. "- Richard Pattis

Los componentes más baratos, rápidos y confiables son aquellos que no están allí. - Gordon Bell

En resumen, cuanto menos código tenga, menos puede salir mal. Si algo no es necesario, córtalo.
Si hay un código demasiado complicado, simplifíquelo hasta que los elementos funcionales reales sean todo lo que queda.

Lo importante aquí es que todos estos se refieren a la funcionalidad y solo tienen el mínimo requerido para hacerlo. No dice nada sobre cómo se expresa eso.

Lo que estás haciendo al intentar tener un código limpio no va en contra de lo anterior. Está agregando a su LOC pero no agrega funcionalidad no utilizada.

El objetivo final es tener un código legible pero sin extras superfluos. Los dos principios no deben actuar uno contra el otro.

Una metáfora sería construir un automóvil. La parte funcional del código es el chasis, el motor, las ruedas ... lo que hace que el automóvil funcione. La forma en que divide eso es más como la suspensión, la dirección asistida, etc., hace que sea más fácil de manejar. Desea que su mecánica sea lo más simple posible mientras realiza su trabajo, para minimizar la posibilidad de que las cosas salgan mal, pero eso no le impide tener buenos asientos.


2

Hay mucha sabiduría en las respuestas existentes, pero me gustaría agregar un factor más: el idioma .

Algunos idiomas requieren más código que otros para obtener el mismo efecto. En particular, aunque Java (que sospecho que es el lenguaje en la pregunta) es extremadamente conocido y generalmente muy sólido, claro y directo, algunos lenguajes más modernos son mucho más concisos y expresivos.

Por ejemplo, en Java podría tomar fácilmente 50 líneas para escribir una nueva clase con tres propiedades, cada una con un getter y setter, y uno o más constructores, mientras que puede lograr exactamente lo mismo en una sola línea de Kotlin * o Scala. (Incluso un mayor ahorro si también quería adecuados equals(), hashCode()y toString()métodos).

El resultado es que en Java, el trabajo adicional significa que es más probable que reutilice un objeto general que realmente no encaja, para comprimir propiedades en objetos existentes o para pasar un montón de propiedades 'desnudas' individualmente; mientras que en un lenguaje conciso y expresivo, es más probable que escriba un mejor código.

(Esto resalta la diferencia entre la complejidad 'superficial' del código y la complejidad de las ideas / modelos / procesamiento que implementa. Las líneas de código no son una mala medida del primero, pero tienen mucho menos que ver con el segundo .)

Por lo tanto, el "costo" de hacer las cosas bien depende del idioma. ¡Quizás una señal de un buen lenguaje es una que no te hace elegir entre hacer bien las cosas y hacerlo de manera simple!

(* Este no es realmente el lugar para un enchufe, pero vale la pena echarle un vistazo a Kotlin).


1

Supongamos que está trabajando con la clase Contactactualmente. El hecho de que esté escribiendo otro método para la validación de la dirección de correo electrónico es evidencia del hecho de que la clase Contactno está manejando una sola responsabilidad.

También está manejando parte de la responsabilidad del correo electrónico, que idealmente debería ser su propia clase.


Otra prueba de que su código es una fusión de Contacty Emailclase es que no podrá probar el código de validación de correo electrónico fácilmente. Se requerirán muchas maniobras para alcanzar el código de validación de correo electrónico en un gran método con los valores correctos. Vea el método a continuación.

private void LargeMethod() {
    //A lot of code which modifies a lot of values. You do all sorts of tricks here.
    //Code.
    //Code..
    //Code...

    //Email validation code becoming very difficult to test as it will be difficult to ensure 
    //that you have the right data till you reach here in the method
    ValidateEmail();

    //Another whole lot of code that modifies all sorts of values.
    //Extra work to preserve the result of ValidateEmail() for your asserts later.
}

Por otro lado, si tuviera una clase de correo electrónico separada con un método para la validación del correo electrónico, para probar la unidad de su código de validación, simplemente haría una simple llamada Email.Validation()con sus datos de prueba.


Contenido adicional : la charla de MFeather sobre la profunda sinergia entre la capacidad de prueba y el buen diseño.


1

Se ha encontrado que la reducción en LOC se correlaciona con defectos reducidos, nada más. Suponiendo que cada vez que reduzca la LOC, haya reducido la posibilidad de defectos, esencialmente está cayendo en la trampa de creer que la correlación es igual a la causalidad. El LOC reducido es el resultado de buenas prácticas de desarrollo y no lo que hace que el código sea bueno.

En mi experiencia, las personas que pueden resolver un problema con menos código (a nivel macro) tienden a ser más hábiles que aquellos que escriben más código para hacer lo mismo. Lo que hacen estos desarrolladores expertos para reducir las líneas de código es usar / crear abstracciones y soluciones reutilizables para resolver problemas comunes. No pasan el tiempo contando líneas de código y agonizando sobre si pueden cortar una línea aquí o allá. A menudo, el código que escriben es más detallado de lo necesario, simplemente escriben menos.

Dejame darte un ejemplo. He tenido que lidiar con la lógica en torno a los períodos de tiempo y cómo se superponen, si son adyacentes y qué brechas existen entre ellos. Cuando comencé a trabajar en estos problemas, tenía bloques de código haciendo los cálculos en todas partes. Finalmente, creé clases para representar los períodos de tiempo y las operaciones que calculaban superposiciones, complementos, etc. Esto eliminó inmediatamente grandes extensiones de código y las convirtió en algunas llamadas a métodos. Pero esas clases en sí mismas no fueron escritas de manera escueta.

En pocas palabras: si está tratando de reducir la LOC intentando cortar una línea de código aquí o allá con más precisión, lo está haciendo mal. Es como tratar de perder peso reduciendo la cantidad de vegetales que come. Escriba código que sea fácil de entender, mantener, depurar y reducir LOC mediante la reutilización y la abstracción.


1

Ha identificado una compensación válida

Entonces, de hecho, hay una compensación aquí y es inherente a la abstracción como un todo. Cada vez que alguien intenta colocar N líneas de código en su propia función para nombrarlo y aislarlo, al mismo tiempo facilita la lectura del sitio de llamada (al referirse a un nombre en lugar de a todos los detalles sangrientos que subyacen en ese nombre) y más complejo (ahora tiene un significado que está enredado en dos partes diferentes de la base de código). "Fácil" es lo opuesto a "difícil", pero no es sinónimo de "simple", que es lo opuesto a "complejo". Los dos no son opuestos, y la abstracción siempre aumenta la complejidad para insertar una forma u otra de manera fácil.

Podemos ver la complejidad añadida directamente cuando algún cambio en los requisitos del negocio hace que la abstracción comience a filtrarse. Tal vez alguna lógica nueva hubiera sido más natural en el medio del código previamente abstraído, por ejemplo, si el código abstraído atraviesa algún árbol y realmente desea recopilar (y tal vez actuar) algún tipo de información mientras está atravesando el árbol. Mientras tanto, si ha extraído este código, puede haber otros sitios de llamadas, y agregar la lógica requerida en el medio del método podría romper esos otros sitios de llamadas. Mira, cada vez que cambiamos una línea de código solo necesitamos mirar el contexto inmediato de esa línea de código; cuando cambiamos un método, debemos Cmd-F todo nuestro código fuente buscando cualquier cosa que pueda romperse como resultado de cambiar el contrato de ese método,

El algoritmo codicioso puede fallar en estos casos

La complejidad también ha hecho que el código en cierto sentido sea menos legible en lugar de más. En un trabajo anterior, traté con una API HTTP que fue estructurada con mucho cuidado y precisión en varias capas, cada punto final es especificado por un controlador que valida la forma del mensaje entrante y luego lo entrega a algún administrador de "capa de lógica empresarial" , que luego solicitó alguna "capa de datos" que se encargó de realizar varias consultas a alguna capa de "objeto de acceso a datos", que se encargó de crear varios Delegados SQL que realmente responderían a su pregunta. Lo primero que puedo decir al respecto fue que algo así como el 90% del código era una copia repetitiva de copiar y pegar, en otras palabras, era sin operaciones. Entonces, en muchos casos, leer cualquier pasaje de código dado fue muy "fácil", porque "oh, este administrador simplemente reenvía la solicitud a ese objeto de acceso a datos".muchos cambios de contexto y búsqueda de archivos e intentar rastrear información que nunca debería haber estado rastreando, "esto se llama X en esta capa, se llama X 'en esta otra capa, luego se llama X' en esta otra otra capa ".

Creo que cuando lo dejé, esta API CRUD simple estaba en la etapa en la que, si la imprimía a 30 líneas por página, ocuparía de 10 a 20 libros de texto de quinientas páginas en un estante: era una enciclopedia completa de repetitivos código. En términos de complejidad esencial, no estoy seguro de que haya siquiera la mitad de un libro de texto de complejidad esencial allí; solo teníamos unos 5-6 diagramas de base de datos para manejarlo. Hacer un pequeño cambio fue una tarea gigantesca, aprender que era una tarea gigantesca, agregar nuevas funcionalidades resultó ser tan doloroso que en realidad teníamos archivos de plantilla repetitivos que usaríamos para agregar nuevas funcionalidades.

Así que he visto de primera mano cómo hacer que cada parte sea muy legible y obvia puede hacer que todo sea muy ilegible y no obvio. Esto significa que el algoritmo codicioso puede fallar. Conoces el algoritmo codicioso, ¿sí? "Voy a hacer cualquier paso a nivel local que mejore la situación más y luego confiaré en que me encuentro en una situación mejorada a nivel mundial". A menudo es un hermoso primer intento, pero también puede fallar en contextos complejos. Por ejemplo, en la fabricación, puede intentar aumentar la eficiencia de cada paso en particular en un proceso de fabricación complejo: hacer lotes más grandes, gritar a las personas en el piso que parecen no estar haciendo nada para ocuparse de otra cosa, y Esto a menudo puede destruir la eficiencia global del sistema.

Mejor práctica: use DRY y longitudes para hacer la llamada

(Nota: el título de esta sección es una broma; a menudo les digo a mis amigos que cuando alguien dice "deberíamos hacer X porque las mejores prácticas lo dicen ") el 90% del tiempo no hablan de algo como la inyección SQL o el hash de contraseñas o lo que sea, las mejores prácticas unilaterales, por lo que la declaración se puede traducir en ese 90% de las veces a "deberíamos hacer X porque yo lo digo ". Como si pudieran tener algún artículo de blog de alguna empresa que hizo un mejor trabajo con X en lugar de X ', pero generalmente no hay garantía de que su negocio se parezca a ese negocio, y generalmente hay algún otro artículo de otro negocio que hizo un mejor trabajo con X' en lugar de X. Por lo tanto, no tome el título también seriamente.)

Lo que recomendaría se basa en una charla de Jack Diederich llamada Stop Writing Classes (youtube.com) . Él hace varios puntos importantes en esa charla: por ejemplo, que puedes saber que una clase es realmente una función cuando solo tiene dos métodos públicos, y uno de ellos es el constructor / inicializador. Pero en un caso está hablando de cómo una biblioteca hipotética que ha reemplazado por cadenas para la charla como "Muffin" declaró su propia clase "MuffinHash", que era una subclase del dicttipo integrado que tiene Python. La implementación estaba completamente vacía: alguien acababa de pensar: "podríamos necesitar agregar una funcionalidad personalizada a los diccionarios de Python más tarde, introduzcamos una abstracción ahora por si acaso".

Y su respuesta desafiante fue simplemente: "siempre podemos hacerlo más tarde, si es necesario".

Creo que a veces simulamos que vamos a ser peores programadores en el futuro de lo que somos ahora, por lo que es posible que deseemos insertar algún tipo de pequeña cosa que nos haga felices en el futuro. Anticipamos las necesidades futuras de nosotros. "Si el tráfico es 100 veces mayor de lo que creemos que será, ese enfoque no se ampliará, por lo que tenemos que poner la inversión inicial en este enfoque más difícil que se ampliará". Muy sospechoso.

Si tomamos ese consejo en serio, entonces necesitamos identificar cuándo ha llegado "más tarde". Probablemente lo más obvio sería establecer un límite superior en la longitud de las cosas por razones de estilo. Y creo que el mejor consejo restante sería usar DRY, no se repita, con estas heurísticas sobre las longitudes de línea para reparar un agujero en los principios SOLID. Basado en la heurística de 30 líneas que son una "página" de texto y una analogía con la prosa,

  1. Refactorice un cheque en una función / método cuando desee copiarlo y pegarlo. Al igual que hay razones ocasionales válidas para copiar y pegar, pero siempre debe sentirse sucio al respecto. Los verdaderos autores no te hacen releer una gran oración larga 50 veces a lo largo de la narración a menos que realmente estén tratando de resaltar un tema.
  2. Una función / método idealmente debería ser un "párrafo". La mayoría de las funciones deben tener aproximadamente media página, o de 1 a 15 líneas de código, y solo el 10% de sus funciones deben tener un rango de página y media, 45 líneas o más. Una vez que esté en más de 120 líneas de código y comentarios, esa cosa debe dividirse en partes.
  3. Un archivo idealmente debería ser un "capítulo". La mayoría de los archivos deben tener 12 páginas o menos, por lo que 360 ​​líneas de código y comentarios. Tal vez solo se debe permitir que el 10% de sus archivos tengan una extensión de 50 páginas o 1500 líneas de código y comentarios.
  4. Idealmente, la mayor parte de su código debe estar sangrado con la línea base de la función o con un nivel de profundidad. Basado en algunas heurísticas sobre el árbol fuente de Linux, si eres religioso al respecto, solo tal vez el 10% de tu código debe tener sangría de 2 niveles o más dentro de la línea de base, menos del 5% de sangría de 3 niveles o más. Esto significa en particular que las cosas que necesitan "envolver" alguna otra preocupación, como el manejo de errores en un gran intento / captura, deben ser sacadas de la lógica real.

Como mencioné allí, probé estas estadísticas con el árbol fuente de Linux actual para encontrar esos porcentajes aproximados, pero también son lógicas en la analogía literaria.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.