Resumen de "Clean Code" de Robert C. Martin
162 Visitas 20 minutos Enlace Permanente
Consejos para tener un código que sea fácil de mantener
Comentarios
- Usar el mínimo de comentarios, pueden quedar desactualizados y confundir.
- Solo lo que no se puede expresar con código.
- Deben aportar valor a otros programadores:
- Referencias a temas legales.
- Ejemplos para expresiones regulares.
- Explicaciones sobre intenciones o contexto (pueden ser URLs).
- Aclaraciones sobre métodos o clases de terceros (que no pueden modificarse).
- Avisos sobre consecuencias.
- TODO, FIX y similares.
- APIs (ej. Swagger)
- Solo usar TODO para cosas a hacer al día siguiente (sino, usar FIX u otros similares).
- No comentar bloques de código: eliminarlos y usar un sistema de control de versiones (Git).
- No usarlos como registro de cambios, para eso está el control de versiones.
Formato
- Definir guías de estilo que deben seguir todos los programadores.
- El ancho máximo debe estar entre 80 y 120 caracteres.
- Sangrados de 2 ó 3 niveles como mucho.
- Un archivo no debería superar las 200 líneas, 500 como máximo.
- Agregar líneas en blanco para separar bloques, métodos, clases, etc.
- Usar adecuadamente los sangrados, misma distancia siempre, sólo espacios o sólo tabuladores (no mezclar).
- Colocar los métodos invocados justo debajo del que invoca (o lo más próximo posible).
Nombres
- Se debe saber qué es algo tan solo viendo su nombre (autodescriptiva, autocomentada).
- Deben pronunciarse con facilidad: facilita la comunicación entre programadores.
- Si hay un grupo de variables relacionadas, mejor agruparlas en una clase propia.
- Usar 1 palabra por cada funcionalidad: no varias para la misma funcionalidad ni una para varias diferentes.
- Usar nombres y terminología del dominio. [mejora comunicación con cliente]
- No usar:
- Nombres que no significan nada (a, d, hp, etc.)
- Nombres muy cortos y genéricos. [dificulta la búsqueda]
- Abreviaturas (usr → usuario).
- Prefijos y sufijos (usrEmail, emailUsr → usuario.Email, es decir, agruparlas en una clase).
- El tipo de dato dentro del nombre (lstUsuarios → usuarios).
- Un nombre que se parezca demasiado a otro ya usado o que sea sinónimo.
- Interfaces: no usar el prefijo “i mayúscula” (IClase , Clase → Clase , ClaseImp). [No se sigue en C#]
- Clases:
- Usar siempre sustantivos, nunca verbos
- Evitar genéricos como Manager, Data, Info, etc. que no aportan nada.
- Evitar constructores sobrecargados, mejor usar static factory methods:
Complex(int i), Complex(double d), etc. → Complex.FromInt(i), Complex.FromDouble(d), etc.
- Métodos: usar siempre verbos.
- Propiedades: usar prefijos get, set, is, etc. (estándar de facto). [No es necesario en propiedades de C#]
- Patrones de Diseño: usar el nombre del patrón (Factory, Facade, etc.) ya que ayuda a indicar su uso a otros.
Datos
- Diferenciar bien entre:
- Objetos: ocultan sus datos y exponen métodos sobre esos datos.
- Estructuras: muestras sus datos y no tienen métodos (ej. DTOs).
- Active Records: representan datos de BD, pueden tener métodos de navegación (save, find, …)
- Mixtas: muestran datos y tienen métodos. [Evitar totalmente]
- Tipos de programación: evaluar en cada caso. [“Todo es un objeto” es un “mito”]
- Procedural:
- Usa estructuras.
- No hay abstracción, los clientes usan sus propiedades. [Alto acoplamiento]
- Fácil de agregar nuevos métodos sin cambiar las estructuras de datos existentes.
- Difícil de agregar nuevas estructuras de datos porque todas los métodos deben cambiar.
- Orientado a Objetos:
- Usa objetos.
- Hay abstracción, los clientes usan su interfaz (métodos). [Bajo acoplamiento]
- Difícil de agregar nuevas métodos sin cambiar todas las clases existentes.
- Fácil de agregar nuevas clases sin tener que cambiar los métodos existentes.
- Ley de Demeter:
- No hablar con desconocidos (bajo acoplamiento).
- Se aplica a objetos, no a estructuras (no tienen métodos y exponen sus datos).
- Un objeto no debe conocer los entresijos de los objetos que manipula.
- Un método de una clase sólo debe acceder a:
- Propiedades de su clase.
- Otros métodos de su clase.
- Propiedades y métodos de los objetos creados por el método.
- Propiedades y métodos de los objetos pasados como argumentos al método.
- Datos de estructuras creadas o pasadas como argumentos.
- Un método de una clase no debe acceder a:
- Propiedades o métodos de objetos devueltos por algún método (si son de tipos desconocidos).
Métodos
- La longitud máxima debería ser unas 20 líneas (50 cómo algo excepcional). Cuanto más cortas, mejor.
- Solo deben hacer una única cosa y hacerlo bien: [se refiere a un único nivel de abstracción, no una instrucción]
- Las instrucciones deben estar todas al mismo nivel de abstracción.
- No debe hacer algo y devolver algo, eso son 2 cosas.
- Si el método está dividida en “secciones”, es que claramente está haciendo más de una cosa
- 2 ó 3 argumentos como máximo: agruparlos en clases. [además, facilita los tests]
- Lo ideal es que no tenga parámetros o que tenga 1 único parámetro.
- Con 2 parámetros es típico equivocarse en el orden y pasarlos al revés.
- Es fácil reducir de 2 a 1 parámetro (ó de 1 a 0) llevando el método a la clase del primer parámetro.
- No usar parámetros de salida, son confusos. [esto incluye parámetros por referencia que son modificados]
- Dentro de un condicional o bucle, lo ideal es que haya solo una línea (instrucción o llamada a método).
- No usar argumentos booleanos que hagan que se ejecute una cosa u otra (flags): dividir el método en dos.
- Evitar los switch: usar patrón Factory Method o Abstract Factory si se puede. [switch rompe principios SOLID]
- No pueden tener efectos secundarios difíciles de prever.
- DRY (Don’t Repeat Yourself): nunca repetir código, se podría modificar uno y no el otro [raíz de todo mal]
- Es más fácil escribir código y reestructurarlo luego que escribirlo bien desde el principio. [Refactoring]
Clases
- Se deben de poder leer siempre de arriba hacia abajo, sin tener que volver arriba.
- Seguir un orden de declaración:
- Constantes y variables estáticas.
- Constantes y variables de instancia.
- Constructores y destructores.
- Métodos estáticos.
- Métodos de instancia.
- En cada bloque, primero siempre lo público y luego lo privado.
- Por defecto, empezar siempre con privado y luego darle el mínimo acceso que vaya haciendo falta.
- Una clase debe tener una única responsabilidad.
- Las clases deben ser siempre pequeñas, aunque eso signifique tener muchas clases.
- Muchas variables de instancia indican que hay múltiples responsabilidades (baja consistencia).
- Lo ideal es que todas las funciones usen todas las variables de instancia (alta consistencia).
- Se deben aplicar los principios SOLID.
Errores
- Usar excepciones en vez de códigos de error:
- Códigos de error: obliga a tratarlos inmediatamente, aunque sea pasarlos al nivel superior.
- Excepciones: se pueden delegar/diferir su tratamiento a niveles superiores.
- Si un bloque puede fallar, separarlo en un método aparte con su try/catch.
- Un método con try/catch llamará al método que puede fallar y no hará nada más (responsabilidad única).
- Adjuntar mensajes de error descriptivos y que indiquen contexto.
- Cuando se definen excepciones, pensar en cómo de útiles le serán a los clientes.
- Nunca usar el tratamiento de errores como una lógica de negocio (ej. usar el catch como un “else”).
- No devolver NULL, usar un objeto especial o una excepción.
- Si lo devuelve una librería externa, encapsularlo en un método.
- No pasar NULL como argumento: obliga a realizar más comprobaciones innecesarias (usar objeto especial).
Librerías Externas
- Usar patrones Facade y Adapter para encapsular y aislar librerías externas.
- Exponer sólo la funcionalidad que se vaya a usar, no toda la de la librería externa.
- Crear un tipo de excepción específico para esa librería.
- Test de Aprendizaje: pruebas unitarias que se hacen para saber cómo funciona la librería y se pueden dejar.
- Crear siempre pruebas unitarias para comprobar que, tras actualizarlas, siguen funcionando como se espera.
Pruebas Unitarias
- Sirven de documentación y de ejemplo.
- Usar TDD (Test-Driven Development: desarrollo guiado por pruebas):
- No crear código de producción hasta haber creado una prueba unitaria que falle.
- No probar más de un concepto en el mismo test (se permiten varios “asserts” para probar ese concepto).
- No crear más código de producción del necesario para superar dicha prueba.
- Su legibilidad es mucho más importante que en el código de producción, aunque afecte al rendimiento.
- La revisión y refactorización debe ser una parte principal de todo desarrollo (tests → sin miedo a cambios).
- Tres partes (AAA o given-when-then):
- Generar (Arrange, Given): crea los datos de prueba.
- Operar (Act, When): realiza la prueba.
- Comprobar (Assert, Then): indica si se pasó la prueba o no.
- Las pruebas deben ser FIRST:
- Rapidez (Fast): deben ser rápidas, se van a ejecutar muchas.
- Independiente: no pueden depender unas de otras.
- Repetición: se deben poder ejecutar en cualquier orden y en cualquier entorno.
- Validación Automática (Self-Validating): deben ser binarias: o fallan o no fallan, sin matices.
- Puntualidad (Timely): deben crearse antes de crear el código de producción (evitar la pereza).
- Usar test obliga a tener clases pequeñas y simples (evitar crecimiento exponencial de pruebas).
- Los métodos privados se pueden definir como protected para poder hacer pruebas sobre ellos. [evitarlo]
- Una mejor solución es crear clases aparte con estos métodos como públicos (no es obligatorio).
Sistemas
- Separar responsabilidades: un ente no puede controlar y saber todo.
- Separar la puesta en marcha de un programa (nada más arrancar) de su uso (cargado y listo para usarse).
- Crear los objetos en “main”.
- Usar patrón Factory Method y Abstract Factory.
- Usar un inyector de dependencias.
- Usar sistemas transversales basados en AOP para persistencia, log, email, caché, excepciones, etc.
- AOP: aspect oriented programming → programación orientada a aspectos.
- Se suele implementar con los patrones Decorator o Proxy.
- También se implementa con anotaciones/atributos/decoradores de clases y funciones.
Concurrencia
- Problemas:
- Genera cierta sobrecarga (no siempre es lo más óptimo).
- Es compleja, incluso con problemas sencillos.
- Los errores no se suelen repetir, y tienden a ser ignorados.
- Suelen conllevar un cambio fundamental en el diseño.
- Principios de Concurrencia:
- Separar el código concurrente del resto del código.
- Encapsular los datos compartidos (y evitarlos al máximo).
- Mejor hacer copias de los datos en vez de compartirlos (a costa de un mayor uso de la memoria).
- Solo un método por dato compartido en la misma clase compartida.
- Las secciones de código bloqueadas deben ser lo más pequeñas posible.
- Usar clases y objetos de concurrencia ya existentes en el lenguaje y el framework.
- Tests:
- No descartes ningún error, investiga todos, aunque pasen muy poco a menudo.
- Primero que nada, estar seguro de que el código sin concurrencia funciona bien.
- Hacer también pruebas de carga, con muchos procesos (más que procesadores).
- Probar muchas configuraciones distintas, cuantas más mejor, aunque sean muy raras.
- La gestión de procesos suele ser distinta en las diferentes arquitecturas, probarlas en todas ellas.
- Tener test que fuercen fallos y errores de concurrencia, para ver cómo reacciona el programa.
Malos Olores
- Comentarios:
- C1 Información fuera de lugar (debería estar en otro sistema, fuera del código).
- C2 Comentario obsoleto.
- C3 Comentario redundante (dice lo mismo que el código).
- C4 Comentario escrito pobremente (no se entiende, es vago, gramática y ortografía pobres, etc.)
- C5 Código comentado (hay que borrarlo, siempre).
- Entorno:
- E1 La construcción del proyecto requiere de más de un paso.
- E2 Pasar los test requiere de más de un paso.
- Funciones:
- F1 Demasiados argumentos (3 ya son muchos).
- F2 Argumentos de salida.
- F3 Argumentos de bandera (indicio de que la función hace más de una cosa).
- F4 Función muerta (hay que borrarla si ya no está siendo usada por nadie).
- General:
- G1 Múltiples lenguajes en un mismo archivo de código fuente (C#, HTML, XML, etc.)
- G2 El comportamiento obvio (ej. según el nombre) no está implementado.
- G3 Comportamiento incorrecto de los valores de frontera.
- G4 Saltarse los mecanismos de seguridad.
- G5 Código o funcionalidad duplicada.
- G6 Código en un nivel de abstracción equivocado.
- G7 Las clases base dependen de sus clases derivadas.
- G8 Demasiada información.
- G9 Código muerto (que nunca es ejecutado, bajo ninguna circunstancia. Hay que borrarlo).
- G10 Separación vertical (mucha separación entre declaración y uso).
- G11 Inconsistencias (no usar estándares, convenciones, guías, etc.)
- G12 Código desordenado o confuso.
- G13 Acoplamiento artificial (no tiene sentido y se ha hecho por conveniencia puntual).
- G14 Uso excesivo de miembros de otras clases (envidia).
- G15 Argumentos selectores (una funcionalidad u otra según el valor del selector).
- G16 Abreviar los nombres (ej. “dLbrl” en vez de “diaLaboral”).
- G17 Responsabilidad fuera de lugar.
- G18 Static inapropiado (sobre todo si pudiera ser una función polimórfica).
- G19 No usar variables intermedias explicativas (sobre todo en cálculos complejos).
- G20 Una función no hace lo que su nombre indica.
- G21 Implementar algoritmos que no entiendes (sobre todo con copiar-pegar).
- G22 Hacer suposiciones sobre códigos que se consumen.
- G23 Usar condicionales en vez de polimorfismo.
- G24 No seguir los estándares.
- G25 Usar literales en vez de constantes con nombre.
- G26 Código impreciso o ambiguo.
- G27 Dar a las convenciones más peso que a la estructura (ej. switch sobre polimorfismo).
- G28 Condicionales complejos sin encapsular (en funciones con nombres descriptivos).
- G29 Condicionales negativos o con dobles negaciones (mejor usar siempre en positivo).
- G30 Función que hace más de una cosa.
- G31 Acoplamientos temporales ocultos (no dejar claro el orden de llamada de ciertas funciones).
- G32 Uso de estructuras arbitrarias (no estando claro las razones para ello).
- G33 Valores de frontera desperdigados (hay que centralizarlos en un único sitio).
- G34 Funciones que acceden a diversos niveles de abstracción.
- G35 Desperdigar configuraciones de alto nivel en niveles de abstracción inferiores.
- G36 Usar llamadas encadenadas a funciones (si no devuelven objeto conocido por la clase). [Demeter]
- Lenguaje:
- L1 Usar importadores concretos (más acoplamiento) si el lenguaje permite importaciones masivas.
- L2 Heredar constantes (poner constantes en una interfaz).
- L3 Usar constantes en vez de enumeraciones.
- Nombres:
- N1 Nombre no descriptivo.
- N2 Nombre no corresponde con el nivel de abstracción.
- N3 No usa nomenclatura estándar.
- N4 Nombre ambiguo.
- N5 Nombre demasiado corto (a mayor alcance, más largo y conciso debe ser un nombre).
- N6 Uso de prefijos o sufijos redundantes o inútiles (ej. notación Húngara).
- N7 El nombre no refleja toda la funcionalidad.
- Tests:
- T1 Faltan tests, son insuficientes.
- T2 No usar una herramienta de cobertura de código.
- T3 No programar tests triviales.
- T4 No probar las condiciones de frontera.
- T5 No hacer más tests “parecidos” cuando se encuentra un error.
- T6 Test unitarios lentos.