La filosofía del diseño de software de John Ousterhout
La filosofía del diseño de software de John Ousterhout

Este libro aborda el tema del diseño de software: cómo descomponer sistemas de software complejos en módulos (como clases y
métodos) que se pueden implementar de forma relativamente independiente.

El libro primero presenta el problema fundamental en el diseño de software, que es la gestión de la complejidad.

Luego analiza cuestiones filosóficas sobre cómo abordar el proceso de diseño de software y presenta una colección de principios de diseño para aplicar durante el diseño de software.

El libro también presenta un conjunto de banderas rojas que identifican problemas de diseño.

Puedes aplicar las ideas de este libro para minimizar la complejidad de los grandes sistemas de software, de modo que pueda escribir software de manera más rápida y económica.


A continuación tienes un resumen y notas de La filosofía del diseño de software , 2nd Edition de John Ousterhout. El resumen en inglés es de Elvis Chidera y puedes encontrar aquí, en su blog personal.

No esperes ninguna maravilla de la traducción.

El problema más fundamental en informática es la descomposición de problemas: cómo tomar un problema complejo y dividirlo en partes que puedan resolverse de forma independiente.

Contenidos

Capítulo 1 Introducción

  1. Los programadores no están sujetos a limitaciones prácticas como las leyes de la física, sino a su capacidad limitada para comprender los sistemas que crean.
  2. La complejidad dificulta la comprensión, lo que impide el desarrollo y genera errores.
  3. La complejidad engendra complejidad.
  4. Cuanto mayor sea el programa y el número de personas que trabajan en él, más difícil será gestionar la complejidad.
  5. Hay un límite de cuánto puede ayudar una herramienta a lidiar con la complejidad.
  6. Hay dos enfoques generales para combatir la complejidad:
    • Eliminación de la complejidad: haga que el código sea más simple y más obvio. Ej: eliminando casos especiales.
    • Encapsulación de complejidad: en módulos relativamente independientes (también conocidos como modular design), para que los programadores puedan trabajar en un sistema sin estar expuestos a toda su complejidad a la vez.
  7. Debido a que el software es tan maleable, el diseño de software es un proceso continuo que abarca todo el ciclo de vida de un sistema de software.
  8. Es más fácil ver los problemas de diseño en el código de otra persona que en el tuyo.
  9. Toda regla tiene sus excepciones, y todo principio tiene sus límites. Si llevas cualquier idea de diseño al extremo, probablemente terminarás en un mal lugar.

Notas

  1. El autor habló sobre cómo las prácticas ágiles pueden reducir la complejidad porque los ingenieros pueden cambiar continuamente un diseño a medida que comprenden mejor el espacio del problema.
  2. Sin embargo, en el límite, los cambios incrementales pueden conducir a un mal diseño en general cuando los ingenieros no tienen una visión holística del sistema.
    Editar: el autor finalmente menciona esto en el capítulo 19.

Capítulo 2 — La naturaleza de la complejidad

Complejidad definida

  1. La complejidad es cualquier cosa relacionada con la estructura de un sistema de software que hace que sea difícil de entender y, por lo tanto, modificar el sistema.
  2. La complejidad general de un sistema ( C) está determinada por la complejidad de cada parte p ( cₚ) ponderada por la fracción de tiempo que los desarrolladores dedican a trabajar en esa parte ( tₚ).

C=p = 0∑norteCpagtpag

  1. Según la fórmula anterior, las interfaces deben diseñarse para que el caso común sea lo más simple posible:

Si la API para una función de uso común obliga a los usuarios a aprender sobre otras funciones que se usan con poca frecuencia , esto aumenta la carga cognitiva de los usuarios que no necesitan las funciones que se usan con poca frecuencia.

Síntomas de la complejidad

  1. Amplificación de cambios: un cambio aparentemente simple requiere modificaciones de código en muchos lugares diferentes.
  2. Carga cognitiva: cuánto necesita saber un desarrollador para completar una tarea.

A veces, un enfoque que requiere más líneas de código es en realidad más simple, porque reduce la carga cognitiva.

  1. Incógnitas desconocidas: no es obvio qué piezas de código deben modificarse o qué información debe tener un desarrollador para completar una tarea.

