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

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.