Causas de la complejidad

  1. Dependencia:
  • Una dependencia existe cuando una determinada pieza de código no se puede entender y modificar de forma aislada.
  • Por ejemplo: existe una dependencia entre una función y todos sus llamadores.
  • Las dependencias conducen a la amplificación del cambio y a una alta carga cognitiva.
  • Debido a que las dependencias son una parte fundamental del software que no se puede eliminar por completo, el objetivo es reducirlas y hacer que lo que quede sea simple y obvio.
  1. Oscuridad:
  • Ocurre cuando la información importante no es obvia.
  • La oscuridad crea incógnitas desconocidas y contribuye a la carga cognitiva.

La complejidad es incremental

  1. Muerte por mil cortes: La complejidad no es causada por un solo error catastrófico; se acumula en muchos pedazos pequeños.

Notas

  1. La definición de complejidad aquí es subjetiva: un ingeniero junior puede encontrar un proyecto difícil de entender o la tarea en cuestión puede ser inherentemente compleja.
  2. El autor no distingue explícitamente entre complejidad esencial y accidental.
  3. La modularidad sugerida en el capítulo 1 puede conducir a la oscuridad.
    Editar: el autor finalmente llega a esto en el capítulo 4.
  4. La complejidad general debe verse en relación con un individuo/equipo: un solo ingeniero o equipo puede trabajar en un módulo más que cualquier otro ingeniero de la empresa combinado.

Capítulo 3: El código de trabajo no es suficiente

Programación táctica

  1. La programación táctica se enfoca en terminar tareas (es decir, enviar código de trabajo) rápidamente. Cualquier esfuerzo como la refactorización que no pague dividendos ahora no tiene prioridad.
  2. Se suma a la complejidad debido a decisiones de diseño miopes que generalmente son deficientes.

Programación estratégica

  1. La programación estratégica requiere una mentalidad de inversión para mejorar el diseño del sistema, incluso si no es el camino más rápido para terminar su proyecto actual.
  2. La inversión puede ser proactiva (pensar en el futuro) o reactiva (incorporar nuevos conocimientos al diseño existente).
  3. A corto plazo, estas inversiones retrasan el desarrollo. A largo plazo, aceleran el desarrollo.

¿Cuánto invertir?

  1. Nuestra experiencia en el dominio de un problema mejora a medida que trabajamos en él. Por lo tanto, una gran inversión inicial (como el modelo en cascada) no funciona debido a los detalles que generalmente surgen solo durante la implementación o el uso real.
  2. El mejor enfoque es hacer muchas pequeñas inversiones de forma continua.
  3. El autor sugiere gastar alrededor 10–20%del tiempo total de desarrollo en inversiones. Esta cantidad es lo suficientemente pequeña como para que no afecte significativamente sus horarios, pero lo suficientemente grande como para producir beneficios significativos con el tiempo.

Notas

  1. En mi experiencia, los equipos efectivos saben cuándo cambiar entre enfoques tácticos y estratégicos.
  2. El autor desacredita el argumento táctico de las startups. Mi contraargumento es que he visto fracasar muchos proyectos “bien diseñados”. De hecho, una gran trampa en la que veo caer a los grandes ingenieros es dedicar una cantidad significativa de tiempo a un producto que no ha sido validado. ¿Cuál es el punto?

Capítulo 4 — Los módulos deben ser profundos

Diseño modular

  1. Los módulos pueden adoptar muchas formas, como clases, subsistemas o servicios.
  2. Para gestionar las dependencias, pensamos en cada módulo en dos partes:
    • La interfaz consta de todo lo que un desarrollador que trabaja en un módulo diferente debe saber para usar el módulo dado.
    • La implementación consiste en el código que lleva a cabo las promesas realizadas por la interfaz.
  3. A los efectos de este libro, un módulo es cualquier unidad de código que tiene una interfaz y una implementación.
  4. Los mejores módulos son aquellos cuyas interfaces son mucho más simples que sus implementaciones. Dichos módulos tienen dos ventajas:
    • Una interfaz simple minimiza la complejidad que un módulo impone al resto del sistema.
    • Si un módulo se modifica de una manera que no cambia su interfaz, ningún otro módulo se verá afectado por la modificación.

¿Qué hay en una interfaz?

  1. Si un desarrollador necesita conocer una información en particular para usar un módulo, entonces esa información es parte de la interfaz del módulo.
  2. Una interfaz tiene dos partes:
    • Partes formales : se especifican explícitamente en el código y se pueden aplicar mediante un lenguaje de programación. Por ejemplo: la firma de un método, una clase de métodos y propiedades públicos, etc.
    • Partes informales : se especifican en la documentación y no pueden ser aplicadas por un lenguaje de programación. Incluye su comportamiento de alto nivel como: lo que hace una función cuando se llama.

abstracciones

  1. Una abstracción es una vista simplificada de una entidad, que omite detalles sin importancia.
  2. Un módulo proporciona una abstracción en la forma de su interfaz: La interfaz presenta una vista simplificada de la funcionalidad del módulo; los detalles de la implementación no son importantes desde el punto de vista de la abstracción del módulo, por lo que se omiten de la interfaz.
  3. Dos trampas con las abstracciones:
    • Inclusión de detalles sin importancia : aumenta la carga cognitiva ya que la abstracción es más complicada de lo necesario.
    • Exclusión de detalles importantes : Esto da como resultado la oscuridad.

Módulos profundos

  1. Los mejores módulos son aquellos que brindan una funcionalidad poderosa pero tienen interfaces simples.
  2. El mecanismo de E/S de archivos de Unix es un buen ejemplo de módulos profundos.
  3. La profundidad del módulo es una forma de pensar sobre el costo versus el beneficio:
    • El beneficio proporcionado por un módulo es su funcionalidad.
    • El costo de un módulo (en términos de complejidad del sistema) es su interfaz.

Módulos poco profundos

  1. Un módulo superficial es aquel cuya interfaz es complicada en relación con la funcionalidad que proporciona.
  2. Los módulos poco profundos no ayudan mucho en la batalla contra la complejidad, porque el beneficio que brindan (no tener que aprender cómo funcionan internamente) se ve anulado por el costo de aprender y usar sus interfaces.
  3. Los módulos pequeños tienden a ser poco profundos.

clasitis

  1. La sabiduría convencional en programación es que las clases deben ser pequeñas, no profundas:
    • Divida las clases más grandes en otras más pequeñas.
    • Cualquier método más largo que Nlas líneas debe dividirse en múltiples métodos
  2. El extremo del enfoque de “las clases deberían ser pequeñas” es un síndrome que el autor llama clasitis:

La classitis puede resultar en clases que son individualmente simples, pero aumenta la complejidad del sistema general. Las clases pequeñas no aportan mucha funcionalidad, por lo que tiene que haber muchas, cada una con su propia interfaz. Estas interfaces se acumulan para crear una enorme complejidad a nivel del sistema. Las clases pequeñas también resultan en un estilo de programación verboso, debido al modelo requerido para cada clase.

Notas

  1. Debido a que todas las abstracciones tienen fugas, en la práctica encontrará que los cambios que no cambian la interfaz de un módulo aún pueden requerir cambios para los consumidores. Ver la ley de Hyrum.

Capítulo 5 — Ocultación (y fuga) de información

Ocultación de información

  1. El ocultamiento de información ocurre cuando cada módulo encapsula algunos conocimientos, que representan decisiones de diseño. El conocimiento está integrado en la implementación del módulo pero no aparece en su interfaz.
  2. La ocultación de información también se aplica dentro de una clase:
    • Diseñe los métodos privados para que cada método encapsule alguna información o capacidad y la oculte del resto de la clase.
    • Minimice el número de lugares donde se usa cada variable de instancia.

Fuga de información

  1. La fuga de información ocurre cuando una decisión de diseño se refleja en múltiples módulos.
  2. La fuga de información es lo opuesto a la ocultación de información.
  3. La fuga puede ocurrir directamente a través de la interfaz de un módulo o indirectamente a través del conocimiento implícito utilizado en diferentes módulos (como la estructura esperada de un archivo).

descomposición temporal

  1. En la descomposición temporal, el orden de ejecución se refleja en la estructura del código: las operaciones que ocurren en diferentes momentos están en diferentes métodos o clases. Si el mismo conocimiento se utiliza en diferentes puntos de la ejecución, se codifica en varios lugares, lo que provoca una fuga de información.
  2. La ocultación de información a menudo se puede mejorar haciendo una clase un poco más grande. Esto nos permite encapsular conocimientos específicos en un solo lugar y elevar el nivel de la interfaz (es decir, en lugar de exponer muchos pasos intermedios de bajo nivel, exponga una pequeña cantidad de pasos de alto nivel).

Capítulo 6: Los módulos de propósito general son más profundos

  1. Las interfaces de propósito general tienen muchas ventajas sobre las de propósito especial:
    • Tienden a ser más simples, con menos métodos que son más profundos.
    • También proporcionan una separación más clara entre clases, mientras que las interfaces de propósito especial tienden a filtrar información entre clases.
  2. Hacer que sus módulos tengan un propósito general es una de las mejores maneras de reducir la complejidad general del sistema.

Notas

  1. Todo esto se relaciona con la estratificación (de la que se hablará en el siguiente capítulo): haga módulos genéricos de bajo nivel y módulos especializados de alto nivel.

Capítulo 7 — Capa diferente, abstracción diferente

Cada pieza de infraestructura de diseño agregada a un sistema, como una interfaz, un argumento, una función, una clase o una definición, agrega complejidad, ya que los desarrolladores deben aprender sobre este elemento. Para que un elemento proporcione una ganancia neta frente a la complejidad, debe eliminar parte de la complejidad que estaría presente en ausencia del elemento de diseño. De lo contrario, es mejor implementar el sistema sin ese elemento en particular. Por ejemplo, una clase puede reducir la complejidad al encapsular la funcionalidad para que los usuarios de la clase no tengan que ser conscientes de ello.

  1. Los sistemas de software están compuestos por capas, donde las capas superiores utilizan las instalaciones proporcionadas por las capas inferiores y cada capa tiene una abstracción (nivel) diferente.

Método de transferencia

  1. Un método de transferencia es aquel que no hace nada excepto pasar sus argumentos a otro método, generalmente con la misma API que el método de transferencia. Esto suele indicar que no existe una división limpia de responsabilidades entre las clases.
  2. La interfaz de una parte de la funcionalidad debe estar en la misma clase que implementa la funcionalidad.
  3. Formas de eliminar los métodos de transferencia:
    • Permita que las personas que llaman invoquen el método directamente
    • Redistribuir la funcionalidad para evitar llamadas entre los dos
    • Combinando las clases

¿Cuándo está bien la duplicación de interfaz?

  1. Tener métodos con la misma firma no siempre es malo. Lo importante es que cada nuevo método debe aportar una funcionalidad significativa. Los métodos de transferencia son malos porque no aportan ninguna funcionalidad nueva.

Decoradores

  1. Un objeto decorador toma un objeto existente y amplía su funcionalidad; proporciona una API similar o idéntica al objeto subyacente y sus métodos invocan los métodos del objeto subyacente.
  2. El patrón de diseño del decorador (también conocido como “envoltorio”) fomenta la duplicación de API entre capas.
  3. Debido a que los decoradores tienden a ser superficiales, vale la pena considerar opciones alternativas:
    • ¿Podría agregar la nueva funcionalidad directamente a la clase subyacente, en lugar de crear una clase de decorador?
    • Si la nueva funcionalidad está especializada para un caso de uso particular, ¿tendría sentido fusionarla con el caso de uso, en lugar de crear una clase separada?
    • ¿Podría fusionar la nueva funcionalidad con un decorador existente, en lugar de crear un nuevo decorador?
    • Pregúntese si la nueva funcionalidad realmente necesita envolver la funcionalidad existente: ¿podría implementarla como una clase independiente que sea independiente de la clase base?

Variables de paso

  1. Otra forma de duplicación de API entre capas es una variable de transferencia, que es una variable que se transmite a través de una larga cadena de métodos.
  2. Formas de eliminar las variables de paso:
    • Almacenar la variable en un objeto compartido
    • Usar variables globales
    • Utilice un objeto de contexto que almacene toda la información de todo el sistema, como un valor de tiempo de espera y contadores de rendimiento; una referencia al contexto se almacena en todos los objetos cuyos métodos necesitan acceder a él.

Notas

  1. El objeto de contexto introduce oscuridad como el autor mencionado: por qué existe un valor y dónde se usa. ¿Cuándo son mejores las variables de paso que los objetos de contexto?
  2. Los objetos de contexto también podrían existir en un ámbito más local.

Capítulo 8 — Tirar de la complejidad hacia abajo

  1. Es más importante que un módulo tenga una interfaz simple que una implementación simple: la mayoría de los módulos tienen más usuarios que desarrolladores, por lo que es mejor que los desarrolladores sufran que los usuarios.
  2. Reducir la complejidad tiene más sentido si:
    • La complejidad que se reduce está estrechamente relacionada con la funcionalidad existente de la clase.
    • Reducir la complejidad resultará en muchas simplificaciones en otras partes de la aplicación.
    • Reducir la complejidad simplifica la interfaz de la clase.

Notas

  1. No estoy de acuerdo con el autor en cuanto a evitar los parámetros de configuración. Cualquiera que haya creado un sistema complejo de nicho se ha topado con un módulo que usaba valores inadecuados para la situación actual.
  2. Tener ajustes o parámetros de configuración no es un fallo de diseño. Se deben proporcionar valores predeterminados razonables, pero el usuario debe poder anularlos en la mayoría de los casos.

Capítulo 9: ¿Mejor juntos o mejor separados?

  1. Una de las preguntas más fundamentales en el diseño de software es la siguiente: dadas dos piezas de funcionalidad, ¿deberían implementarse juntas en el mismo lugar o sus implementaciones deberían estar separadas?
  2. Si bien una gran cantidad de módulos pequeños conducen a módulos individuales más simples, generalmente aumentan la complejidad general del sistema:
    • Complejidad por la cantidad de módulos: es difícil hacer un seguimiento de todos ellos o encontrar un módulo deseado. La subdivisión generalmente da como resultado más interfaces, y cada nueva interfaz agrega complejidad.
    • Puede requerir código adicional para administrar múltiples módulos.
    • Crea separación:
      • Para módulos realmente independientes, la separación es buena: permite al desarrollador concentrarse en un solo módulo a la vez.
      • Para los módulos dependientes, la separación es mala: la separación hace que sea más difícil para los desarrolladores ver los módulos al mismo tiempo, o incluso darse cuenta de su existencia.
    • Puede resultar en la duplicación.
  3. El código relacionado debe combinarse. Indicaciones de que el código está relacionado:
    • Comparten conocimientos específicos: X e Y tienen conocimientos sobre Z
    • Se usan juntos de forma bidireccional: X se usa con Y e Y se usa con X
    • Se superponen conceptualmente: hay una categoría simple de nivel superior que incluye ambas piezas de código.
    • Es difícil entender una de las piezas de código sin mirar la otra.

Métodos de división y unión.

  1. Cada método debe hacer una cosa y hacerlo completamente .

  2. Debería ser posible comprender cada método de forma independiente. Si no puede comprender la implementación de un método sin comprender también la implementación de otro, es una señal de alerta.

    Esta es una instancia de una bandera roja general: si dos piezas de código están físicamente separadas, pero cada una solo puede entenderse mirando a la otra.

  3. Los métodos se pueden dividir si el método original:

  • Se puede dividir en subtareas de propósito general independientes
  • Tiene una interfaz compleja y hace demasiado, se puede dividir en varios métodos, siempre que las personas que llaman no siempre tengan que usarlos todos juntos en un orden exacto.
  1. Hay situaciones en las que un sistema puede simplificarse uniendo métodos:
  • Los métodos de unión pueden reemplazar dos métodos superficiales con un método más profundo
  • Podría eliminar la duplicación de código.
  • Podría eliminar las dependencias entre los métodos originales o las estructuras de datos intermedias.
  • Podría dar como resultado una mejor encapsulación, de modo que el conocimiento que antes estaba presente en varios lugares ahora esté aislado en un solo lugar.
  • Podría resultar en una interfaz más simple.

Capítulo 10 — Definir errores fuera de existencia

  1. Las excepciones lanzadas por una clase son parte de su interfaz.
  2. Cuatro técnicas para reducir el número de controladores de excepciones:
    • Defina errores fuera de existencia mediante el diseño de API que hagan que una excepción sea imposible/innecesaria.
    • Enmascare las excepciones detectándolas y manejándolas a bajo nivel.
    • Agregación de excepciones al manejar muchas excepciones con una sola pieza de código (en un nivel superior).
    • Simplemente bloquee cuando una excepción sea rara y difícil de manejar.

Notas

  1. Otra técnica es tener una parte limpia y otra sucia del sistema. El análisis se realiza en la parte sucia y son posibles excepciones. Los objetos analizados se analizan en la parte limpia donde las excepciones son raras. Ver: Analizar no validar .

Capítulo 11 — Diséñalo dos veces

  1. Diseñar software es difícil, por lo que es poco probable que sus primeros pensamientos sobre cómo estructurar un módulo o sistema produzcan el mejor diseño. Obtendrá un resultado mucho mejor si considera múltiples opciones para cada decisión importante de diseño.

Notas

  1. Mi parte favorita del libro. Me recuerda a un párrafo en SICP:

“Todo programa de computadora es un modelo, tramado en la mente, de un proceso real o mental. Estos procesos, que surgen de la experiencia y el pensamiento humanos, son enormes en número, intrincados en detalles y, en cualquier momento, sólo parcialmente comprendidos. Son modelados a nuestra satisfacción permanente rara vez por nuestros programas de computadora. Por lo tanto, aunque nuestros programas son colecciones discretas de símbolos, mosaicos de funciones entrelazadas cuidadosamente elaborados a mano, evolucionan continuamente: los cambiamos a medida que nuestra percepción del modelo se profundiza, amplía, generaliza hasta que el modelo finalmente alcanza un lugar metaestable dentro de otro modelo más con el cual luchamos”

Capítulo 12: ¿Por qué escribir comentarios? Las cuatro excusas

  1. La idea general detrás de los comentarios es capturar información que estaba en la mente del diseñador pero que no se pudo representar en el código.

i. El buen código se autodocumenta

  1. Un buen código reduce la necesidad y la cantidad de comentarios, no elimina la necesidad de comentarios.
  2. Hay una cantidad significativa de información de diseño que no se puede representar en el código.
  3. Si bien el código es la fuente de la verdad, es doloroso y lleva mucho tiempo esperar que las personas lean el código para comprender la interfaz.

Los comentarios son fundamentales para las abstracciones: una abstracción es una vista simplificada de una entidad, que conserva información esencial pero omite detalles que pueden ignorarse con seguridad. Si los usuarios deben leer el código de un método para usarlo, entonces no hay abstracción: se expone toda la complejidad del método.

ii. no tengo tiempo para escribir comentarios

  1. Los buenos comentarios marcan una gran diferencia en la capacidad de mantenimiento del software, por lo que el esfuerzo dedicado a ellos se amortizará rápidamente.
  2. Esta excusa sacrifica la velocidad a largo plazo, por la velocidad a corto plazo.

iii. Los comentarios quedan desactualizados y se vuelven engañosos

  1. Actualizar los documentos no toma tanto tiempo como actualizar el código; los equipos disciplinados deben sincronizar los documentos como parte del proceso de desarrollo.

IV. Todos los comentarios que he visto no valen nada.

  1. Esto se puede solucionar aprendiendo a escribir documentación sólida.

Notas

  1. Los comentarios sin valor generalmente se pueden atribuir a la ley de Goodhart . Las personas escriben comentarios porque hay algún incentivo para escribir comentarios, pero como no les importa mucho, solo satisfacen el requisito mínimo o fácilmente medible: ¿está documentado un fragmento de código?

Capítulo 13: Los comentarios deben describir cosas que no son obvias del código

elegir convenciones

  1. Las convenciones tienen dos propósitos:
    • Garantizan la coherencia, lo que hace que los comentarios sean más fáciles de leer y comprender.
    • Ayudan a garantizar que realmente escriba comentarios: las restricciones liberan, las libertades restringen.
  2. La mayoría de los comentarios pertenecen a una de las siguientes categorías:
    • Interfaz : un bloque de comentarios que precede inmediatamente y describe un módulo, como una clase, una estructura de datos o un método.
    • Miembro de estructura de datos : un comentario junto a la declaración de un campo en una estructura de datos, como una variable de instancia o una variable estática para una clase.
    • Comentario de implementación : un comentario dentro del código de un método, que describe cómo funciona el código internamente.
    • Comentario de módulo cruzado : un comentario que describe las dependencias que cruzan los límites del módulo.

No repitas el código

  1. Un comentario no es útil si la información que contiene ya es obvia a partir del código que se encuentra junto a él. Un ejemplo de esto es cuando el comentario usa las mismas palabras que componen el nombre de lo que está describiendo.

Los comentarios de bajo nivel agregan precisión

  1. Los comentarios aumentan el código proporcionando información a un nivel diferente de detalle.
  2. Algunos comentarios proporcionan información a un nivel más bajo y más detallado que el código; estos comentarios agregan precisión al aclarar el significado exacto del código.
  3. Otros comentarios proporcionan información a un nivel más alto y abstracto que el código; estos comentarios ofrecen intuición, como el razonamiento detrás del código, o una forma más simple y abstracta de pensar sobre el código.
  4. Es probable que los comentarios del mismo nivel que el código lo repitan.
  5. Al documentar una variable, piense en sustantivos, no en verbos. En otras palabras, concéntrese en lo que representa la variable, no en cómo se manipula.

Documentación de la interfaz

  1. Los comentarios de la interfaz brindan información que alguien necesita saber para usar una clase o método; definen la abstracción.
  2. Los comentarios de implementación describen cómo funciona internamente una clase o método para implementar la abstracción.
  3. Si los comentarios de la interfaz también deben describir la implementación, entonces la clase o el método son superficiales.
  4. La documentación de implementación contamina la interfaz.

Comentarios de implementación: qué y por qué, no cómo

  1. La mayoría de los métodos son tan cortos y simples que no necesitan ningún comentario de implementación: dado el código y los comentarios de la interfaz, es fácil averiguar cómo funciona un método.
  2. El objetivo principal de los comentarios de implementación es ayudar a los lectores a comprender qué está haciendo el código (no cómo lo hace).

Notas

  1. Algunas de las cosas que el autor dijo que deberían estar en los comentarios pueden estar en código:
  • Una unidad variable puede estar en su nombre.
  • La validación y la información sobre los parámetros del método pueden estar en clases de dominio ricas.
  • Los nombres de variables se pueden usar para la documentación; tome el ejemplo de la subcadena, puede tener (startInclusive, endExclusive)en lugar de (start, end): el primero no necesita documentación adicional, el segundo sí.
  1. “Si su código está siendo revisado y un revisor le dice que algo no es obvio, no discuta con ellos; si un lector piensa que no es obvio, entonces no es obvio

    Si bien esto suele ser cierto, no siempre es cierto. Además, “obvio” es subjetivo: puede hacer que su código sea obvio para una persona y se vuelva oscuro para otra.

  2. Tenga en cuenta que las personas deben usar palabras no relacionadas para describir un fenómeno. Por ejemplo: combinar la falta de familiaridad con la complejidad.

Capítulo 14 — Elegir nombres

  1. Los buenos nombres son una forma de documentación:
    • Hacen que el código sea más fácil de entender.
    • Reducen la necesidad de otra documentación.
    • Facilitan la detección de errores.
  2. Los nombres son una forma de abstracción: proporcionan una forma simplificada de pensar sobre una entidad subyacente más compleja.
  3. Los nombres deben ser precisos: si el nombre de una variable o método es lo suficientemente amplio como para hacer referencia a muchas cosas diferentes, entonces no transmite mucha información al desarrollador y es más probable que se haga un uso indebido de la entidad subyacente.
  4. Si es difícil encontrar un nombre simple para una variable o método que cree una imagen clara del objeto subyacente, eso es una pista de que el objeto subyacente puede no tener un diseño limpio.
  5. Use nombres consistentemente: la nomenclatura consistente reduce la carga cognitiva de la misma manera que reutilizar una clase común: una vez que el lector ha visto el nombre en un contexto, puede reutilizar su conocimiento y hacer suposiciones instantáneamente cuando ve el nombre en un contexto diferente.

“Cuanto mayor sea la distancia entre la declaración de un nombre y sus usos, más largo debería ser el nombre”. — Andrew Gerrand

Capítulo 15 — Escribe los comentarios primero

  1. Utilice los comentarios como parte del proceso de diseño.
  2. Los comentarios retrasados son [generalmente] malos comentarios.

Notas

  1. Soy un gran admirador del “desarrollo basado en la documentación”, por lo que este capítulo resonó conmigo.
  2. En múltiples ocasiones, simplifiqué mis diseños o descubrí mejores enfoques escribiendo primero documentación de alto nivel.
  3. Sin embargo, el autor parece proponer esto como la multitud TDD propone TDD. A veces, la claridad se obtiene escribiendo primero el código. En mi opinión, no hay nada de malo en “piratear código” localmente; a veces, el código es el mejor lienzo.

Capítulo 16: Modificación del código existente

  1. Idealmente, cuando haya terminado con cada cambio, el sistema tendrá la estructura que hubiera tenido si lo hubiera diseñado desde el principio con ese cambio en mente.
  2. Si no está mejorando el diseño, probablemente lo esté empeorando.
  3. La mejor manera de asegurarse de que los comentarios se actualicen es colocarlos cerca del código que describen.
  4. Cuanto más lejos esté un comentario del código que describe, más abstracto debería ser (esto reduce la probabilidad de que el comentario sea invalidado por cambios en el código).
  5. Los comentarios pertenecen al código, no al registro de confirmación.
  6. Enlace a recursos (externos), no los duplique: es importante que los lectores puedan encontrar fácilmente toda la documentación necesaria para comprender su código, pero eso no significa que tenga que escribir toda esa documentación.

Notas

  1. A veces, es útil copiar recursos externos en los documentos: la información se puede mostrar de manera consistente y conveniente; y la información copiada no se ve afectada por enlaces muertos.

Capítulo 17 — Coherencia

  1. La consistencia crea un apalancamiento cognitivo: una vez que ha aprendido cómo se hace algo en un lugar, puede usar ese conocimiento para comprender de inmediato otros lugares que usan el mismo enfoque.

Capítulo 18: El código debe ser obvio

  1. El software debe estar diseñado para facilitar la lectura, no la facilidad de escritura.
  2. Si el código no es obvio, eso generalmente significa que hay información importante sobre el código que el lector no tiene.

Cosas que hacen que el código sea más obvio

  1. Uso juicioso del espacio en blanco.
  2. Comentarios: A veces no es posible evitar el código que no es obvio. Cuando esto sucede, es importante usar comentarios para compensar proporcionando la información que falta.

Cosas que hacen que el código sea menos obvio

  1. Programación basada en eventos: la programación basada en eventos dificulta seguir el flujo de control.

  2. Contenedores genéricos (como Pair en Java): los contenedores genéricos dan como resultado un código no obvio porque los elementos agrupados tienen nombres genéricos que oscurecen su significado.

  3. Diferentes tipos para declaración y asignación.

private List<Message> incomingMessageList;
...
incomingMessageList = new ArrayList<Message>();
  1. Código que viola las expectativas del lector.

Notas

  1. ¿Se podría resolver el problema de la programación basada en eventos con mejores herramientas?
  2. La oscuridad podría eliminarse si los objetos en un contenedor genérico son objetos de dominio enriquecido.

Capítulo 19: Tendencias de software

Programación y herencia orientada a objetos

  1. Se pueden utilizar métodos y variables privados para garantizar la ocultación de la información.
  2. La primera forma de herencia es la herencia de interfaz, en la que una clase principal define las firmas de uno o más métodos, pero no implementa los métodos. Cada subclase debe implementar las firmas.
  3. La herencia de interfaz ofrece ventajas frente a la complejidad al reutilizar la misma interfaz para múltiples propósitos. Permite que los conocimientos adquiridos para resolver un problema (por ejemplo, cómo usar una interfaz de E/S para leer y escribir archivos de disco) se utilicen para resolver otros problemas (como la comunicación a través de un socket de red).
  4. La segunda forma de herencia es la herencia de implementación. De esta forma, una clase principal define no solo firmas para uno o más métodos, sino también implementaciones predeterminadas. Las subclases pueden optar por heredar la implementación de un método del padre o anularla definiendo un nuevo método con la misma firma.
  5. La herencia de implementación puede reducir la cantidad de código que debe modificarse a medida que evoluciona el sistema (es decir, el problema de amplificación de cambios).
  6. La herencia de implementación crea dependencias entre la clase principal y cada una de sus subclases.
  7. Favorece la composición sobre la herencia de implementación.

Desarrollo ágil

  1. Uno de los elementos más importantes del desarrollo ágil es la noción de que el desarrollo debe ser incremental e iterativo.
  2. Uno de los riesgos del desarrollo ágil es que puede conducir a una programación táctica.

Pruebas unitarias

  1. Las pruebas facilitan la refactorización.

Desarrollo basado en pruebas

  1. Los problemas con el desarrollo basado en pruebas:
    • Enfoca la atención en hacer que funciones específicas funcionen, en lugar de encontrar el mejor diseño: esta es la programación táctica.
    • Es demasiado incremental: en cualquier momento, es tentador piratear la siguiente función para pasar la siguiente prueba.
  2. Escribir pruebas primero tiene más sentido cuando se corrigen errores.

Patrones de diseño

  1. Un patrón de diseño es un enfoque comúnmente utilizado para resolver un tipo particular de problema.
  2. El mayor riesgo con los patrones de diseño es la aplicación excesiva. No todos los problemas se pueden resolver limpiamente con un patrón de diseño existente; no intente forzar un problema en un patrón de diseño cuando un enfoque personalizado será más limpio.
  3. El uso de patrones de diseño no mejora automáticamente un sistema de software; solo lo hace si los patrones de diseño encajan.
  4. Al igual que con muchas ideas en el diseño de software, la noción de que los patrones de diseño son buenos no significa necesariamente que más patrones de diseño sean mejores.

Getters y setters

  1. El argumento para getters y setters es que permiten realizar funciones adicionales al obtener y configurar.
  2. Los getters y setters son métodos poco profundos (por lo general, solo una línea), por lo que agregan desorden a la interfaz de la clase sin proporcionar mucha funcionalidad.

Notas

  1. Los lenguajes modernos como Kotlin eliminaron la necesidad de getters/setters tradicionales.
  2. Las pruebas mal escritas o la mala infraestructura de pruebas pueden dificultar la refactorización.

Capítulo 20 — Diseño para el desempeño

  1. Medir antes de modificar: le permite identificar problemas reales de rendimiento y crea una línea de base para comparar sus cambios de rendimiento.